mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-21 03:18:56 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19b9e82afd | ||
|
|
01ab7edc8c | ||
|
|
bd0b274973 | ||
|
|
7b7a86201d | ||
|
|
5317fb0d5c | ||
|
|
022958f417 | ||
|
|
3431eee81a | ||
|
|
470326830a | ||
|
|
b6225b6ae4 | ||
|
|
f38f0ffa25 | ||
|
|
6c939d72f8 | ||
|
|
d083b27e91 |
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-net-postgis-types</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,7 +41,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -60,13 +60,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>composites</artifactId>
|
||||
|
||||
+282
-1
@@ -136,8 +136,31 @@ already recognise, which is worth spelling out explicitly so it's easy to "grok
|
||||
or the query can't group correctly at all. Fixed so `REF` always contributes its association name to the
|
||||
root `select(...)` (deduped against any existing `NESTED_ONE`/`NESTED_MANY` fetch of the same path).
|
||||
|
||||
### Read-only entity memory overhead: `InterceptReadOnly`
|
||||
**Bug found and fixed (validation phase, testing against `central-access`): primitive-typed field +
|
||||
nullable intermediate hop = unboxing `NullPointerException`.** A multi-hop `@DtoPath` (or `@DtoRef`,
|
||||
which is always 2-hop) null-guards each intermediate getter with a ternary, e.g.
|
||||
`(source.getOrganisation() == null ? null : source.getOrganisation().getId())`. That ternary's static
|
||||
type is always the boxed wrapper (`Long`), since one branch is the `null` literal - fine when the DTO
|
||||
field is itself a reference type (`Long organisationId`), but when the DTO field is a **primitive**
|
||||
(`long organisationId`), passing that boxed expression to the constructor auto-unboxes it, throwing an
|
||||
unhelpful `NullPointerException` at runtime whenever the relation really is `null`. This compiled clean
|
||||
and only failed at runtime with real (nullable) production data - exactly the kind of gap a hand-written
|
||||
mapper would defensively guard against (e.g. `cEbox.getOrganisation() == null ? 0 : ...getId()`) but
|
||||
generated code didn't.
|
||||
|
||||
Fixed in the generator: when a multi-hop `SCALAR`/`REF` property's DTO field type is primitive, the
|
||||
whole null-guarded chain is now wrapped in a small runtime helper (`io.ebean.DtoMapperSupport`) that
|
||||
resolves it safely:
|
||||
- **Default** (`@DtoPath` with no `failOnNull`, or any `@DtoRef`): silently defaults to the primitive's
|
||||
zero-equivalent value (`0`/`false`/etc.) - matches the old hand-written-mapper convention.
|
||||
- **`@DtoPath(failOnNull = true)`**: throws a clear `IllegalStateException` naming the offending property
|
||||
path instead, for callers who'd rather fail fast than silently mask a null they don't expect.
|
||||
|
||||
`@DtoRef` has no `failOnNull` attribute (it has no other attributes at all) - it always uses the
|
||||
default (silent zero) behaviour. See `PrimitiveNullPathDto`/`PrimitiveNullPathFailOnNullDto` /
|
||||
`TestPrimitiveNullPath` for regression coverage.
|
||||
|
||||
### Read-only entity memory overhead: `InterceptReadOnly`
|
||||
`setUnmodifiable(true)` isn't just a behavioural fail-fast flag - it also swaps the per-bean intercept
|
||||
implementation to `InterceptReadOnly`, which is deliberately minimal: just a `boolean[] loaded` (one flag
|
||||
per property) and a `boolean frozen`, plus the inherited owner reference and `fullyLoadedBean` flag. Compare
|
||||
@@ -701,6 +724,264 @@ callers must consume it via try-with-resources to ensure the underlying resource
|
||||
`DtoConverterManager`) are all constructed eagerly during `Database` startup, which can be
|
||||
triggered by whichever test class in the module happens to run first.
|
||||
|
||||
- **Fixed (validation phase, found via `central-access`): `@DtoPath` through a computed/derived
|
||||
getter now fails at compile time, with an explicit `requires()` escape hatch.** `@DtoPath`
|
||||
assumes every dotted segment names a real, fetchable Ebean bean property - so a path like
|
||||
`@DtoPath("currentMachine.organisationMachine.registrationPlate")`, where `getOrganisationMachine()`
|
||||
is a hand-written derived getter (not a real relation/column), used to **compile cleanly** (the
|
||||
codegen had no way to tell it apart from a real property from source alone) but **fail at
|
||||
runtime** with a `PersistenceException: No property found for [organisationMachine] in
|
||||
expression ...`, because the generated `FetchGroup` builder tried to `fetch`/`select` it as if it
|
||||
were a real Ebean property.
|
||||
- Two genuinely separate sub-problems: (1) *detecting* that a path segment isn't a real,
|
||||
fetchable property - solvable at compile time, since a real persistent property always has a
|
||||
backing field (Ebean requires one to enhance), checked via `javax.lang.model`
|
||||
(`ElementFilter.fieldsIn(...)` over the type + superclass chain, see `DtoMappingReader.hasField(...)`);
|
||||
versus (2) *knowing what the computed getter needs fetched* to execute safely - not solvable at
|
||||
compile time without full static/bytecode analysis of the getter's method body, out of scope.
|
||||
- Resolution: don't attempt to infer (2) automatically. When `DtoMappingReader` detects a `@DtoPath`
|
||||
segment with no backing field, it now fails fast at compile time (`ctx.logError(...)`) unless the
|
||||
developer explicitly declares the real entity paths that must be fetched via
|
||||
`@DtoPath(requires = {...})` (dot-notation, same convention as `@DtoPath`'s own `value()`) - e.g.
|
||||
`@DtoPath(value = "primaryContact.lastName", requires = "contacts")` where `getPrimaryContact()`
|
||||
picks the first entry out of the `contacts` collection. The real prefix before the computed
|
||||
segment (if any) is automatically combined with the declared `requires()` paths, so the developer
|
||||
doesn't need to redundantly repeat it. Declared paths are emitted as bare `.fetch(path)` calls in
|
||||
the generated `FetchGroup` (distinct from the `.fetch(path, "props")` shape used for ordinary
|
||||
scalar `@DtoPath` properties, since there's no specific target property list to narrow to here).
|
||||
- **The zero-extra-fetch case is also supported, via an explicit `requires = {}`** - e.g.
|
||||
`@DtoPath(value = "idBadge", requires = {})` where `getIdBadge()` derives purely from `id`
|
||||
(always fetched regardless). An explicit empty array confirms "nothing extra needed", distinct
|
||||
from omitting `requires()` entirely ("not yet considered", still a compile error) - `requires()`
|
||||
itself can't tell the two cases apart (both read back as an empty `List`), so `DtoMappingReader`
|
||||
checks the avaje-prism-generated `DtoPathPrism.values.requires()` instead, which returns `null`
|
||||
only when the member was left at its default (i.e. omitted from source). `DtoPropertyMeta`
|
||||
correspondingly carries `hasComputedSegment()` as its own boolean flag (set whenever a computed
|
||||
segment was detected at all), independent of whether `requiredFetchPaths()` happens to be empty -
|
||||
an earlier version conflated the two (inferring "has a computed segment" from "has a non-empty
|
||||
requiredFetchPaths list"), which broke exactly this explicit-empty case by falling through to the
|
||||
ordinary scalar `.select(...)` path and failing at runtime with `PersistenceException: Property
|
||||
not found - idBadge` (`idBadge` isn't a real Ebean property, so it can't be selected).
|
||||
- Implemented in `ebean-annotation` (`DtoPath.requires()`), and `querybean-generator`
|
||||
(`DtoMappingReader` computed-segment detection/validation, `DtoPropertyMeta.requiredFetchPaths()`/
|
||||
`hasComputedSegment()`, `DtoMapperWriter.fetchGroupChainCalls()` bare-fetch emission). Test
|
||||
coverage: `tests/test-dto-mapping` `ComputedPathDto`/`TestComputedPath` (happy path, `requires`
|
||||
correctly fetches the dependency and the mapped value is correct), `ComputedPathNoFetchDto`/
|
||||
`TestComputedPathNoFetch` (explicit `requires = {}`, genuinely nothing extra needed), and
|
||||
`querybean-generator`'s `DtoMapperComputedPathTest` (negative case - omitting `requires` on a
|
||||
computed segment is a compile-time `ERROR` diagnostic, verified via direct `javax.tools.JavaCompiler`
|
||||
compilation, mirroring `DtoMapperFetchPathCollisionTest`).
|
||||
- Known gap: the dedup between the computed segment's required fetch paths and existing
|
||||
`pathSelect`/`nestedAssocPaths` keys in `DtoMapperWriter` is a simplified exact-path-string check
|
||||
(skip emitting a duplicate `.fetch(path)`), not full collision detection like the existing
|
||||
NESTED_ONE/MANY vs `@DtoPath` check - a bare `fetch(path)` and an existing `fetch(path,
|
||||
"specific,props")` for the same path string are not merged/reconciled, just left as two separate
|
||||
calls if that edge case arises.
|
||||
|
||||
- **Fixed: a single-hop `@DtoPath` rename through a computed/derived getter whose return type is
|
||||
itself a registered nested DTO (`NESTED_ONE`/`NESTED_MANY`, not `SCALAR`) bypassed the
|
||||
computed-segment validation above entirely.** E.g. `@DtoPath("primaryContact")` where the DTO
|
||||
field's declared type is `ContactDto` (a type with its own `@DtoMapping(source = Contact.class,
|
||||
target = ContactDto.class)`) and `getPrimaryContact()` is a computed getter with no backing
|
||||
field on `Customer`. This resolves to a single-segment path, so `DtoMappingReader.resolveProperty()`
|
||||
took its `properties.size() == 1` nested-lookup shortcut and returned early - before the
|
||||
`computedFrom`/`requires()` validation block (added for the `SCALAR` case above) ever ran. The
|
||||
generated `FetchGroup` then emitted a broken `fetch("primaryContact",
|
||||
contactMapper.fetchGroup())` call (`"primaryContact"` isn't a real Ebean fetch path), failing at
|
||||
runtime rather than compile time - the exact class of bug the `SCALAR` fix was meant to close off
|
||||
entirely.
|
||||
- Resolution: restructured `resolveProperty()` so the computed-segment detection/validation block
|
||||
runs *before* the `properties.size() == 1` nested-lookup branch, so both `SCALAR` and
|
||||
`NESTED_ONE`/`NESTED_MANY` paths share the same detection/validation. `DtoPropertyMeta` gained a
|
||||
matching constructor overload for `NESTED_ONE`/`NESTED_MANY` carrying `computedSegment`/
|
||||
`requiredFetchPaths`. In `DtoMapperWriter.fetchGroupChainCalls()`, a `NESTED_ONE`/`NESTED_MANY`
|
||||
property with `hasComputedSegment()` true is routed into `extraFetchPaths` (the same bare
|
||||
`.fetch(path)` mechanism as the `SCALAR` case) instead of emitting `fetch(path,
|
||||
mapper.fetchGroup())` - since the nested mapper's own `FetchGroup` requirements can't be
|
||||
meaningfully attached under a path name that doesn't exist on the source entity.
|
||||
- Note the nested mapper's *own* fetch requirements (e.g. if `ContactDto` itself needed
|
||||
`customer.billingAddress`) are **not** automatically propagated up through a computed segment -
|
||||
only whatever the computed getter itself needs (via `requires()`) is fetched. The nested
|
||||
mapper's `map(...)` call still works via plain Java method invocation regardless (Ebean
|
||||
transparent lazy loading covers any gap), but relying on that silently reintroduces N+1 queries,
|
||||
so the nested DTO used through a computed segment should ideally be a "leaf" shape needing
|
||||
nothing beyond what `requires()` already declares.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()` restructuring,
|
||||
`DtoPropertyMeta`'s new constructor overload, `DtoMapperWriter.fetchGroupChainCalls()`). Test
|
||||
coverage: `tests/test-dto-mapping` `ContactLeafDto`/`ComputedNestedDto`/`TestComputedNestedPath`
|
||||
(happy path - generated `FetchGroup` is `.select("id").fetch("contacts")`, no broken
|
||||
`fetch("primaryContact", ...)` call, and the mapped value is correct end-to-end), and
|
||||
`querybean-generator`'s `DtoMapperComputedPathTest#dtoPathThroughComputedGetter_targetingNestedDto_withoutRequires_expectCompileError`
|
||||
(negative case, mirroring the `SCALAR` one). The `NESTED_MANY` variant (a computed getter
|
||||
returning a `List` of a type with its own registered nested DTO mapping) shares the identical
|
||||
code path but had no dedicated regression test until later confirmed via `Customer
|
||||
.getRecentContacts()` / `ComputedNestedListDto` / `TestComputedNestedListPath` (coverage only,
|
||||
not a bug fix - passed cleanly first try, confirming the shared code path does work end-to-end
|
||||
for both `NESTED_ONE` and `NESTED_MANY`).
|
||||
|
||||
- **Fixed: `@DtoRef` never checked for a computed/derived association getter at all.** Unlike
|
||||
`@DtoPath`, `@DtoRef`'s association name (derived by stripping the `Id` suffix off the field
|
||||
name, e.g. `primaryContactId` -> `primaryContact`) was never checked against `hasField(...)` -
|
||||
so `@DtoRef` on a computed getter (e.g. `getPrimaryContact()` picking the first entry out of a
|
||||
`contacts` collection) compiled cleanly and generated a broken `FetchGroup.select("primaryContact")`
|
||||
call (`"primaryContact"` isn't a real Ebean property), failing at runtime rather than compile
|
||||
time - the same class of bug as the original `@DtoPath` fix, just entirely unaddressed for
|
||||
`@DtoRef`'s separate code path.
|
||||
- Resolution: `@DtoRef` gained its own `requires()` attribute (dot-notation, same convention and
|
||||
explicit-empty semantics as `@DtoPath#requires()`, using the same `DtoRefPrism.values.requires()
|
||||
== null` omitted-vs-explicit-empty technique). `DtoMappingReader`'s `@DtoRef` branch now checks
|
||||
`hasField(meta.source(), assocName)` and fails fast at compile time (`ctx.logError(...)`) when
|
||||
the association has no backing field and `requires()` wasn't specified. `DtoPropertyMeta`'s
|
||||
`REF` properties now carry `computedSegment`/`requiredFetchPaths` through the existing fields
|
||||
(no new constructor needed - the full constructor already had the right shape).
|
||||
`DtoMapperWriter.fetchGroupChainCalls()`'s `REF` case now checks `hasComputedSegment()` and
|
||||
routes into `extraFetchPaths` (bare `.fetch(path)`) instead of `rootSelect.add(assoc)` when
|
||||
true - the value expression itself (`source.getPrimaryContact().getId()`, null-guarded) is
|
||||
unaffected, since it's plain Java method invocation regardless of whether the association name
|
||||
is a real Ebean property.
|
||||
- Implemented in `ebean-annotation` (`DtoRef.requires()`), and `querybean-generator`
|
||||
(`DtoMappingReader`'s `@DtoRef` branch, `DtoMapperWriter.fetchGroupChainCalls()`'s `REF` case).
|
||||
Test coverage: `tests/test-dto-mapping` `ComputedRefDto`/`TestComputedRefPath` (happy path -
|
||||
generated `FetchGroup` is `.select("id").fetch("contacts")`, no broken `select("primaryContact")`
|
||||
call, and the mapped id is correct end-to-end), and `querybean-generator`'s
|
||||
`DtoMapperComputedPathTest#dtoRefThroughComputedGetter_withoutRequires_expectCompileError`
|
||||
(negative case, mirroring the `@DtoPath` ones).
|
||||
|
||||
- **Fixed: `requires()` path values themselves were never validated against the source type's
|
||||
real property graph.** `@DtoPath(requires = {...})`/`@DtoRef(requires = {...})` values are
|
||||
handed straight through to `FetchGroup.fetch(...)` unmodified - a typo (e.g. `requires =
|
||||
"contactz"` for the real `contacts` property) compiled cleanly, since only the *computed
|
||||
segment itself* was checked against `hasField(...)`, not the developer-declared dependency
|
||||
paths meant to fix it. That silently reintroduced the exact runtime `PersistenceException` the
|
||||
whole `requires()` escape hatch exists to prevent, just one step removed and harder to spot.
|
||||
- Resolution: added `DtoMappingReader.validateRequiresPath(...)`, which walks each dot-notation
|
||||
segment of a declared `requires()` value from the source root (`meta.source()`), checking
|
||||
`hasField(...)` at every hop exactly like `@DtoPath#value()`'s own segments are checked, and
|
||||
unwrapping a `java.util.List`-typed intermediate hop to its element type (via
|
||||
`listElementType(TypeMirror)`) so a collection segment followed by a further hop resolves
|
||||
correctly - needed a new `getterReturnTypeMirror(...)` helper (returning the raw `TypeMirror`
|
||||
rather than converting straight to `TypeElement`, which can't distinguish a `List` from any
|
||||
other declared type) alongside the existing `getterReturnType(...)`. Called for every entry in
|
||||
`pathPrism.requires()`/`refPrism.requires()` right after they're read, for both the `@DtoPath`
|
||||
and `@DtoRef` branches. The already-validated real prefix (segments before the computed one in
|
||||
a `@DtoPath#value()`) is intentionally *not* re-validated, since it was already checked while
|
||||
walking `value()` itself.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.validateRequiresPath(...)`,
|
||||
`getterReturnTypeMirror(...)`, called from both the `@DtoPath` and `@DtoRef` branches). Test
|
||||
coverage: `querybean-generator`'s
|
||||
`DtoMapperComputedPathTest#dtoPathRequires_withTypoInPathValue_expectCompileError` (negative
|
||||
case - a typo'd `requires()` segment is a compile-time `ERROR` diagnostic); existing
|
||||
`tests/test-dto-mapping`/`central-access` suites (real multi-segment `requires()` values like
|
||||
`"currentMachine.organisationMachines"`) continue to pass unchanged, confirming the validation
|
||||
doesn't false-positive on legitimate paths.
|
||||
|
||||
- **Fixed: a bare, full `requires()` fetch and a sibling property's narrowed `@DtoPath` fetch of
|
||||
the exact same path silently conflicted, with the narrow one always (incorrectly) winning.**
|
||||
`DtoMapperWriter.fetchGroupChainCalls()`'s dedup logic used to skip emitting a computed
|
||||
segment's bare `fetch(path)` call whenever another property's `@DtoPath` already had a narrowed
|
||||
`fetch(path, "specific,props")` entry for that exact path string - on the assumption the two
|
||||
were interchangeable/redundant. They aren't: `FetchGroup`'s builder (`OrmQueryDetail.fetch(...)`)
|
||||
keys fetch calls by path in a plain `Map` and **replaces** rather than merges same-path entries,
|
||||
so whichever call format was emitted meant the *other* was silently discarded. Since the narrow
|
||||
entry was always emitted first and the bare one skipped whenever it existed, the narrow selection
|
||||
always won - meaning a computed getter's `requires()` declaration could be completely ignored
|
||||
whenever an unrelated sibling `@DtoPath` happened to narrow-select the exact same path, leaving
|
||||
whatever extra properties the computed getter actually touches unfetched (a silent lazy load, or
|
||||
a hard `LazyInitialisationException` outside a persistence context).
|
||||
- Resolution: reversed the priority - `fetchGroupChainCalls()` now skips a narrowed `pathSelect`
|
||||
entry when `extraFetchPaths` (the computed segment's `requires()`) declares the exact same
|
||||
path, letting the bare, full `fetch(path)` call win instead. This is always safe since a full
|
||||
fetch is a superset of any narrower property selection - the narrow entry's own properties are
|
||||
included within it regardless. The existing `nestedAssocPaths` priority (a `NESTED_ONE`/
|
||||
`NESTED_MANY` property's full `fetch(path, mapper.fetchGroup())` always wins over a bare
|
||||
`fetch(path)`) was correct already and left unchanged - a nested mapper's own `FetchGroup` is
|
||||
strictly richer than either form and must not be replaced by either.
|
||||
- Implemented in `querybean-generator` (`DtoMapperWriter.fetchGroupChainCalls()`). Test coverage:
|
||||
`tests/test-dto-mapping` `FetchCollisionDto`/`TestFetchCollisionPath`, plus a new computed
|
||||
getter `Customer.getBillingSummary()` (reads `billingAddress.getLine1()`, deliberately a
|
||||
different `Address` property to the `city` narrowly selected by a sibling `@DtoPath` on the
|
||||
same DTO) - confirmed to reproduce `LazyInitialisationException: Property not loaded: line1`
|
||||
when the fix is reverted, and pass cleanly (correct `line1`-derived value, generated
|
||||
`FetchGroup` is `.select("id").fetch("billingAddress")` with no narrowed variant at all) with
|
||||
it in place.
|
||||
|
||||
- **Fixed: two `@DtoMixin` companion types targeting the same DTO class silently conflicted, with
|
||||
the second-processed one winning.** `DtoMappingReader.collectMixins()` keyed a single
|
||||
`mixinsByTarget` map by the target DTO's FQN, and `Map.put(...)` unconditionally overwrote any
|
||||
existing entry - so if two mixin interfaces (e.g. a legitimate one plus an accidental duplicate,
|
||||
or two independently-added mixins that both happened to target the same generated/unowned DTO)
|
||||
both declared `@DtoMixin(SameDto.class)`, whichever was visited last by
|
||||
`roundEnv.getElementsAnnotatedWith(...)` silently won, and *all* of the other mixin's
|
||||
`@DtoPath`/`@DtoRef`/`@DtoConvert` overlays were discarded with no diagnostic at all.
|
||||
- Resolution: `collectMixins()` now checks for an existing registration before storing a new one
|
||||
and raises a compile `ERROR` naming both the target and the already-registered mixin's
|
||||
qualified name, rather than silently overwriting it.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.collectMixins()`). Test coverage: new
|
||||
negative compile-error test `DtoMapperComputedPathTest#duplicateDtoMixin_forSameTarget_expectCompileError`
|
||||
(two minimal `@DtoMixin(FooDto.class)` interfaces both declaring a `bar()` method, compiled
|
||||
together, asserting the `Duplicate @DtoMixin` diagnostic is raised); existing
|
||||
`tests/test-dto-mapping` `TestDtoMixin` (single, legitimate mixin usage) continues to pass
|
||||
unchanged.
|
||||
|
||||
- **Fixed: `@DtoRef` and `@DtoPath` both present on the same field silently conflicted, with
|
||||
`@DtoRef` always (invisibly) winning.** `resolveProperty()` checked `refPrism != null` first and
|
||||
returned immediately whenever present, so a field carrying both annotations at once - whether by
|
||||
copy/paste mistake, a half-finished rename from one style to the other, or simple confusion
|
||||
between the two escape hatches - had its `@DtoPath` completely ignored with no diagnostic at all.
|
||||
- Resolution: `resolveProperty()` now resolves both prisms upfront and raises a compile `ERROR`
|
||||
naming the field when both are present, rather than silently picking `@DtoRef` and discarding
|
||||
`@DtoPath`.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()`). Test coverage: new
|
||||
negative compile-error test `DtoMapperComputedPathTest#dtoRefAndDtoPath_onSameField_expectCompileError`
|
||||
(a field carrying both `@DtoRef` and `@DtoPath("bar.id")` over a real, non-computed association,
|
||||
isolating the conflict diagnostic from the separate computed-getter `requires()` diagnostics).
|
||||
|
||||
- **Fixed: `@DtoConvert` on a `NESTED_ONE`/`NESTED_MANY` property was silently ignored.**
|
||||
`resolveProperty()` resolves the property's `DtoConverterMeta` unconditionally up front (before
|
||||
it's known whether the property will resolve to `SCALAR`/`REF`/`NESTED_ONE`/`NESTED_MANY`), but
|
||||
only the `SCALAR`/`REF` `DtoPropertyMeta` constructors actually accept/store a converter - the
|
||||
`NESTED_ONE`/`NESTED_MANY` constructor calls never took one, so a resolved converter was simply
|
||||
dropped on the floor with no diagnostic. A developer adding `@DtoConvert` to a nested-DTO field
|
||||
(e.g. hoping to post-process the nested mapper's result) would see it silently do nothing -
|
||||
`DtoMapperWriter.propertyValueExpression()`'s `NESTED_ONE`/`NESTED_MANY` cases call straight into
|
||||
`mapperFieldName(property) + ".map(...)"`/`".mapList(...)"` with no converter wrapping at all.
|
||||
- Resolution: added `rejectConverterOnNested(...)`, called at each of the four call sites that
|
||||
construct a `NESTED_ONE`/`NESTED_MANY` `DtoPropertyMeta` (the single-hop `@DtoPath`-rename
|
||||
branch's two cases, and the plain non-`@DtoPath` branch's two cases) - raises a compile `ERROR`
|
||||
naming the field whenever a converter was resolved for it, rather than silently discarding it.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()`,
|
||||
`rejectConverterOnNested()`). Test coverage: new negative compile-error test
|
||||
`DtoMapperComputedPathTest#dtoConvertOnNestedOne_expectCompileError` (a `NESTED_ONE` field
|
||||
carrying `@DtoConvert` over a legitimately nested, separately-`@DtoMapping`-registered type);
|
||||
existing `tests/test-dto-mapping` suite (no nested property currently combines `@DtoConvert`
|
||||
with `NESTED_ONE`/`NESTED_MANY`) continues to pass unchanged, confirming no false positives on
|
||||
plain nested properties.
|
||||
|
||||
- **Fixed: `@DtoConvert(method = ...)` resolution ignored parameter arity/overloads.** The shared
|
||||
`findMethod(type, name)` helper (also used for the builder's `build()` lookup and `@DtoMixin`
|
||||
companion-method lookup) matches purely by simple name - the first `ExecutableElement` found -
|
||||
with no arity or parameter-type check at all. For `@DtoConvert` specifically this is a real risk:
|
||||
its documented contract is a method "taking the source property value and returning the
|
||||
converted DTO property value" (i.e. exactly one parameter), but a shared/reusable conversion
|
||||
utility class is a very plausible place to have multiple same-named overloads (e.g. `format
|
||||
(Instant)` and `format(LocalDate)`) - `findMethod` would silently bind to whichever one
|
||||
`ElementFilter.methodsIn` happened to return first, independent of which one the developer
|
||||
actually meant, generating either a confusing arity/type-mismatch compile error in the generated
|
||||
mapper or, if both overloads happened to be call-compatible, silently invoking the wrong one.
|
||||
- Resolution: added a dedicated `findConverterMethod(...)` (used only by `resolveConverter()`,
|
||||
leaving the shared `findMethod()` untouched for the builder/mixin call sites which have their
|
||||
own, different arity expectations) that filters same-named candidates down to those taking
|
||||
exactly one parameter. Zero matches raises a clear "not found ... taking exactly one
|
||||
parameter" error; more than one match (multiple 1-arg overloads sharing the name) raises an
|
||||
"ambiguous - N overloads take exactly one parameter" error, since `@DtoConvert` has no
|
||||
parameter-type-based way to disambiguate and the developer must rename one of the overloads.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveConverter()`,
|
||||
`findConverterMethod()`). Test coverage: new negative compile-error tests
|
||||
`DtoMapperComputedPathTest#dtoConvertMethod_withAmbiguousOverloads_expectCompileError` (two
|
||||
same-named 1-arg overloads) and `#dtoConvertMethod_withWrongArity_expectCompileError` (a
|
||||
same-named 0-arg method, no 1-arg candidate at all); existing `tests/test-dto-mapping`
|
||||
converter usage (a single, unambiguous 1-arg method per converter type) continues to resolve
|
||||
and pass unchanged.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean api</name>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package io.ebean;
|
||||
|
||||
/**
|
||||
* Runtime helpers used by generated {@link DtoMapper} implementations to safely resolve a
|
||||
* primitive-typed DTO field whose value is derived from a multi-hop {@code @DtoPath} that
|
||||
* traverses a nullable intermediate relation.
|
||||
* <p>
|
||||
* A {@code null}-guarded getter-chain (e.g. {@code source.getOrganisation() == null ? null :
|
||||
* source.getOrganisation().getId()}) always types as the boxed wrapper (since one branch is the
|
||||
* {@code null} literal). When the DTO's target field is a primitive (e.g. {@code long
|
||||
* organisationId}), passing that boxed expression to the constructor auto-unboxes it - which
|
||||
* throws a raw, unhelpful {@link NullPointerException} if the relation really is {@code null}.
|
||||
* <p>
|
||||
* These methods give the generated mapper a choice, controlled by {@code @DtoPath#failOnNull()}:
|
||||
* default to the primitive's zero-equivalent value ({@code orZero} methods, the default), or
|
||||
* throw a clear, descriptive exception naming the offending property path ({@code require}
|
||||
* methods, opted into via {@code failOnNull = true}).
|
||||
*
|
||||
* @see io.ebean.annotation.DtoPath
|
||||
*/
|
||||
public final class DtoMapperSupport {
|
||||
|
||||
private DtoMapperSupport() {
|
||||
}
|
||||
|
||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static long orZero(Long value) {
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
|
||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static int orZero(Integer value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static short orZero(Short value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static byte orZero(Byte value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
/** Return {@code 0.0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static double orZero(Double value) {
|
||||
return value == null ? 0.0 : value;
|
||||
}
|
||||
|
||||
/** Return {@code 0.0f} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static float orZero(Float value) {
|
||||
return value == null ? 0.0f : value;
|
||||
}
|
||||
|
||||
/** Return {@code false} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static boolean orZero(Boolean value) {
|
||||
return value != null && value;
|
||||
}
|
||||
|
||||
/** Return {@code '\u0000'} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static char orZero(Character value) {
|
||||
return value == null ? '\u0000' : value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static long require(Long value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static int require(Integer value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static short require(Short value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static byte require(Byte value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static double require(Double value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static float require(Float value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static boolean require(Boolean value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static char require(Character value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static IllegalStateException failure(String path) {
|
||||
return new IllegalStateException(
|
||||
"@DtoPath(\"" + path + "\") resolved to null via a nullable intermediate relation, but the"
|
||||
+ " target DTO field is primitive and failOnNull=true - either handle the null case in"
|
||||
+ " source data, use a boxed wrapper type for the DTO field, or remove failOnNull to"
|
||||
+ " default to the primitive's zero-equivalent value instead.");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-bench</artifactId>
|
||||
|
||||
+28
-28
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean bom</name>
|
||||
@@ -89,25 +89,25 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -125,13 +125,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -155,37 +155,37 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>kotlin-querybean-generator</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-redis</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-spring-txn</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- platforms -->
|
||||
@@ -193,91 +193,91 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-clickhouse</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-db2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-h2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-hana</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mariadb</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mysql</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-nuodb</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-oracle</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlite</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlserver</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
<artifactId>ebean-core-json</artifactId>
|
||||
<name>ebean-core-json</name>
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
+7
-7
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-core</artifactId>
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-json</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -52,7 +52,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -157,21 +157,21 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
+3
@@ -18,12 +18,14 @@ final class InsertMetaOptionsPostgres implements InsertMetaOptions {
|
||||
private final InsertMeta meta;
|
||||
private final BeanDescriptor<?> desc;
|
||||
private final String baseTable;
|
||||
private final List<String> nonUpdatableColumns;
|
||||
private final Map<String, String> sqlCache = new ConcurrentHashMap<>();
|
||||
|
||||
InsertMetaOptionsPostgres(InsertMeta meta, BeanDescriptor<?> desc) {
|
||||
this.meta = meta;
|
||||
this.desc = desc;
|
||||
this.baseTable = desc.baseTable();
|
||||
this.nonUpdatableColumns = InsertMetaOptionsSupport.nonUpdatableColumns(desc);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -84,6 +86,7 @@ final class InsertMetaOptionsPostgres implements InsertMetaOptions {
|
||||
private void setColumns(boolean withId, GenerateDmlRequest request, List<String> uniqueColumns) {
|
||||
List<String> columns = request.columns();
|
||||
columns.removeAll(uniqueColumns);
|
||||
columns.removeAll(nonUpdatableColumns);
|
||||
if (withId) {
|
||||
BeanProperty idProperty = desc.idProperty();
|
||||
if (idProperty != null && !idProperty.isEmbedded()) {
|
||||
|
||||
+3
@@ -21,12 +21,14 @@ final class InsertMetaOptionsSqlite implements InsertMetaOptions {
|
||||
private final InsertMeta meta;
|
||||
private final BeanDescriptor<?> desc;
|
||||
private final String baseTable;
|
||||
private final List<String> nonUpdatableColumns;
|
||||
private final Map<String, String> sqlCache = new ConcurrentHashMap<>();
|
||||
|
||||
InsertMetaOptionsSqlite(InsertMeta meta, BeanDescriptor<?> desc) {
|
||||
this.meta = meta;
|
||||
this.desc = desc;
|
||||
this.baseTable = desc.baseTable();
|
||||
this.nonUpdatableColumns = InsertMetaOptionsSupport.nonUpdatableColumns(desc);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -85,6 +87,7 @@ final class InsertMetaOptionsSqlite implements InsertMetaOptions {
|
||||
private void setColumns(boolean withId, GenerateDmlRequest request, List<String> uniqueColumns) {
|
||||
List<String> columns = request.columns();
|
||||
columns.removeAll(uniqueColumns);
|
||||
columns.removeAll(nonUpdatableColumns);
|
||||
if (withId) {
|
||||
BeanProperty idProperty = desc.idProperty();
|
||||
if (idProperty != null && !idProperty.isEmbedded()) {
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.ebeaninternal.server.persist.dml;
|
||||
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.BeanProperty;
|
||||
import io.ebeaninternal.server.deploy.generatedproperty.GeneratedProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Shared support for the platform specific insert on conflict do update generation.
|
||||
*/
|
||||
final class InsertMetaOptionsSupport {
|
||||
|
||||
private InsertMetaOptionsSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the columns that should be excluded from the generated "do update set" clause
|
||||
* as they are not updatable, e.g. {@code @Column(updatable=false)} or a generated property
|
||||
* that is insert only such as {@code @WhenCreated}/{@code @WhoCreated}.
|
||||
*/
|
||||
static List<String> nonUpdatableColumns(BeanDescriptor<?> desc) {
|
||||
List<String> columns = new ArrayList<>();
|
||||
for (BeanProperty prop : desc.propertiesNonTransient()) {
|
||||
if (!prop.isDbUpdatable() || isInsertOnlyGenerated(prop)) {
|
||||
columns.add(prop.dbColumn());
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
private static boolean isInsertOnlyGenerated(BeanProperty prop) {
|
||||
GeneratedProperty gen = prop.generatedProperty();
|
||||
return gen != null && gen.includeInInsert() && !gen.includeInUpdate();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean ddl generation</name>
|
||||
@@ -28,14 +28,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean net postgis types</name>
|
||||
@@ -19,14 +19,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- provided scope -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-opentelemetry</artifactId>
|
||||
@@ -28,7 +28,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -71,21 +71,21 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean pgvector types</name>
|
||||
@@ -19,14 +19,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- provided scope -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean postgis types</name>
|
||||
@@ -19,14 +19,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- provided scope -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean querybean</name>
|
||||
@@ -17,7 +17,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -59,14 +59,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-redis</artifactId>
|
||||
@@ -29,35 +29,35 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-redisson</artifactId>
|
||||
@@ -29,35 +29,35 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-spring-txn</artifactId>
|
||||
@@ -28,7 +28,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean test</name>
|
||||
@@ -33,20 +33,20 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -149,14 +149,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.tests.insert;
|
||||
|
||||
import io.ebean.annotation.Index;
|
||||
import io.ebean.annotation.WhenCreated;
|
||||
import io.ebean.annotation.WhenModified;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Used to verify that insert on conflict do update excludes insert-only generated
|
||||
* properties (such as {@code @WhenCreated}) from the generated update set clause.
|
||||
*/
|
||||
@Entity
|
||||
public class EConflictWithCreated {
|
||||
|
||||
@Id
|
||||
Long id;
|
||||
|
||||
@Index(unique = true)
|
||||
String code;
|
||||
|
||||
@WhenCreated
|
||||
Instant whenCreated;
|
||||
|
||||
@WhenModified
|
||||
Instant whenUpdated;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public Instant getWhenCreated() {
|
||||
return whenCreated;
|
||||
}
|
||||
|
||||
public void setWhenCreated(Instant whenCreated) {
|
||||
this.whenCreated = whenCreated;
|
||||
}
|
||||
|
||||
public Instant getWhenUpdated() {
|
||||
return whenUpdated;
|
||||
}
|
||||
|
||||
public void setWhenUpdated(Instant whenUpdated) {
|
||||
this.whenUpdated = whenUpdated;
|
||||
}
|
||||
}
|
||||
@@ -307,6 +307,35 @@ class TestInsertOnConflict extends BaseTestCase {
|
||||
assertThat(list.get(0).getWhenUpdated()).isEqualTo(bean.getWhenUpdated());
|
||||
}
|
||||
|
||||
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE, Platform.SQLITE})
|
||||
@Test
|
||||
void insertOnConflictUpdate_excludesWhenCreatedFromUpdateSet() {
|
||||
Database db = DB.getDefault();
|
||||
db.truncate(EConflictWithCreated.class);
|
||||
LoggedSql.start();
|
||||
|
||||
var bean = new EConflictWithCreated();
|
||||
bean.setCode("abc");
|
||||
db.insert(bean, ON_CONFLICT_UPDATE);
|
||||
|
||||
var bean2 = new EConflictWithCreated();
|
||||
bean2.setCode("abc");
|
||||
db.insert(bean2, ON_CONFLICT_UPDATE);
|
||||
|
||||
var sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(2);
|
||||
// when_created is not included in the "do update set" clause - it is insert only
|
||||
assertThat(sql.get(0)).contains("on conflict (code) do update set when_updated=excluded.when_updated");
|
||||
assertThat(sql.get(0)).doesNotContain("when_created=excluded.when_created");
|
||||
assertThat(sql.get(1)).contains("on conflict (code) do update set when_updated=excluded.when_updated");
|
||||
assertThat(sql.get(1)).doesNotContain("when_created=excluded.when_created");
|
||||
|
||||
List<EConflictWithCreated> list = db.find(EConflictWithCreated.class).findList();
|
||||
assertThat(list).hasSize(1);
|
||||
// original whenCreated value is preserved rather than being overwritten by the 2nd insert
|
||||
assertThat(list.get(0).getWhenCreated()).isEqualTo(bean.getWhenCreated());
|
||||
}
|
||||
|
||||
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE})
|
||||
@Test
|
||||
void updateQueryReturning() throws SQLException {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<name>kotlin querybean generator</name>
|
||||
@@ -21,7 +21,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -56,14 +56,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
+14
-14
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,67 +16,67 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hsqldb</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlanywhere</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Provided scope so that the H2HistoryTrigger can live in Ebean core
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>platforms</artifactId>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>ebean parent</name>
|
||||
@@ -45,7 +45,7 @@
|
||||
<ebean-persistence-api.version>3.2</ebean-persistence-api.version>
|
||||
<ebean-types.version>3.0</ebean-types.version>
|
||||
|
||||
<ebean-annotation.version>8.6</ebean-annotation.version>
|
||||
<ebean-annotation.version>8.9</ebean-annotation.version>
|
||||
<ebean-ddl-runner.version>2.3</ebean-ddl-runner.version>
|
||||
<ebean-migration-auto.version>1.2</ebean-migration-auto.version>
|
||||
<ebean-migration.version>14.3.0</ebean-migration.version>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<name>querybean generator</name>
|
||||
@@ -45,7 +45,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
+52
-1
@@ -104,6 +104,9 @@ class DtoMapperWriter {
|
||||
if (!meta.converterDeps().isEmpty()) {
|
||||
imports.add("io.ebean.DtoConverterManager");
|
||||
}
|
||||
if (meta.properties().stream().anyMatch(DtoPropertyMeta::usesMapperSupport)) {
|
||||
imports.add("io.ebean.DtoMapperSupport");
|
||||
}
|
||||
if (variableProperties().stream().anyMatch(p -> p.kind() == DtoPropertyMeta.Kind.NESTED_MANY)) {
|
||||
imports.add("java.util.List");
|
||||
}
|
||||
@@ -222,21 +225,41 @@ class DtoMapperWriter {
|
||||
}
|
||||
Set<String> nestedAssocPaths = new LinkedHashSet<>();
|
||||
for (DtoPropertyMeta property : activeProperties) {
|
||||
if (property.kind() == DtoPropertyMeta.Kind.NESTED_ONE || property.kind() == DtoPropertyMeta.Kind.NESTED_MANY) {
|
||||
if ((property.kind() == DtoPropertyMeta.Kind.NESTED_ONE || property.kind() == DtoPropertyMeta.Kind.NESTED_MANY)
|
||||
&& !property.hasComputedSegment()) {
|
||||
nestedAssocPaths.add(property.sourcePropertyPath().get(0));
|
||||
}
|
||||
}
|
||||
Set<String> rootSelect = new LinkedHashSet<>();
|
||||
Map<String, List<String>> pathSelect = new LinkedHashMap<>();
|
||||
Set<String> extraFetchPaths = new LinkedHashSet<>();
|
||||
List<String> fetchCalls = new ArrayList<>();
|
||||
for (DtoPropertyMeta property : activeProperties) {
|
||||
switch (property.kind()) {
|
||||
case NESTED_ONE:
|
||||
case NESTED_MANY:
|
||||
if (property.hasComputedSegment()) {
|
||||
// a single-hop @DtoPath rename traversing a computed/derived getter (no backing
|
||||
// field) that happens to target a nested DTO type - just as unfetchable via
|
||||
// fetch(path, mapper.fetchGroup()) as the analogous SCALAR case, since "path" here
|
||||
// isn't a real Ebean fetch path either. The nested mapper is still invoked directly
|
||||
// against whatever the getter returns (see DtoMapperWriter#propertyValueExpression) -
|
||||
// it's purely the FetchGroup derivation that must fall back to @DtoPath#requires().
|
||||
extraFetchPaths.addAll(property.requiredFetchPaths());
|
||||
break;
|
||||
}
|
||||
fetchCalls.add(String.format("fetch(\"%s\", %s.fetchGroup())",
|
||||
property.sourcePropertyPath().get(0), mapperFieldName(property)));
|
||||
break;
|
||||
case SCALAR:
|
||||
if (property.hasComputedSegment()) {
|
||||
// the path traverses a computed/derived getter (no backing field) - its own segments
|
||||
// past that point aren't real Ebean fetch paths, so don't add them to pathSelect/
|
||||
// rootSelect at all; @DtoPath#requires() (plus the real prefix, if any) already names
|
||||
// exactly what needs fetching instead - see DtoPropertyMeta#requiredFetchPaths().
|
||||
extraFetchPaths.addAll(property.requiredFetchPaths());
|
||||
break;
|
||||
}
|
||||
List<String> path = property.sourcePropertyPath();
|
||||
if (path.size() == 1) {
|
||||
rootSelect.add(path.get(0));
|
||||
@@ -254,6 +277,15 @@ class DtoMapperWriter {
|
||||
break;
|
||||
case REF:
|
||||
default:
|
||||
if (property.hasComputedSegment()) {
|
||||
// the association has no backing field (a computed/derived getter) - "assoc" isn't a
|
||||
// real Ebean property name, so it can't be handed to FetchGroup.select(...) directly;
|
||||
// @DtoRef#requires() already names exactly what needs fetching instead - see
|
||||
// DtoPropertyMeta#requiredFetchPaths(). The value mapping itself (source.getAssoc().
|
||||
// getId()) still works via plain Java method invocation regardless.
|
||||
extraFetchPaths.addAll(property.requiredFetchPaths());
|
||||
break;
|
||||
}
|
||||
String assoc = property.sourcePropertyPath().get(0);
|
||||
if (!nestedAssocPaths.contains(assoc)) {
|
||||
rootSelect.add(assoc);
|
||||
@@ -262,8 +294,27 @@ class DtoMapperWriter {
|
||||
}
|
||||
}
|
||||
for (var entry : pathSelect.entrySet()) {
|
||||
if (extraFetchPaths.contains(entry.getKey())) {
|
||||
// an unrelated computed-segment property also needs a bare, full fetch(path) at this
|
||||
// exact same path (emitted below) - FetchGroup's builder REPLACES (not merges) same-path
|
||||
// fetch calls (OrmQueryDetail.fetch(...) is a plain Map.put keyed by path), so emitting
|
||||
// both a narrowed fetch(path, "props") here and a bare fetch(path) below would leave
|
||||
// only whichever call happens to be added last in effect, silently discarding the other's
|
||||
// requirement depending on emission order. Skip the narrow entry - a full fetch(path) is
|
||||
// always a safe superset of any narrower property selection, so let the bare fetch below
|
||||
// win deterministically instead of depending on iteration order.
|
||||
continue;
|
||||
}
|
||||
fetchCalls.add(String.format("fetch(\"%s\", \"%s\")", entry.getKey(), String.join(",", entry.getValue())));
|
||||
}
|
||||
for (String extraPath : extraFetchPaths) {
|
||||
// already covered by another property's NESTED_ONE/MANY fetch of the exact same path (a
|
||||
// full nested mapper.fetchGroup()) - that's richer than a bare fetch(path) (which would
|
||||
// replace it and lose the nested mapper's own fetch requirements), so it must win instead.
|
||||
if (!nestedAssocPaths.contains(extraPath)) {
|
||||
fetchCalls.add(String.format("fetch(\"%s\")", extraPath));
|
||||
}
|
||||
}
|
||||
List<String> calls = new ArrayList<>();
|
||||
if (!rootSelect.isEmpty()) {
|
||||
calls.add(String.format("select(\"%s\")", String.join(",", rootSelect)));
|
||||
|
||||
+239
-10
@@ -135,7 +135,14 @@ class DtoMappingReader {
|
||||
ctx.logError(element, "@DtoMixin value() must be a class type");
|
||||
continue;
|
||||
}
|
||||
mixinsByTarget.put(target.getQualifiedName().toString(), (TypeElement) element);
|
||||
String targetFqn = target.getQualifiedName().toString();
|
||||
TypeElement existing = mixinsByTarget.get(targetFqn);
|
||||
if (existing != null) {
|
||||
ctx.logError(element, "Duplicate @DtoMixin for target %s - %s already declares a mixin for"
|
||||
+ " it, only one @DtoMixin is allowed per target", targetFqn, existing.getQualifiedName());
|
||||
continue;
|
||||
}
|
||||
mixinsByTarget.put(targetFqn, (TypeElement) element);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,63 +451,178 @@ class DtoMappingReader {
|
||||
String name = field.getSimpleName().toString();
|
||||
DtoConverterMeta converter = resolveConverter(field, meta);
|
||||
DtoRefPrism refPrism = prismOn(field, meta, DtoRefPrism::getInstanceOn);
|
||||
DtoPathPrism pathPrism = prismOn(field, meta, DtoPathPrism::getInstanceOn);
|
||||
if (refPrism != null && pathPrism != null) {
|
||||
ctx.logError(field, "%s.%s carries both @DtoRef and @DtoPath - these are mutually exclusive"
|
||||
+ " (@DtoRef always wins silently otherwise), remove whichever doesn't apply",
|
||||
meta.targetFullName(), name);
|
||||
}
|
||||
if (refPrism != null) {
|
||||
String assocName = (name.endsWith("Id") && name.length() > 2) ? name.substring(0, name.length() - 2) : name;
|
||||
String assocGetter = getterName(meta.source(), assocName);
|
||||
TypeElement assocType = getterReturnType(meta.source(), assocGetter);
|
||||
String idGetter = getterName(assocType, "id");
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.REF, List.of(assocGetter, idGetter), List.of(assocName, "id"), null, converter);
|
||||
boolean computedSegment = meta.source() == null || !hasField(meta.source(), assocName);
|
||||
List<String> requiredFetchPaths = List.of();
|
||||
if (computedSegment) {
|
||||
// the association has no backing field on the source - a computed/derived getter (e.g.
|
||||
// picking one entry out of a collection) rather than a real, fetchable Ebean relation, so
|
||||
// its data dependencies can't be inferred automatically - same problem class, and same
|
||||
// requires() escape hatch, as the analogous @DtoPath handling above. Unlike @DtoPath there's
|
||||
// no "real prefix" to combine with, since @DtoRef only ever derives a single association
|
||||
// name directly off the source (no dotted value() of its own).
|
||||
if (refPrism.values.requires() == null) {
|
||||
ctx.logError(field,
|
||||
"@DtoRef on %s traverses '%s' which has no backing field on %s - it looks like a"
|
||||
+ " computed/derived getter rather than a real, fetchable Ebean relation, so its data"
|
||||
+ " dependencies can't be inferred automatically. Specify @DtoRef(requires = {...})"
|
||||
+ " naming the real entity paths that must be fetched for it to execute safely, or"
|
||||
+ " requires = {} if it genuinely needs nothing extra fetched, or remove @DtoRef and"
|
||||
+ " compute this value another way (e.g. @DtoConvert).",
|
||||
meta.targetFullName(), assocName, meta.source() != null ? meta.source().getSimpleName() : "?");
|
||||
}
|
||||
requiredFetchPaths = refPrism.requires();
|
||||
String annotationDisplay = String.format("@DtoRef(requires = ...) on %s.%s", meta.targetFullName(), name);
|
||||
for (String requiresPath : requiredFetchPaths) {
|
||||
validateRequiresPath(field, meta.source(), requiresPath, annotationDisplay);
|
||||
}
|
||||
}
|
||||
// @DtoRef has no failOnNull escape hatch - always default to the primitive's zero-equivalent
|
||||
// rather than let a null-guarded getter chain auto-unbox to a NullPointerException.
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.REF, List.of(assocGetter, idGetter), List.of(assocName, "id"),
|
||||
null, converter, field.asType().getKind().isPrimitive(), false, computedSegment, requiredFetchPaths);
|
||||
}
|
||||
DtoPathPrism pathPrism = prismOn(field, meta, DtoPathPrism::getInstanceOn);
|
||||
if (pathPrism != null) {
|
||||
List<String> getters = new ArrayList<>();
|
||||
List<String> properties = new ArrayList<>();
|
||||
TypeElement currentType = meta.source();
|
||||
TypeElement computedDeclaringType = null;
|
||||
int computedFrom = -1;
|
||||
for (String segment : pathPrism.value().split("\\.")) {
|
||||
String getter = getterName(currentType, segment);
|
||||
getters.add(getter);
|
||||
properties.add(segment);
|
||||
if (computedFrom < 0 && (currentType == null || !hasField(currentType, segment))) {
|
||||
computedFrom = properties.size() - 1;
|
||||
computedDeclaringType = currentType;
|
||||
}
|
||||
currentType = currentType != null ? getterReturnType(currentType, getter) : null;
|
||||
}
|
||||
List<String> requiredFetchPaths = List.of();
|
||||
if (computedFrom >= 0) {
|
||||
// a segment with no backing field is a computed/derived getter, not a real, fetchable
|
||||
// Ebean property - its own data dependencies (what it touches internally) can't be
|
||||
// inferred from the path alone, so require the developer to spell them out explicitly
|
||||
// via @DtoPath(requires = {...}) rather than silently generating a FetchGroup.fetch(...)
|
||||
// call for a path segment Ebean doesn't actually recognise (which only fails at runtime).
|
||||
// Computed here regardless of whether the path ultimately resolves to a plain SCALAR or a
|
||||
// single-hop NESTED_ONE/MANY rename below - both cases share the exact same problem: a
|
||||
// fake path segment that can't be handed to FetchGroup as a real fetch/select target.
|
||||
String realPrefix = computedFrom == 0 ? null : String.join(".", properties.subList(0, computedFrom));
|
||||
// pathPrism.requires() always returns List.of() whether the attribute was explicitly
|
||||
// written as an empty array or omitted entirely - only pathPrism.values.requires() (which
|
||||
// returns null for a defaulted/omitted member) can tell the two apart. That distinction
|
||||
// matters here: an explicit requires = {} is the developer's way of confirming the
|
||||
// computed getter genuinely needs nothing extra fetched, whereas omitting requires
|
||||
// entirely means they haven't considered it yet - only the latter should fail the build.
|
||||
if (pathPrism.values.requires() == null) {
|
||||
String hint = realPrefix != null
|
||||
? String.format(" (e.g. requires = \"%s\", or a deeper path under it your getter actually needs)", realPrefix)
|
||||
: "";
|
||||
ctx.logError(field,
|
||||
"@DtoPath(\"%s\") on %s traverses '%s' which has no backing field on %s - it looks like"
|
||||
+ " a computed/derived getter rather than a real, fetchable Ebean property, so its data"
|
||||
+ " dependencies can't be inferred automatically. Specify @DtoPath(requires = {...})"
|
||||
+ " naming the real entity paths that must be fetched for it to execute safely%s, or"
|
||||
+ " requires = {} if it genuinely needs nothing extra fetched, or remove @DtoPath and"
|
||||
+ " compute this value another way (e.g. @DtoConvert).",
|
||||
pathPrism.value(), meta.targetFullName(), properties.get(computedFrom),
|
||||
computedDeclaringType != null ? computedDeclaringType.getSimpleName() : "?", hint);
|
||||
}
|
||||
List<String> combined = new ArrayList<>();
|
||||
if (realPrefix != null) {
|
||||
combined.add(realPrefix);
|
||||
}
|
||||
combined.addAll(pathPrism.requires());
|
||||
requiredFetchPaths = combined;
|
||||
// realPrefix is already known-good (each of its segments was hasField-checked while
|
||||
// walking value() above) - only the developer-declared requires() values themselves are
|
||||
// unchecked and need validating here.
|
||||
String annotationDisplay = String.format("@DtoPath(\"%s\").requires()", pathPrism.value());
|
||||
for (String requiresPath : pathPrism.requires()) {
|
||||
validateRequiresPath(field, meta.source(), requiresPath, annotationDisplay);
|
||||
}
|
||||
}
|
||||
// a single-hop @DtoPath rename (e.g. @DtoPath("eboxStatus") on a field named "status")
|
||||
// can still target a type with its own registered @DtoMapping - detect that the same way
|
||||
// the plain (non-@DtoPath) branches below do, rather than always falling back to a raw
|
||||
// scalar getter call that would fail to compile with a type mismatch against the nested
|
||||
// DTO type. Multi-hop paths keep the existing scalar/flattening behaviour since fetch spec
|
||||
// derivation for NESTED_ONE/MANY only supports a single association name.
|
||||
// derivation for NESTED_ONE/MANY only supports a single association name. A computed
|
||||
// segment here (the single segment has no backing field - e.g. @DtoPath("primaryContact")
|
||||
// where getPrimaryContact() is a derived getter) is just as unfetchable as the SCALAR case
|
||||
// above, so it carries the same computedFrom/requiredFetchPaths validation through - see
|
||||
// DtoMapperWriter's NESTED_ONE/NESTED_MANY handling of DtoPropertyMeta#hasComputedSegment().
|
||||
if (properties.size() == 1) {
|
||||
TypeMirror fieldType = field.asType();
|
||||
TypeMirror listElementType = listElementType(fieldType);
|
||||
if (listElementType != null) {
|
||||
DtoBeanMeta nested = lookupByTarget(listElementType);
|
||||
if (nested != null) {
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_MANY, getters, properties, nested);
|
||||
rejectConverterOnNested(field, converter, name, meta);
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_MANY, getters, properties, nested,
|
||||
computedFrom >= 0, requiredFetchPaths);
|
||||
}
|
||||
} else {
|
||||
DtoBeanMeta nested = lookupByTarget(fieldType);
|
||||
if (nested != null) {
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_ONE, getters, properties, nested);
|
||||
rejectConverterOnNested(field, converter, name, meta);
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_ONE, getters, properties, nested,
|
||||
computedFrom >= 0, requiredFetchPaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, getters, properties, null, converter);
|
||||
// A multi-hop path can pass through a nullable intermediate relation - if the DTO field is
|
||||
// primitive, the generated null-guarded getter chain would otherwise auto-unbox a null
|
||||
// straight into a NullPointerException. Default to the primitive's zero-equivalent value,
|
||||
// or fail fast with a clear message instead when @DtoPath(failOnNull = true).
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, getters, properties, null, converter,
|
||||
field.asType().getKind().isPrimitive(), pathPrism.failOnNull(), computedFrom >= 0, requiredFetchPaths);
|
||||
}
|
||||
TypeMirror fieldType = field.asType();
|
||||
TypeMirror listElementType = listElementType(fieldType);
|
||||
if (listElementType != null) {
|
||||
DtoBeanMeta nested = lookupByTarget(listElementType);
|
||||
if (nested != null) {
|
||||
rejectConverterOnNested(field, converter, name, meta);
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_MANY, List.of(getterName(meta.source(), name)), List.of(name), nested);
|
||||
}
|
||||
} else {
|
||||
DtoBeanMeta nested = lookupByTarget(fieldType);
|
||||
if (nested != null) {
|
||||
rejectConverterOnNested(field, converter, name, meta);
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_ONE, List.of(getterName(meta.source(), name)), List.of(name), nested);
|
||||
}
|
||||
}
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, List.of(getterName(meta.source(), name)), List.of(name), null, converter);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code @DtoConvert} only applies to {@code SCALAR}/{@code REF} value expressions
|
||||
* ({@link DtoMapperWriter#propertyValueExpression}) - a {@code NESTED_ONE}/{@code NESTED_MANY}
|
||||
* property's value comes entirely from its own nested mapper's {@code map(...)}/{@code
|
||||
* mapList(...)} call, so a converter resolved for it would silently have no effect at all.
|
||||
* Raise a compile error instead of letting the annotation quietly do nothing.
|
||||
*/
|
||||
private void rejectConverterOnNested(VariableElement field, DtoConverterMeta converter, String name, DtoBeanMeta meta) {
|
||||
if (converter != null) {
|
||||
ctx.logError(field, "@DtoConvert on %s.%s has no effect - it isn't supported on a nested DTO"
|
||||
+ " graph property (NESTED_ONE/NESTED_MANY), only on SCALAR/REF properties. Remove"
|
||||
+ " @DtoConvert, or perform the conversion inside the nested DTO's own mapper instead.",
|
||||
meta.targetFullName(), name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a prism instance for {@code field}, falling back to the corresponding
|
||||
* {@code @DtoMixin} companion method (same simple name) when {@code field} carries no such
|
||||
@@ -544,15 +666,54 @@ class DtoMappingReader {
|
||||
return null;
|
||||
}
|
||||
String methodName = prism.method();
|
||||
ExecutableElement method = findMethod(converterType, methodName);
|
||||
ExecutableElement method = findConverterMethod(field, converterType, methodName);
|
||||
if (method == null) {
|
||||
ctx.logError(field, "@DtoConvert method \"%s\" not found on %s", methodName, converterType.getQualifiedName());
|
||||
return null;
|
||||
}
|
||||
boolean isStatic = method.getModifiers().contains(Modifier.STATIC);
|
||||
return new DtoConverterMeta(converterType.getQualifiedName().toString(), methodName, isStatic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve {@code @DtoConvert}'s {@code method()} on {@code converterType} - unlike
|
||||
* {@link #findMethod}, this requires exactly one candidate taking a single parameter (the
|
||||
* documented {@code @DtoConvert} contract: "taking the source property value and returning the
|
||||
* converted DTO property value"). {@code findMethod} alone matches by simple name only, so a
|
||||
* converter type with two same-named overloads (a very plausible shape for a shared conversion
|
||||
* utility class, e.g. {@code format(Instant)} and {@code format(LocalDate)}) would silently bind
|
||||
* to whichever one {@code ElementFilter.methodsIn} happens to return first, regardless of which
|
||||
* one the developer actually intended - generating either a confusing compile error in the
|
||||
* generated mapper (arity/type mismatch) or, worse, silently generating a call to the wrong
|
||||
* overload if both happen to be call-compatible.
|
||||
*/
|
||||
private ExecutableElement findConverterMethod(VariableElement field, TypeElement converterType, String methodName) {
|
||||
List<ExecutableElement> oneArgCandidates = new ArrayList<>();
|
||||
boolean anyNameMatch = false;
|
||||
for (ExecutableElement candidate : ElementFilter.methodsIn(converterType.getEnclosedElements())) {
|
||||
if (!candidate.getSimpleName().contentEquals(methodName)) {
|
||||
continue;
|
||||
}
|
||||
anyNameMatch = true;
|
||||
if (candidate.getParameters().size() == 1) {
|
||||
oneArgCandidates.add(candidate);
|
||||
}
|
||||
}
|
||||
if (oneArgCandidates.size() == 1) {
|
||||
return oneArgCandidates.get(0);
|
||||
}
|
||||
if (oneArgCandidates.isEmpty()) {
|
||||
ctx.logError(field, "@DtoConvert method \"%s\" not found on %s taking exactly one parameter%s",
|
||||
methodName, converterType.getQualifiedName(),
|
||||
anyNameMatch ? " (a method with that name exists but doesn't take exactly one parameter)" : "");
|
||||
return null;
|
||||
}
|
||||
ctx.logError(field, "@DtoConvert method \"%s\" on %s is ambiguous - %d overloads take exactly one"
|
||||
+ " parameter, and @DtoConvert can't disambiguate by parameter type. Rename one of the"
|
||||
+ " overloads so the reference is unambiguous.",
|
||||
methodName, converterType.getQualifiedName(), oneArgCandidates.size());
|
||||
return null;
|
||||
}
|
||||
|
||||
private ExecutableElement findMethod(TypeElement type, String methodName) {
|
||||
for (ExecutableElement method : ElementFilter.methodsIn(type.getEnclosedElements())) {
|
||||
if (method.getSimpleName().contentEquals(methodName)) {
|
||||
@@ -615,6 +776,29 @@ class DtoMappingReader {
|
||||
return getName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether {@code type} (searching {@code type} and its superclass chain) declares a field
|
||||
* named {@code propertyName} - used to distinguish a real, fetchable Ebean bean property (which
|
||||
* always has a backing field once enhanced) from a computed/derived getter with no backing
|
||||
* storage at all (e.g. a hand-written method that filters/derives a value from other
|
||||
* properties). {@code type == null} (unresolvable) conservatively returns {@code true} so an
|
||||
* already-unresolvable segment doesn't also get flagged as "computed" - it'll already have
|
||||
* fallen back to a guessed getter name via {@link #getterName}.
|
||||
*/
|
||||
private boolean hasField(TypeElement type, String propertyName) {
|
||||
if (type == null) {
|
||||
return true;
|
||||
}
|
||||
for (TypeElement current = type; current != null; current = superclassOf(current)) {
|
||||
for (VariableElement f : ElementFilter.fieldsIn(current.getEnclosedElements())) {
|
||||
if (f.getSimpleName().contentEquals(propertyName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a no-arg method named {@code methodName} exists on {@code type} (searching
|
||||
* {@code type} and its superclass chain), optionally constrained to a specific return
|
||||
@@ -642,16 +826,61 @@ class DtoMappingReader {
|
||||
* which case later segments fall back to the guessed {@code getXxx()} name.
|
||||
*/
|
||||
private TypeElement getterReturnType(TypeElement type, String getterMethodName) {
|
||||
TypeMirror mirror = getterReturnTypeMirror(type, getterMethodName);
|
||||
return mirror != null ? asTypeElement(mirror) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* As {@link #getterReturnType(TypeElement, String)}, but returns the raw {@link TypeMirror}
|
||||
* rather than converting it to a {@link TypeElement} - needed by {@link #validateRequiresPath}
|
||||
* to detect a {@code java.util.List}-typed return (via {@link #listElementType(TypeMirror)}),
|
||||
* which {@link #getterReturnType(TypeElement, String)}'s {@code asTypeElement} conversion can't
|
||||
* distinguish from any other declared type.
|
||||
*/
|
||||
private TypeMirror getterReturnTypeMirror(TypeElement type, String getterMethodName) {
|
||||
for (TypeElement current = type; current != null; current = superclassOf(current)) {
|
||||
for (ExecutableElement method : ElementFilter.methodsIn(current.getEnclosedElements())) {
|
||||
if (method.getParameters().isEmpty() && method.getSimpleName().contentEquals(getterMethodName)) {
|
||||
return asTypeElement(method.getReturnType());
|
||||
return method.getReturnType();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that every dot-notation segment of a declared {@code @DtoPath(requires = ...)}/
|
||||
* {@code @DtoRef(requires = ...)} path value names a real, fetchable Ebean property (one with a
|
||||
* backing field) on {@code source} - these are meant to be real entity fetch paths handed
|
||||
* straight through to {@code FetchGroup.fetch(...)}, so a typo here would otherwise silently
|
||||
* reintroduce the exact "compiles cleanly, fails at runtime with {@code PersistenceException}"
|
||||
* problem the {@code requires()} escape hatch itself exists to prevent - it just wouldn't be
|
||||
* caught until much later, since {@code requires()} paths are trusted verbatim rather than
|
||||
* walked/checked like {@code @DtoPath#value()}'s own segments are. Handles a {@code List}-typed
|
||||
* intermediate hop (e.g. {@code "currentMachine.organisationMachines"}) by unwrapping to the
|
||||
* element type via {@link #listElementType(TypeMirror)}, mirroring how a real fetch path can
|
||||
* traverse a collection.
|
||||
*/
|
||||
private void validateRequiresPath(Element field, TypeElement source, String requiresPath, String annotationDisplay) {
|
||||
TypeElement currentType = source;
|
||||
for (String segment : requiresPath.split("\\.")) {
|
||||
if (currentType == null) {
|
||||
return; // already unresolvable upstream - don't cascade a confusing secondary error
|
||||
}
|
||||
if (!hasField(currentType, segment)) {
|
||||
ctx.logError(field,
|
||||
"%s names '%s' (in \"%s\") which has no backing field on %s - check for a typo, every"
|
||||
+ " requires() path segment must be a real, fetchable Ebean property.",
|
||||
annotationDisplay, segment, requiresPath, currentType.getSimpleName());
|
||||
return;
|
||||
}
|
||||
String getter = getterName(currentType, segment);
|
||||
TypeMirror returnMirror = getterReturnTypeMirror(currentType, getter);
|
||||
TypeMirror elementType = returnMirror != null ? listElementType(returnMirror) : null;
|
||||
currentType = asTypeElement(elementType != null ? elementType : returnMirror);
|
||||
}
|
||||
}
|
||||
|
||||
private TypeElement superclassOf(TypeElement type) {
|
||||
return asTypeElement(type.getSuperclass());
|
||||
}
|
||||
|
||||
+88
-1
@@ -25,18 +25,59 @@ class DtoPropertyMeta {
|
||||
private final List<String> sourcePropertyPath;
|
||||
private final DtoBeanMeta nested;
|
||||
private final DtoConverterMeta converter;
|
||||
private final boolean primitiveTarget;
|
||||
private final boolean failOnNull;
|
||||
private final boolean computedSegment;
|
||||
private final List<String> requiredFetchPaths;
|
||||
|
||||
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath, DtoBeanMeta nested) {
|
||||
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code NESTED_ONE}/{@code NESTED_MANY} constructor variant for a single-hop {@code @DtoPath}
|
||||
* rename that traverses a computed/derived getter segment (no backing field) - see
|
||||
* {@link #hasComputedSegment()}. Just as unfetchable via {@code FetchGroup.fetch(path, ...)} as
|
||||
* the analogous {@link Kind#SCALAR} case, so it carries the same
|
||||
* {@code computedSegment}/{@code requiredFetchPaths} through to {@code DtoMapperWriter}.
|
||||
*/
|
||||
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath,
|
||||
DtoBeanMeta nested, boolean computedSegment, List<String> requiredFetchPaths) {
|
||||
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, null, false, false, computedSegment, requiredFetchPaths);
|
||||
}
|
||||
|
||||
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath, DtoBeanMeta nested, DtoConverterMeta converter) {
|
||||
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, converter, false, false);
|
||||
}
|
||||
|
||||
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath,
|
||||
DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean failOnNull) {
|
||||
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, converter, primitiveTarget, failOnNull, false, List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Full constructor - {@code primitiveTarget}/{@code failOnNull} only matter for a multi-hop
|
||||
* ({@code sourceGetterPath.size() > 1}) {@link Kind#SCALAR}/{@link Kind#REF} property whose DTO
|
||||
* field type is a Java primitive, per {@code @DtoPath#failOnNull()} - see
|
||||
* {@link #sourceValueExpression(String)}. {@code computedSegment} is {@code true} only for a
|
||||
* {@code @DtoPath} that traverses a computed/derived getter segment (no backing field) - see
|
||||
* {@link #hasComputedSegment()}; kept separate from whether {@code requiredFetchPaths} happens
|
||||
* to be empty, since {@code @DtoPath(requires = {})} legitimately declares "nothing extra
|
||||
* needed" for a computed segment.
|
||||
*/
|
||||
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath,
|
||||
DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean failOnNull,
|
||||
boolean computedSegment, List<String> requiredFetchPaths) {
|
||||
this.dtoFieldName = dtoFieldName;
|
||||
this.kind = kind;
|
||||
this.sourceGetterPath = sourceGetterPath;
|
||||
this.sourcePropertyPath = sourcePropertyPath;
|
||||
this.nested = nested;
|
||||
this.converter = converter;
|
||||
this.primitiveTarget = primitiveTarget;
|
||||
this.failOnNull = failOnNull;
|
||||
this.computedSegment = computedSegment;
|
||||
this.requiredFetchPaths = requiredFetchPaths;
|
||||
}
|
||||
|
||||
String dtoFieldName() {
|
||||
@@ -79,11 +120,40 @@ class DtoPropertyMeta {
|
||||
return converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code true} if this {@code @DtoPath} traverses a segment with no backing field (a computed/
|
||||
* derived getter rather than a real, fetchable Ebean property) - in which case
|
||||
* {@link #sourcePropertyPath()} must NOT be used to derive a {@code .fetch(path, "props")}
|
||||
* ({@link Kind#SCALAR}) or {@code .fetch(path, mapper.fetchGroup())} ({@link Kind#NESTED_ONE}/
|
||||
* {@link Kind#NESTED_MANY}) call (the path isn't a real Ebean fetch path), and
|
||||
* {@link #requiredFetchPaths()} should be used instead (see {@code @DtoPath#requires()}).
|
||||
*/
|
||||
boolean hasComputedSegment() {
|
||||
return computedSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Real entity paths that must be added to the {@code FetchGroup} to support this property's
|
||||
* computed/derived getter segment - the real prefix path (if any) followed by the declared
|
||||
* {@code @DtoPath#requires()} paths. Empty by default when {@link #hasComputedSegment()} is
|
||||
* {@code false}; can also legitimately be empty when it's {@code true} (an explicit
|
||||
* {@code @DtoPath(requires = {})} confirming nothing extra is needed).
|
||||
*/
|
||||
List<String> requiredFetchPaths() {
|
||||
return requiredFetchPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a source expression chaining {@link #sourceGetterPath()} getters off the given root
|
||||
* variable. A single getter is a plain call, e.g. {@code s.getName()}; a multi-hop chain (from
|
||||
* {@code @DtoPath} or {@code @DtoRef}) null-guards each intermediate hop, e.g.
|
||||
* {@code (s.getBillingAddress() == null ? null : s.getBillingAddress().getLine1())}.
|
||||
* <p>
|
||||
* That null-guarded chain always types as the boxed wrapper (one ternary branch is the
|
||||
* {@code null} literal) - when {@link #primitiveTarget} is set (the DTO field is a Java
|
||||
* primitive), the whole chain is additionally wrapped in a {@code DtoMapperSupport} call so it
|
||||
* safely resolves to the primitive's zero-equivalent value (the default), or throws a clear
|
||||
* exception instead, per {@code @DtoPath#failOnNull()} - see {@code DtoMapperSupport}.
|
||||
*/
|
||||
String sourceValueExpression(String rootVariable) {
|
||||
if (sourceGetterPath.size() == 1) {
|
||||
@@ -91,7 +161,23 @@ class DtoPropertyMeta {
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
appendGuardedChain(sb, rootVariable, 0);
|
||||
return sb.toString();
|
||||
String chain = sb.toString();
|
||||
if (!primitiveTarget) {
|
||||
return chain;
|
||||
}
|
||||
return failOnNull
|
||||
? "DtoMapperSupport.require(" + chain + ", \"" + String.join(".", sourcePropertyPath) + "\")"
|
||||
: "DtoMapperSupport.orZero(" + chain + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code true} if {@link #sourceValueExpression(String)} wraps its chain in a
|
||||
* {@code DtoMapperSupport} call - i.e. this is a multi-hop {@link Kind#SCALAR}/{@link Kind#REF}
|
||||
* property whose DTO field type is primitive. Used to conditionally import
|
||||
* {@code io.ebean.DtoMapperSupport} only when actually referenced.
|
||||
*/
|
||||
boolean usesMapperSupport() {
|
||||
return primitiveTarget && sourceGetterPath.size() > 1;
|
||||
}
|
||||
|
||||
private void appendGuardedChain(StringBuilder sb, String prefix, int index) {
|
||||
@@ -105,3 +191,4 @@ class DtoPropertyMeta {
|
||||
sb.append(')');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+814
@@ -0,0 +1,814 @@
|
||||
package io.ebean.querybean.generator;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.tools.Diagnostic;
|
||||
import javax.tools.DiagnosticCollector;
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.JavaFileObject;
|
||||
import javax.tools.StandardJavaFileManager;
|
||||
import javax.tools.ToolProvider;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.io.Writer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Regression test for {@code @DtoPath#requires()} - a {@code @DtoPath} traversing a segment with
|
||||
* no backing field (a computed/derived getter, not a real fetchable Ebean property) must fail
|
||||
* fast at compile time when {@code requires()} isn't specified, rather than compiling cleanly and
|
||||
* failing later at runtime because the generated {@code FetchGroup} doesn't fetch whatever the
|
||||
* getter itself needs internally (see docs/dto-mapping-design.md, "computed/derived getter"
|
||||
* limitation).
|
||||
* <p>
|
||||
* Compiles a minimal in-memory source set directly through {@code javax.tools.JavaCompiler} with
|
||||
* this module's {@link Processor} registered explicitly - no external compile-testing dependency
|
||||
* required (mirrors {@link DtoMapperFetchPathCollisionTest}).
|
||||
*/
|
||||
class DtoMapperComputedPathTest {
|
||||
|
||||
@Test
|
||||
void dtoPathThroughComputedGetter_withoutRequires_expectCompileError() throws IOException {
|
||||
Path sourceDir = Files.createTempDirectory("dto-computed-src");
|
||||
Path outDir = Files.createTempDirectory("dto-computed-out");
|
||||
writeSource(sourceDir, "org.tests.computed.Bar",
|
||||
"package org.tests.computed;\n"
|
||||
+ "\n"
|
||||
+ "public class Bar {\n"
|
||||
+ " private Long id;\n"
|
||||
+ " private String name;\n"
|
||||
+ "\n"
|
||||
+ " public Long getId() { return id; }\n"
|
||||
+ " public String getName() { return name; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.computed.Foo",
|
||||
"package org.tests.computed;\n"
|
||||
+ "\n"
|
||||
+ "import java.util.List;\n"
|
||||
+ "\n"
|
||||
+ "public class Foo {\n"
|
||||
+ " private List<Bar> bars;\n"
|
||||
+ "\n"
|
||||
+ " public List<Bar> getBars() { return bars; }\n"
|
||||
+ "\n"
|
||||
+ " // computed/derived getter - no backing 'firstBar' field\n"
|
||||
+ " public Bar getFirstBar() { return bars.isEmpty() ? null : bars.get(0); }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.computed.FooDto",
|
||||
"package org.tests.computed;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoPath;\n"
|
||||
+ "\n"
|
||||
+ "public class FooDto {\n"
|
||||
+ " @DtoPath(\"firstBar.name\")\n"
|
||||
+ " private final String firstBarName;\n"
|
||||
+ "\n"
|
||||
+ " public FooDto(String firstBarName) {\n"
|
||||
+ " this.firstBarName = firstBarName;\n"
|
||||
+ " }\n"
|
||||
+ "\n"
|
||||
+ " public String getFirstBarName() { return firstBarName; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.computed.package-info",
|
||||
"@DtoMapping(source = Foo.class, target = FooDto.class)\n"
|
||||
+ "package org.tests.computed;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoMapping;\n");
|
||||
|
||||
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.getDefault(), null)) {
|
||||
List<Path> sourceFiles;
|
||||
try (var walk = Files.walk(sourceDir)) {
|
||||
sourceFiles = walk.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
|
||||
}
|
||||
Iterable<? extends JavaFileObject> compilationUnits =
|
||||
fileManager.getJavaFileObjectsFromPaths(sourceFiles);
|
||||
|
||||
List<String> options = List.of(
|
||||
"-d", outDir.toString(),
|
||||
"-classpath", System.getProperty("java.class.path"),
|
||||
"-processor", Processor.class.getName());
|
||||
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(
|
||||
null, fileManager, diagnostics, options, null, compilationUnits);
|
||||
boolean success = task.call();
|
||||
|
||||
assertFalse(success, "compilation should fail due to the missing @DtoPath(requires = ...)");
|
||||
List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.getDiagnostics().stream()
|
||||
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
|
||||
.collect(Collectors.toList());
|
||||
assertFalse(errors.isEmpty(), "expected at least one compile ERROR diagnostic");
|
||||
boolean matched = errors.stream()
|
||||
.map(d -> d.getMessage(Locale.getDefault()))
|
||||
.anyMatch(msg -> msg.contains("no backing field")
|
||||
&& msg.contains("computed/derived getter")
|
||||
&& msg.contains("requires"));
|
||||
assertTrue(matched, "expected the computed-getter requires() error message, got: " + errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same defect as above, but exercising the {@code NESTED_ONE} branch rather than {@code SCALAR}
|
||||
* - a single-hop {@code @DtoPath} rename whose target field type matches a separately registered
|
||||
* nested DTO mapping. Prior to the fix, this case bypassed the computed-segment validation
|
||||
* entirely (the nested-lookup branch returned early before it ran).
|
||||
*/
|
||||
@Test
|
||||
void dtoPathThroughComputedGetter_targetingNestedDto_withoutRequires_expectCompileError() throws IOException {
|
||||
Path sourceDir = Files.createTempDirectory("dto-computed-nested-src");
|
||||
Path outDir = Files.createTempDirectory("dto-computed-nested-out");
|
||||
writeSource(sourceDir, "org.tests.computednested.Bar",
|
||||
"package org.tests.computednested;\n"
|
||||
+ "\n"
|
||||
+ "public class Bar {\n"
|
||||
+ " private Long id;\n"
|
||||
+ " private String name;\n"
|
||||
+ "\n"
|
||||
+ " public Long getId() { return id; }\n"
|
||||
+ " public String getName() { return name; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.computednested.BarDto",
|
||||
"package org.tests.computednested;\n"
|
||||
+ "\n"
|
||||
+ "public class BarDto {\n"
|
||||
+ " private final Long id;\n"
|
||||
+ " private final String name;\n"
|
||||
+ "\n"
|
||||
+ " public BarDto(Long id, String name) {\n"
|
||||
+ " this.id = id;\n"
|
||||
+ " this.name = name;\n"
|
||||
+ " }\n"
|
||||
+ "\n"
|
||||
+ " public Long getId() { return id; }\n"
|
||||
+ " public String getName() { return name; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.computednested.Foo",
|
||||
"package org.tests.computednested;\n"
|
||||
+ "\n"
|
||||
+ "import java.util.List;\n"
|
||||
+ "\n"
|
||||
+ "public class Foo {\n"
|
||||
+ " private List<Bar> bars;\n"
|
||||
+ "\n"
|
||||
+ " public List<Bar> getBars() { return bars; }\n"
|
||||
+ "\n"
|
||||
+ " // computed/derived getter - no backing 'firstBar' field\n"
|
||||
+ " public Bar getFirstBar() { return bars.isEmpty() ? null : bars.get(0); }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.computednested.FooDto",
|
||||
"package org.tests.computednested;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoPath;\n"
|
||||
+ "\n"
|
||||
+ "public class FooDto {\n"
|
||||
+ " @DtoPath(\"firstBar\")\n"
|
||||
+ " private final BarDto firstBar;\n"
|
||||
+ "\n"
|
||||
+ " public FooDto(BarDto firstBar) {\n"
|
||||
+ " this.firstBar = firstBar;\n"
|
||||
+ " }\n"
|
||||
+ "\n"
|
||||
+ " public BarDto getFirstBar() { return firstBar; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.computednested.package-info",
|
||||
"@DtoMapping(source = Bar.class, target = BarDto.class)\n"
|
||||
+ "@DtoMapping(source = Foo.class, target = FooDto.class)\n"
|
||||
+ "package org.tests.computednested;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoMapping;\n");
|
||||
|
||||
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.getDefault(), null)) {
|
||||
List<Path> sourceFiles;
|
||||
try (var walk = Files.walk(sourceDir)) {
|
||||
sourceFiles = walk.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
|
||||
}
|
||||
Iterable<? extends JavaFileObject> compilationUnits =
|
||||
fileManager.getJavaFileObjectsFromPaths(sourceFiles);
|
||||
|
||||
List<String> options = List.of(
|
||||
"-d", outDir.toString(),
|
||||
"-classpath", System.getProperty("java.class.path"),
|
||||
"-processor", Processor.class.getName());
|
||||
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(
|
||||
null, fileManager, diagnostics, options, null, compilationUnits);
|
||||
boolean success = task.call();
|
||||
|
||||
assertFalse(success, "compilation should fail due to the missing @DtoPath(requires = ...)");
|
||||
List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.getDiagnostics().stream()
|
||||
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
|
||||
.collect(Collectors.toList());
|
||||
assertFalse(errors.isEmpty(), "expected at least one compile ERROR diagnostic");
|
||||
boolean matched = errors.stream()
|
||||
.map(d -> d.getMessage(Locale.getDefault()))
|
||||
.anyMatch(msg -> msg.contains("no backing field")
|
||||
&& msg.contains("computed/derived getter")
|
||||
&& msg.contains("requires"));
|
||||
assertTrue(matched, "expected the computed-getter requires() error message, got: " + errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same defect class as the {@code @DtoPath} cases above, but for {@code @DtoRef} - a computed
|
||||
* association getter with no backing field used via {@code @DtoRef} without {@code requires()}
|
||||
* must fail fast at compile time rather than compile cleanly and fail later at runtime.
|
||||
*/
|
||||
@Test
|
||||
void dtoRefThroughComputedGetter_withoutRequires_expectCompileError() throws IOException {
|
||||
Path sourceDir = Files.createTempDirectory("dto-ref-computed-src");
|
||||
Path outDir = Files.createTempDirectory("dto-ref-computed-out");
|
||||
writeSource(sourceDir, "org.tests.refcomputed.Bar",
|
||||
"package org.tests.refcomputed;\n"
|
||||
+ "\n"
|
||||
+ "public class Bar {\n"
|
||||
+ " private Long id;\n"
|
||||
+ "\n"
|
||||
+ " public Long getId() { return id; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.refcomputed.Foo",
|
||||
"package org.tests.refcomputed;\n"
|
||||
+ "\n"
|
||||
+ "import java.util.List;\n"
|
||||
+ "\n"
|
||||
+ "public class Foo {\n"
|
||||
+ " private List<Bar> bars;\n"
|
||||
+ "\n"
|
||||
+ " public List<Bar> getBars() { return bars; }\n"
|
||||
+ "\n"
|
||||
+ " // computed/derived getter - no backing 'firstBar' field\n"
|
||||
+ " public Bar getFirstBar() { return bars.isEmpty() ? null : bars.get(0); }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.refcomputed.FooDto",
|
||||
"package org.tests.refcomputed;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoRef;\n"
|
||||
+ "\n"
|
||||
+ "public class FooDto {\n"
|
||||
+ " @DtoRef\n"
|
||||
+ " private final Long firstBarId;\n"
|
||||
+ "\n"
|
||||
+ " public FooDto(Long firstBarId) {\n"
|
||||
+ " this.firstBarId = firstBarId;\n"
|
||||
+ " }\n"
|
||||
+ "\n"
|
||||
+ " public Long getFirstBarId() { return firstBarId; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.refcomputed.package-info",
|
||||
"@DtoMapping(source = Foo.class, target = FooDto.class)\n"
|
||||
+ "package org.tests.refcomputed;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoMapping;\n");
|
||||
|
||||
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.getDefault(), null)) {
|
||||
List<Path> sourceFiles;
|
||||
try (var walk = Files.walk(sourceDir)) {
|
||||
sourceFiles = walk.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
|
||||
}
|
||||
Iterable<? extends JavaFileObject> compilationUnits =
|
||||
fileManager.getJavaFileObjectsFromPaths(sourceFiles);
|
||||
|
||||
List<String> options = List.of(
|
||||
"-d", outDir.toString(),
|
||||
"-classpath", System.getProperty("java.class.path"),
|
||||
"-processor", Processor.class.getName());
|
||||
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(
|
||||
null, fileManager, diagnostics, options, null, compilationUnits);
|
||||
boolean success = task.call();
|
||||
|
||||
assertFalse(success, "compilation should fail due to the missing @DtoRef(requires = ...)");
|
||||
List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.getDiagnostics().stream()
|
||||
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
|
||||
.collect(Collectors.toList());
|
||||
assertFalse(errors.isEmpty(), "expected at least one compile ERROR diagnostic");
|
||||
boolean matched = errors.stream()
|
||||
.map(d -> d.getMessage(Locale.getDefault()))
|
||||
.anyMatch(msg -> msg.contains("no backing field")
|
||||
&& msg.contains("computed/derived getter")
|
||||
&& msg.contains("requires"));
|
||||
assertTrue(matched, "expected the computed-getter requires() error message, got: " + errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code requires()} path value with a typo'd segment (not the computed segment itself, the
|
||||
* developer-declared dependency path) must also fail fast at compile time - {@code requires()}
|
||||
* values are handed straight through to {@code FetchGroup.fetch(...)}, so an unchecked typo
|
||||
* there would silently reintroduce the exact runtime {@code PersistenceException} the whole
|
||||
* escape hatch exists to prevent.
|
||||
*/
|
||||
@Test
|
||||
void dtoPathRequires_withTypoInPathValue_expectCompileError() throws IOException {
|
||||
Path sourceDir = Files.createTempDirectory("dto-requires-typo-src");
|
||||
Path outDir = Files.createTempDirectory("dto-requires-typo-out");
|
||||
writeSource(sourceDir, "org.tests.requirestypo.Bar",
|
||||
"package org.tests.requirestypo;\n"
|
||||
+ "\n"
|
||||
+ "public class Bar {\n"
|
||||
+ " private Long id;\n"
|
||||
+ " private String name;\n"
|
||||
+ "\n"
|
||||
+ " public Long getId() { return id; }\n"
|
||||
+ " public String getName() { return name; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.requirestypo.Foo",
|
||||
"package org.tests.requirestypo;\n"
|
||||
+ "\n"
|
||||
+ "import java.util.List;\n"
|
||||
+ "\n"
|
||||
+ "public class Foo {\n"
|
||||
+ " private List<Bar> bars;\n"
|
||||
+ "\n"
|
||||
+ " public List<Bar> getBars() { return bars; }\n"
|
||||
+ "\n"
|
||||
+ " // computed/derived getter - no backing 'firstBar' field\n"
|
||||
+ " public Bar getFirstBar() { return bars.isEmpty() ? null : bars.get(0); }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.requirestypo.FooDto",
|
||||
"package org.tests.requirestypo;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoPath;\n"
|
||||
+ "\n"
|
||||
+ "public class FooDto {\n"
|
||||
+ " // 'barz' is a typo for the real 'bars' property\n"
|
||||
+ " @DtoPath(value = \"firstBar.name\", requires = \"barz\")\n"
|
||||
+ " private final String firstBarName;\n"
|
||||
+ "\n"
|
||||
+ " public FooDto(String firstBarName) {\n"
|
||||
+ " this.firstBarName = firstBarName;\n"
|
||||
+ " }\n"
|
||||
+ "\n"
|
||||
+ " public String getFirstBarName() { return firstBarName; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.requirestypo.package-info",
|
||||
"@DtoMapping(source = Foo.class, target = FooDto.class)\n"
|
||||
+ "package org.tests.requirestypo;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoMapping;\n");
|
||||
|
||||
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.getDefault(), null)) {
|
||||
List<Path> sourceFiles;
|
||||
try (var walk = Files.walk(sourceDir)) {
|
||||
sourceFiles = walk.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
|
||||
}
|
||||
Iterable<? extends JavaFileObject> compilationUnits =
|
||||
fileManager.getJavaFileObjectsFromPaths(sourceFiles);
|
||||
|
||||
List<String> options = List.of(
|
||||
"-d", outDir.toString(),
|
||||
"-classpath", System.getProperty("java.class.path"),
|
||||
"-processor", Processor.class.getName());
|
||||
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(
|
||||
null, fileManager, diagnostics, options, null, compilationUnits);
|
||||
boolean success = task.call();
|
||||
|
||||
assertFalse(success, "compilation should fail due to the typo'd requires() path value");
|
||||
List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.getDiagnostics().stream()
|
||||
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
|
||||
.collect(Collectors.toList());
|
||||
assertFalse(errors.isEmpty(), "expected at least one compile ERROR diagnostic");
|
||||
boolean matched = errors.stream()
|
||||
.map(d -> d.getMessage(Locale.getDefault()))
|
||||
.anyMatch(msg -> msg.contains("no backing field")
|
||||
&& msg.contains("barz")
|
||||
&& msg.contains("typo"));
|
||||
assertTrue(matched, "expected the requires() typo error message, got: " + errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Two {@code @DtoMixin} companion types targeting the same DTO class must fail fast at compile
|
||||
* time - previously the second registration silently overwrote the first in
|
||||
* {@code mixinsByTarget}, so whichever mixin was processed last would win with no diagnostic at
|
||||
* all, silently discarding the other mixin's {@code @DtoPath}/{@code @DtoRef}/{@code @DtoConvert}
|
||||
* overlays.
|
||||
*/
|
||||
@Test
|
||||
void duplicateDtoMixin_forSameTarget_expectCompileError() throws IOException {
|
||||
Path sourceDir = Files.createTempDirectory("dto-mixin-dup-src");
|
||||
Path outDir = Files.createTempDirectory("dto-mixin-dup-out");
|
||||
writeSource(sourceDir, "org.tests.mixindup.FooDto",
|
||||
"package org.tests.mixindup;\n"
|
||||
+ "\n"
|
||||
+ "public class FooDto {\n"
|
||||
+ " private final String bar;\n"
|
||||
+ "\n"
|
||||
+ " public FooDto(String bar) {\n"
|
||||
+ " this.bar = bar;\n"
|
||||
+ " }\n"
|
||||
+ "\n"
|
||||
+ " public String getBar() { return bar; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.mixindup.FooMixinA",
|
||||
"package org.tests.mixindup;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoMixin;\n"
|
||||
+ "\n"
|
||||
+ "@DtoMixin(FooDto.class)\n"
|
||||
+ "interface FooMixinA {\n"
|
||||
+ " String bar();\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.mixindup.FooMixinB",
|
||||
"package org.tests.mixindup;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoMixin;\n"
|
||||
+ "\n"
|
||||
+ "@DtoMixin(FooDto.class)\n"
|
||||
+ "interface FooMixinB {\n"
|
||||
+ " String bar();\n"
|
||||
+ "}\n");
|
||||
|
||||
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.getDefault(), null)) {
|
||||
List<Path> sourceFiles;
|
||||
try (var walk = Files.walk(sourceDir)) {
|
||||
sourceFiles = walk.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
|
||||
}
|
||||
Iterable<? extends JavaFileObject> compilationUnits =
|
||||
fileManager.getJavaFileObjectsFromPaths(sourceFiles);
|
||||
|
||||
List<String> options = List.of(
|
||||
"-d", outDir.toString(),
|
||||
"-classpath", System.getProperty("java.class.path"),
|
||||
"-processor", Processor.class.getName());
|
||||
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(
|
||||
null, fileManager, diagnostics, options, null, compilationUnits);
|
||||
boolean success = task.call();
|
||||
|
||||
assertFalse(success, "compilation should fail due to the duplicate @DtoMixin for the same target");
|
||||
List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.getDiagnostics().stream()
|
||||
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
|
||||
.collect(Collectors.toList());
|
||||
assertFalse(errors.isEmpty(), "expected at least one compile ERROR diagnostic");
|
||||
boolean matched = errors.stream()
|
||||
.map(d -> d.getMessage(Locale.getDefault()))
|
||||
.anyMatch(msg -> msg.contains("Duplicate @DtoMixin") && msg.contains("FooDto"));
|
||||
assertTrue(matched, "expected the duplicate @DtoMixin error message, got: " + errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A field carrying both {@code @DtoRef} and {@code @DtoPath} at once must fail fast at compile
|
||||
* time - previously {@code resolveProperty()} checked {@code @DtoRef} first and returned early,
|
||||
* silently ignoring any {@code @DtoPath} also present on the same field with no diagnostic,
|
||||
* discarding whichever rename/path semantics the developer actually intended.
|
||||
*/
|
||||
@Test
|
||||
void dtoRefAndDtoPath_onSameField_expectCompileError() throws IOException {
|
||||
Path sourceDir = Files.createTempDirectory("dto-ref-path-conflict-src");
|
||||
Path outDir = Files.createTempDirectory("dto-ref-path-conflict-out");
|
||||
writeSource(sourceDir, "org.tests.refpathconflict.Bar",
|
||||
"package org.tests.refpathconflict;\n"
|
||||
+ "\n"
|
||||
+ "public class Bar {\n"
|
||||
+ " private Long id;\n"
|
||||
+ "\n"
|
||||
+ " public Long getId() { return id; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.refpathconflict.Foo",
|
||||
"package org.tests.refpathconflict;\n"
|
||||
+ "\n"
|
||||
+ "public class Foo {\n"
|
||||
+ " private Bar bar;\n"
|
||||
+ "\n"
|
||||
+ " public Bar getBar() { return bar; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.refpathconflict.FooDto",
|
||||
"package org.tests.refpathconflict;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoPath;\n"
|
||||
+ "import io.ebean.annotation.DtoRef;\n"
|
||||
+ "\n"
|
||||
+ "public class FooDto {\n"
|
||||
+ " @DtoRef\n"
|
||||
+ " @DtoPath(\"bar.id\")\n"
|
||||
+ " private final Long barId;\n"
|
||||
+ "\n"
|
||||
+ " public FooDto(Long barId) {\n"
|
||||
+ " this.barId = barId;\n"
|
||||
+ " }\n"
|
||||
+ "\n"
|
||||
+ " public Long getBarId() { return barId; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.refpathconflict.package-info",
|
||||
"@DtoMapping(source = Foo.class, target = FooDto.class)\n"
|
||||
+ "package org.tests.refpathconflict;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoMapping;\n");
|
||||
|
||||
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.getDefault(), null)) {
|
||||
List<Path> sourceFiles;
|
||||
try (var walk = Files.walk(sourceDir)) {
|
||||
sourceFiles = walk.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
|
||||
}
|
||||
Iterable<? extends JavaFileObject> compilationUnits =
|
||||
fileManager.getJavaFileObjectsFromPaths(sourceFiles);
|
||||
|
||||
List<String> options = List.of(
|
||||
"-d", outDir.toString(),
|
||||
"-classpath", System.getProperty("java.class.path"),
|
||||
"-processor", Processor.class.getName());
|
||||
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(
|
||||
null, fileManager, diagnostics, options, null, compilationUnits);
|
||||
boolean success = task.call();
|
||||
|
||||
assertFalse(success, "compilation should fail due to both @DtoRef and @DtoPath on the same field");
|
||||
List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.getDiagnostics().stream()
|
||||
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
|
||||
.collect(Collectors.toList());
|
||||
assertFalse(errors.isEmpty(), "expected at least one compile ERROR diagnostic");
|
||||
boolean matched = errors.stream()
|
||||
.map(d -> d.getMessage(Locale.getDefault()))
|
||||
.anyMatch(msg -> msg.contains("both @DtoRef and @DtoPath") && msg.contains("mutually exclusive"));
|
||||
assertTrue(matched, "expected the @DtoRef/@DtoPath conflict error message, got: " + errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code @DtoConvert} on a {@code NESTED_ONE} (or {@code NESTED_MANY}) property must fail fast
|
||||
* at compile time - previously the converter was resolved but simply never wired into the
|
||||
* {@code NESTED_ONE}/{@code NESTED_MANY} {@link DtoPropertyMeta} constructor calls, so the
|
||||
* annotation silently had zero effect (the nested mapper's own {@code map(...)} call always
|
||||
* fully determines the value), with no diagnostic telling the developer it was ignored.
|
||||
*/
|
||||
@Test
|
||||
void dtoConvertOnNestedOne_expectCompileError() throws IOException {
|
||||
Path sourceDir = Files.createTempDirectory("dto-convert-nested-src");
|
||||
Path outDir = Files.createTempDirectory("dto-convert-nested-out");
|
||||
writeSource(sourceDir, "org.tests.convertnested.Bar",
|
||||
"package org.tests.convertnested;\n"
|
||||
+ "\n"
|
||||
+ "public class Bar {\n"
|
||||
+ " private Long id;\n"
|
||||
+ "\n"
|
||||
+ " public Long getId() { return id; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.convertnested.BarDto",
|
||||
"package org.tests.convertnested;\n"
|
||||
+ "\n"
|
||||
+ "public class BarDto {\n"
|
||||
+ " private final Long id;\n"
|
||||
+ "\n"
|
||||
+ " public BarDto(Long id) {\n"
|
||||
+ " this.id = id;\n"
|
||||
+ " }\n"
|
||||
+ "\n"
|
||||
+ " public Long getId() { return id; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.convertnested.Foo",
|
||||
"package org.tests.convertnested;\n"
|
||||
+ "\n"
|
||||
+ "public class Foo {\n"
|
||||
+ " private Bar bar;\n"
|
||||
+ "\n"
|
||||
+ " public Bar getBar() { return bar; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.convertnested.BarConverter",
|
||||
"package org.tests.convertnested;\n"
|
||||
+ "\n"
|
||||
+ "public class BarConverter {\n"
|
||||
+ " public static BarDto identity(BarDto dto) { return dto; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.convertnested.FooDto",
|
||||
"package org.tests.convertnested;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoConvert;\n"
|
||||
+ "\n"
|
||||
+ "public class FooDto {\n"
|
||||
+ " @DtoConvert(value = BarConverter.class, method = \"identity\")\n"
|
||||
+ " private final BarDto bar;\n"
|
||||
+ "\n"
|
||||
+ " public FooDto(BarDto bar) {\n"
|
||||
+ " this.bar = bar;\n"
|
||||
+ " }\n"
|
||||
+ "\n"
|
||||
+ " public BarDto getBar() { return bar; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.convertnested.package-info",
|
||||
"@DtoMapping(source = Bar.class, target = BarDto.class)\n"
|
||||
+ "@DtoMapping(source = Foo.class, target = FooDto.class)\n"
|
||||
+ "package org.tests.convertnested;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoMapping;\n");
|
||||
|
||||
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.getDefault(), null)) {
|
||||
List<Path> sourceFiles;
|
||||
try (var walk = Files.walk(sourceDir)) {
|
||||
sourceFiles = walk.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
|
||||
}
|
||||
Iterable<? extends JavaFileObject> compilationUnits =
|
||||
fileManager.getJavaFileObjectsFromPaths(sourceFiles);
|
||||
|
||||
List<String> options = List.of(
|
||||
"-d", outDir.toString(),
|
||||
"-classpath", System.getProperty("java.class.path"),
|
||||
"-processor", Processor.class.getName());
|
||||
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(
|
||||
null, fileManager, diagnostics, options, null, compilationUnits);
|
||||
boolean success = task.call();
|
||||
|
||||
assertFalse(success, "compilation should fail due to @DtoConvert on a NESTED_ONE property");
|
||||
List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.getDiagnostics().stream()
|
||||
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
|
||||
.collect(Collectors.toList());
|
||||
assertFalse(errors.isEmpty(), "expected at least one compile ERROR diagnostic");
|
||||
boolean matched = errors.stream()
|
||||
.map(d -> d.getMessage(Locale.getDefault()))
|
||||
.anyMatch(msg -> msg.contains("@DtoConvert") && msg.contains("has no effect")
|
||||
&& msg.contains("nested DTO graph property"));
|
||||
assertTrue(matched, "expected the @DtoConvert-on-nested error message, got: " + errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code @DtoConvert(method = ...)} reference to a converter type with two overloads sharing
|
||||
* that name, both taking exactly one parameter, must fail fast at compile time rather than
|
||||
* silently binding to whichever overload {@code ElementFilter.methodsIn} happens to return
|
||||
* first (unrelated to which one the developer actually meant) - {@code @DtoConvert} has no way
|
||||
* to disambiguate by parameter type since it's declared by name alone.
|
||||
*/
|
||||
@Test
|
||||
void dtoConvertMethod_withAmbiguousOverloads_expectCompileError() throws IOException {
|
||||
Path sourceDir = Files.createTempDirectory("dto-convert-ambiguous-src");
|
||||
Path outDir = Files.createTempDirectory("dto-convert-ambiguous-out");
|
||||
writeSource(sourceDir, "org.tests.convertambiguous.Foo",
|
||||
"package org.tests.convertambiguous;\n"
|
||||
+ "\n"
|
||||
+ "public class Foo {\n"
|
||||
+ " private String name;\n"
|
||||
+ "\n"
|
||||
+ " public String getName() { return name; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.convertambiguous.NameConverter",
|
||||
"package org.tests.convertambiguous;\n"
|
||||
+ "\n"
|
||||
+ "public class NameConverter {\n"
|
||||
+ " public static String format(String value) { return value; }\n"
|
||||
+ " public static String format(Object value) { return String.valueOf(value); }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.convertambiguous.FooDto",
|
||||
"package org.tests.convertambiguous;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoConvert;\n"
|
||||
+ "\n"
|
||||
+ "public class FooDto {\n"
|
||||
+ " @DtoConvert(value = NameConverter.class, method = \"format\")\n"
|
||||
+ " private final String name;\n"
|
||||
+ "\n"
|
||||
+ " public FooDto(String name) {\n"
|
||||
+ " this.name = name;\n"
|
||||
+ " }\n"
|
||||
+ "\n"
|
||||
+ " public String getName() { return name; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.convertambiguous.package-info",
|
||||
"@DtoMapping(source = Foo.class, target = FooDto.class)\n"
|
||||
+ "package org.tests.convertambiguous;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoMapping;\n");
|
||||
|
||||
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.getDefault(), null)) {
|
||||
List<Path> sourceFiles;
|
||||
try (var walk = Files.walk(sourceDir)) {
|
||||
sourceFiles = walk.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
|
||||
}
|
||||
Iterable<? extends JavaFileObject> compilationUnits =
|
||||
fileManager.getJavaFileObjectsFromPaths(sourceFiles);
|
||||
|
||||
List<String> options = List.of(
|
||||
"-d", outDir.toString(),
|
||||
"-classpath", System.getProperty("java.class.path"),
|
||||
"-processor", Processor.class.getName());
|
||||
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(
|
||||
null, fileManager, diagnostics, options, null, compilationUnits);
|
||||
boolean success = task.call();
|
||||
|
||||
assertFalse(success, "compilation should fail due to the ambiguous @DtoConvert method overloads");
|
||||
List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.getDiagnostics().stream()
|
||||
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
|
||||
.collect(Collectors.toList());
|
||||
assertFalse(errors.isEmpty(), "expected at least one compile ERROR diagnostic");
|
||||
boolean matched = errors.stream()
|
||||
.map(d -> d.getMessage(Locale.getDefault()))
|
||||
.anyMatch(msg -> msg.contains("ambiguous") && msg.contains("format") && msg.contains("2 overloads"));
|
||||
assertTrue(matched, "expected the ambiguous @DtoConvert overload error message, got: " + errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@code @DtoConvert(method = ...)} reference to a method that exists on the converter type
|
||||
* but doesn't take exactly one parameter (e.g. a zero-arg or two-arg overload sharing the name)
|
||||
* must fail fast at compile time with a clear message, rather than {@code findMethod} matching
|
||||
* it anyway and generating a call the compiler will reject with an unrelated arity-mismatch
|
||||
* error in the generated mapper source.
|
||||
*/
|
||||
@Test
|
||||
void dtoConvertMethod_withWrongArity_expectCompileError() throws IOException {
|
||||
Path sourceDir = Files.createTempDirectory("dto-convert-arity-src");
|
||||
Path outDir = Files.createTempDirectory("dto-convert-arity-out");
|
||||
writeSource(sourceDir, "org.tests.convertarity.Foo",
|
||||
"package org.tests.convertarity;\n"
|
||||
+ "\n"
|
||||
+ "public class Foo {\n"
|
||||
+ " private String name;\n"
|
||||
+ "\n"
|
||||
+ " public String getName() { return name; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.convertarity.NameConverter",
|
||||
"package org.tests.convertarity;\n"
|
||||
+ "\n"
|
||||
+ "public class NameConverter {\n"
|
||||
+ " public static String format() { return \"\"; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.convertarity.FooDto",
|
||||
"package org.tests.convertarity;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoConvert;\n"
|
||||
+ "\n"
|
||||
+ "public class FooDto {\n"
|
||||
+ " @DtoConvert(value = NameConverter.class, method = \"format\")\n"
|
||||
+ " private final String name;\n"
|
||||
+ "\n"
|
||||
+ " public FooDto(String name) {\n"
|
||||
+ " this.name = name;\n"
|
||||
+ " }\n"
|
||||
+ "\n"
|
||||
+ " public String getName() { return name; }\n"
|
||||
+ "}\n");
|
||||
writeSource(sourceDir, "org.tests.convertarity.package-info",
|
||||
"@DtoMapping(source = Foo.class, target = FooDto.class)\n"
|
||||
+ "package org.tests.convertarity;\n"
|
||||
+ "\n"
|
||||
+ "import io.ebean.annotation.DtoMapping;\n");
|
||||
|
||||
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.getDefault(), null)) {
|
||||
List<Path> sourceFiles;
|
||||
try (var walk = Files.walk(sourceDir)) {
|
||||
sourceFiles = walk.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
|
||||
}
|
||||
Iterable<? extends JavaFileObject> compilationUnits =
|
||||
fileManager.getJavaFileObjectsFromPaths(sourceFiles);
|
||||
|
||||
List<String> options = List.of(
|
||||
"-d", outDir.toString(),
|
||||
"-classpath", System.getProperty("java.class.path"),
|
||||
"-processor", Processor.class.getName());
|
||||
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(
|
||||
null, fileManager, diagnostics, options, null, compilationUnits);
|
||||
boolean success = task.call();
|
||||
|
||||
assertFalse(success, "compilation should fail due to the wrong-arity @DtoConvert method");
|
||||
List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.getDiagnostics().stream()
|
||||
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
|
||||
.collect(Collectors.toList());
|
||||
assertFalse(errors.isEmpty(), "expected at least one compile ERROR diagnostic");
|
||||
boolean matched = errors.stream()
|
||||
.map(d -> d.getMessage(Locale.getDefault()))
|
||||
.anyMatch(msg -> msg.contains("not found on") && msg.contains("exactly one parameter"));
|
||||
assertTrue(matched, "expected the wrong-arity @DtoConvert error message, got: " + errors);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSource(Path sourceDir, String fqn, String content) {
|
||||
try {
|
||||
Path pkgDir = sourceDir.resolve(fqn.substring(0, fqn.lastIndexOf('.')).replace('.', '/'));
|
||||
Files.createDirectories(pkgDir);
|
||||
String simpleName = fqn.substring(fqn.lastIndexOf('.') + 1);
|
||||
Path file = pkgDir.resolve(simpleName + ".java");
|
||||
try (Writer writer = Files.newBufferedWriter(file)) {
|
||||
writer.write(content);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>tests</artifactId>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>tests</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>test-dto-mapping</artifactId>
|
||||
|
||||
@@ -49,4 +49,51 @@ public class Customer extends Model {
|
||||
public List<Contact> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed/derived getter (no backing field) - not a real Ebean property. Exercises
|
||||
* {@code @DtoPath#requires()}: a {@code @DtoPath} traversing this segment must explicitly name
|
||||
* {@code "contacts"} as a required fetch, since this method's own data dependency (the
|
||||
* {@code contacts} collection) can't be inferred from the path string alone.
|
||||
*/
|
||||
public Contact getPrimaryContact() {
|
||||
return contacts.isEmpty() ? null : contacts.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed/derived getter (no backing field) deriving purely from {@code id} - which is always
|
||||
* fetched as a matter of course, so unlike {@link #getPrimaryContact()} this one genuinely needs
|
||||
* nothing extra fetched. Exercises {@code @DtoPath(requires = {})} (explicit empty array,
|
||||
* confirming "nothing extra needed") as distinct from omitting {@code requires} entirely (a
|
||||
* compile error).
|
||||
*/
|
||||
public String getIdBadge() {
|
||||
return "CUST-" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed/derived getter (no backing field) reading {@code billingAddress.line1} - deliberately
|
||||
* a different {@code Address} property to {@code city} (used narrowly elsewhere, see
|
||||
* {@code FetchCollisionDto}), to exercise the priority between a bare, full {@code requires()}
|
||||
* fetch of {@code billingAddress} and a sibling property's narrowed {@code fetch("billingAddress",
|
||||
* "city")}: {@code FetchGroup}'s builder replaces (not merges) same-path fetch calls, so if the
|
||||
* narrowed selection silently won, {@code line1} would never be loaded and calling this getter
|
||||
* outside a persistence context would throw {@code LazyInitialisationException} instead of
|
||||
* returning a value.
|
||||
*/
|
||||
public String getBillingSummary() {
|
||||
return billingAddress == null ? null : billingAddress.getLine1() + ", " + billingAddress.getCity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed/derived getter (no backing field) returning a {@code List} - the {@code NESTED_MANY}
|
||||
* counterpart to {@link #getPrimaryContact()}'s {@code NESTED_ONE} case. Exercises
|
||||
* {@code @DtoPath#requires()} through a computed getter whose return type is a {@code List} of
|
||||
* a type with its own registered nested DTO mapping - same underlying code path as
|
||||
* {@code getPrimaryContact()} (single-hop computed segment, {@code DtoMapperWriter}'s
|
||||
* {@code NESTED_ONE}/{@code NESTED_MANY} branch), just the collection variant.
|
||||
*/
|
||||
public List<Contact> getRecentContacts() {
|
||||
return contacts.isEmpty() ? List.of() : contacts.subList(0, Math.min(2, contacts.size()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.annotation.DtoPath;
|
||||
|
||||
/**
|
||||
* Regression coverage for a single-hop {@code @DtoPath} rename traversing a computed/derived
|
||||
* getter whose return type matches a <b>registered nested DTO mapping</b> - a variant of
|
||||
* {@link ComputedPathDto} that exercises {@code DtoMapperWriter}'s {@code NESTED_ONE}/
|
||||
* {@code NESTED_MANY} branch rather than its {@code SCALAR} branch.
|
||||
* <p>
|
||||
* {@code primaryContact} traverses {@code Customer#getPrimaryContact()} (no backing field - see
|
||||
* {@link org.tests.dtomapping.model.Customer}), but its return type ({@code Contact}) has its own
|
||||
* registered {@code @DtoMapping} to {@link ContactLeafDto} - so the field type here is
|
||||
* {@code ContactLeafDto}, not a plain scalar. Without the fix, this single-hop case bypassed the
|
||||
* computed-segment {@code requires()} validation entirely (the {@code NESTED_ONE} lookup returned
|
||||
* early before it ran) and would have generated a broken {@code fetch("primaryContact",
|
||||
* contactLeafMapper.fetchGroup())} call - {@code "primaryContact"} isn't a real Ebean fetch path,
|
||||
* so that would fail at runtime with {@code PersistenceException: No property found}.
|
||||
* <p>
|
||||
* {@code @DtoMapping(source = Customer.class, target = ComputedNestedDto.class)} is declared on
|
||||
* {@code package-info.java}.
|
||||
*/
|
||||
public class ComputedNestedDto {
|
||||
|
||||
private final Long id;
|
||||
|
||||
@DtoPath(value = "primaryContact", requires = "contacts")
|
||||
private final ContactLeafDto primaryContact;
|
||||
|
||||
public ComputedNestedDto(Long id, ContactLeafDto primaryContact) {
|
||||
this.id = id;
|
||||
this.primaryContact = primaryContact;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ContactLeafDto getPrimaryContact() {
|
||||
return primaryContact;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.annotation.DtoPath;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Regression coverage for a single-hop {@code @DtoPath} rename traversing a computed/derived
|
||||
* getter whose return type is a {@code List} matching a <b>registered nested DTO mapping</b> -
|
||||
* the {@code NESTED_MANY} counterpart to {@link ComputedNestedDto}'s {@code NESTED_ONE} case.
|
||||
* <p>
|
||||
* {@code recentContacts} traverses {@code Customer#getRecentContacts()} (no backing field - see
|
||||
* {@link org.tests.dtomapping.model.Customer}), returning a {@code List<Contact>} - its element
|
||||
* type has its own registered {@code @DtoMapping} to {@link ContactLeafDto}, so the field type
|
||||
* here is {@code List<ContactLeafDto>}. Shares the exact same codegen path as
|
||||
* {@link ComputedNestedDto} ({@code DtoMapperWriter}'s {@code NESTED_ONE}/{@code NESTED_MANY}
|
||||
* branch both route computed segments to {@code extraFetchPaths} identically) - this is coverage
|
||||
* confirming the {@code NESTED_MANY} variant actually works end-to-end, not a bug fix.
|
||||
* <p>
|
||||
* {@code @DtoMapping(source = Customer.class, target = ComputedNestedListDto.class)} is declared
|
||||
* on {@code package-info.java}.
|
||||
*/
|
||||
public class ComputedNestedListDto {
|
||||
|
||||
private final Long id;
|
||||
|
||||
@DtoPath(value = "recentContacts", requires = "contacts")
|
||||
private final List<ContactLeafDto> recentContacts;
|
||||
|
||||
public ComputedNestedListDto(Long id, List<ContactLeafDto> recentContacts) {
|
||||
this.id = id;
|
||||
this.recentContacts = recentContacts;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public List<ContactLeafDto> getRecentContacts() {
|
||||
return recentContacts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.annotation.DtoPath;
|
||||
|
||||
/**
|
||||
* Regression coverage for {@code @DtoPath#requires()} - {@code primaryContactLastName} traverses
|
||||
* {@code Customer#getPrimaryContact()}, a computed/derived getter with no backing field (it picks
|
||||
* the first entry out of the {@code contacts} collection). Since the generator can't infer that
|
||||
* this getter's own data dependency is {@code contacts}, {@code requires = "contacts"} declares it
|
||||
* explicitly so the generated {@code FetchGroup} includes a bare {@code fetch("contacts")} call -
|
||||
* without it, {@code getPrimaryContact()} would run against an unfetched/lazy collection and either
|
||||
* fail or trigger an extra lazy-load query at map time.
|
||||
* <p>
|
||||
* See {@link org.tests.dtomapping.TestComputedPath} and {@code
|
||||
* DtoMapperComputedPathTest} (querybean-generator module) for the companion negative case - the
|
||||
* same shape of {@code @DtoPath} but with {@code requires} omitted, which must be a compile-time
|
||||
* error rather than a silent runtime failure.
|
||||
* <p>
|
||||
* {@code @DtoMapping(source = Customer.class, target = ComputedPathDto.class)} is declared on
|
||||
* {@code package-info.java}.
|
||||
*/
|
||||
public class ComputedPathDto {
|
||||
|
||||
private final Long id;
|
||||
|
||||
@DtoPath(value = "primaryContact.lastName", requires = "contacts")
|
||||
private final String primaryContactLastName;
|
||||
|
||||
public ComputedPathDto(Long id, String primaryContactLastName) {
|
||||
this.id = id;
|
||||
this.primaryContactLastName = primaryContactLastName;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getPrimaryContactLastName() {
|
||||
return primaryContactLastName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.annotation.DtoPath;
|
||||
|
||||
/**
|
||||
* Regression coverage for {@code @DtoPath(requires = {})} - {@code idBadge} traverses {@code
|
||||
* Customer#getIdBadge()}, a computed/derived getter with no backing field, but one that genuinely
|
||||
* needs nothing extra fetched (it derives purely from {@code id}, which is always fetched as a
|
||||
* matter of course). The explicit empty array confirms that to the generator, as opposed to
|
||||
* omitting {@code requires} entirely (a compile error - see {@link ComputedPathDto} for the case
|
||||
* that does need a real fetch path, and {@code DtoMapperComputedPathTest} in the
|
||||
* querybean-generator module for the negative/omitted case).
|
||||
* <p>
|
||||
* {@code @DtoMapping(source = Customer.class, target = ComputedPathNoFetchDto.class)} is declared
|
||||
* on {@code package-info.java}.
|
||||
*/
|
||||
public class ComputedPathNoFetchDto {
|
||||
|
||||
private final Long id;
|
||||
|
||||
@DtoPath(value = "idBadge", requires = {})
|
||||
private final String idBadge;
|
||||
|
||||
public ComputedPathNoFetchDto(Long id, String idBadge) {
|
||||
this.id = id;
|
||||
this.idBadge = idBadge;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getIdBadge() {
|
||||
return idBadge;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.annotation.DtoRef;
|
||||
|
||||
/**
|
||||
* Regression coverage for {@code @DtoRef} traversing a computed/derived association getter (no
|
||||
* backing field) - a variant of {@link ComputedPathDto} that exercises {@code @DtoRef}'s
|
||||
* {@code Kind.REF} branch rather than {@code @DtoPath}'s {@code SCALAR}/{@code NESTED_ONE}
|
||||
* branches.
|
||||
* <p>
|
||||
* {@code primaryContactId} derives its association name ("primaryContact") from the field name,
|
||||
* which resolves to {@code Customer#getPrimaryContact()} - a computed getter with no backing
|
||||
* field (see {@link org.tests.dtomapping.model.Customer}). Without the fix, {@code @DtoRef} never
|
||||
* checked whether the association had a backing field at all, so this compiled cleanly and
|
||||
* generated a broken {@code FetchGroup.select("primaryContact")} call - {@code "primaryContact"}
|
||||
* isn't a real Ebean property, so that would fail at runtime with {@code PersistenceException: No
|
||||
* property found}.
|
||||
* <p>
|
||||
* {@code @DtoMapping(source = Customer.class, target = ComputedRefDto.class)} is declared on
|
||||
* {@code package-info.java}.
|
||||
*/
|
||||
public class ComputedRefDto {
|
||||
|
||||
private final Long id;
|
||||
|
||||
@DtoRef(requires = "contacts")
|
||||
private final Long primaryContactId;
|
||||
|
||||
public ComputedRefDto(Long id, Long primaryContactId) {
|
||||
this.id = id;
|
||||
this.primaryContactId = primaryContactId;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Long getPrimaryContactId() {
|
||||
return primaryContactId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
/**
|
||||
* Minimal "leaf" DTO for {@link org.tests.dtomapping.model.Contact} - deliberately has no further
|
||||
* nested relations of its own (unlike {@link ContactDto}, which also maps {@code customer}), so
|
||||
* {@link ComputedNestedDto}'s bare {@code fetch("contacts")} (everything {@code Contact} itself
|
||||
* owns, no deeper paths) is sufficient to satisfy it without needing to reason about propagating a
|
||||
* nested mapper's own fetch requirements up through a computed getter - see {@link ComputedNestedDto}.
|
||||
* <p>
|
||||
* {@code @DtoMapping(source = Contact.class, target = ContactLeafDto.class)} is declared on
|
||||
* {@code package-info.java}.
|
||||
*/
|
||||
public class ContactLeafDto {
|
||||
|
||||
private final Long id;
|
||||
private final String firstName;
|
||||
private final String lastName;
|
||||
|
||||
public ContactLeafDto(Long id, String firstName, String lastName) {
|
||||
this.id = id;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.annotation.DtoPath;
|
||||
|
||||
/**
|
||||
* Regression coverage for the priority between a bare, full {@code requires()} fetch and a
|
||||
* sibling property's narrowed {@code @DtoPath} fetch of the exact same path - see
|
||||
* {@link org.tests.dtomapping.model.Customer#getBillingSummary()}.
|
||||
* <p>
|
||||
* {@code billingCity} narrows the {@code billingAddress} fetch down to just {@code city} (a plain
|
||||
* {@code @DtoPath("billingAddress.city")}). {@code billingSummary} traverses the computed
|
||||
* {@code getBillingSummary()} getter (no backing field), declaring {@code requires =
|
||||
* "billingAddress"} - a bare, full fetch of the exact same path. {@code FetchGroup}'s builder
|
||||
* replaces (not merges) same-path fetch calls, so without prioritizing the full fetch over the
|
||||
* narrow one, {@code line1} (needed internally by {@code getBillingSummary()}, but not by
|
||||
* {@code billingCity}) would silently never be loaded - calling {@code getBillingSummary()} would
|
||||
* then throw {@code LazyInitialisationException} rather than return a value.
|
||||
* <p>
|
||||
* {@code @DtoMapping(source = Customer.class, target = FetchCollisionDto.class)} is declared on
|
||||
* {@code package-info.java}.
|
||||
*/
|
||||
public class FetchCollisionDto {
|
||||
|
||||
private final Long id;
|
||||
|
||||
@DtoPath("billingAddress.city")
|
||||
private final String billingCity;
|
||||
|
||||
@DtoPath(value = "billingSummary", requires = "billingAddress")
|
||||
private final String billingSummary;
|
||||
|
||||
public FetchCollisionDto(Long id, String billingCity, String billingSummary) {
|
||||
this.id = id;
|
||||
this.billingCity = billingCity;
|
||||
this.billingSummary = billingSummary;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getBillingCity() {
|
||||
return billingCity;
|
||||
}
|
||||
|
||||
public String getBillingSummary() {
|
||||
return billingSummary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.annotation.DtoPath;
|
||||
|
||||
/**
|
||||
* Regression coverage for a primitive-typed DTO field derived via a multi-hop {@code @DtoPath}
|
||||
* through a <b>nullable</b> ToOne relation ({@code Customer#getBillingAddress()} can be
|
||||
* {@code null}) - the generated null-guarded getter chain (e.g. {@code (x == null ? null :
|
||||
* x.getId())}) types as boxed {@code Long}, which would auto-unbox to a {@code
|
||||
* NullPointerException} when passed to a primitive constructor parameter if not handled.
|
||||
* <p>
|
||||
* {@code billingAddressId} exercises the default behaviour (silently defaults to {@code 0}) - see
|
||||
* {@link org.tests.dtomapping.PrimitiveNullPathFailOnNullDto} for the {@code failOnNull = true}
|
||||
* counterpart, and {@link org.tests.dtomapping.TestPrimitiveNullPath} for both cases.
|
||||
* <p>
|
||||
* {@code @DtoMapping(source = Contact.class, target = PrimitiveNullPathDto.class)} is declared
|
||||
* on {@code package-info.java}.
|
||||
*/
|
||||
public class PrimitiveNullPathDto {
|
||||
|
||||
@DtoPath("customer.billingAddress.id")
|
||||
private final long billingAddressId;
|
||||
|
||||
public PrimitiveNullPathDto(long billingAddressId) {
|
||||
this.billingAddressId = billingAddressId;
|
||||
}
|
||||
|
||||
public long getBillingAddressId() {
|
||||
return billingAddressId;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.annotation.DtoPath;
|
||||
|
||||
/**
|
||||
* Companion to {@link PrimitiveNullPathDto} isolating {@code @DtoPath(failOnNull = true)} in its
|
||||
* own DTO type - kept separate so a null intermediate hop's exception doesn't also abort
|
||||
* construction of {@link PrimitiveNullPathDto}'s default (silently-zero) field in the same
|
||||
* constructor call. See {@link org.tests.dtomapping.TestPrimitiveNullPath}.
|
||||
* <p>
|
||||
* {@code @DtoMapping(source = Contact.class, target = PrimitiveNullPathFailOnNullDto.class)} is
|
||||
* declared on {@code package-info.java}.
|
||||
*/
|
||||
public class PrimitiveNullPathFailOnNullDto {
|
||||
|
||||
@DtoPath(value = "customer.billingAddress.id", failOnNull = true)
|
||||
private final long billingAddressId;
|
||||
|
||||
public PrimitiveNullPathFailOnNullDto(long billingAddressId) {
|
||||
this.billingAddressId = billingAddressId;
|
||||
}
|
||||
|
||||
public long getBillingAddressId() {
|
||||
return billingAddressId;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.DB;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.dtomapping.model.Contact;
|
||||
import org.tests.dtomapping.model.Customer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Regression test for {@link ComputedNestedListDto} - see its javadoc.
|
||||
*/
|
||||
class TestComputedNestedListPath {
|
||||
|
||||
@Test
|
||||
void mapTo_whenComputedGetterReturnsListTargetingNestedDto_requiresFetchesItsDependency() {
|
||||
Customer customer = new Customer("ComputedNestedListCo");
|
||||
customer.save();
|
||||
Contact alice = new Contact("Alice", "Smith", customer);
|
||||
alice.save();
|
||||
Contact bob = new Contact("Bob", "Jones", customer);
|
||||
bob.save();
|
||||
|
||||
ComputedNestedListDto dto = DB.find(Customer.class)
|
||||
.where().idEq(customer.getId())
|
||||
.mapTo(ComputedNestedListDto.class)
|
||||
.findOne();
|
||||
|
||||
assertThat(dto).isNotNull();
|
||||
assertThat(dto.getRecentContacts()).hasSize(2);
|
||||
assertThat(dto.getRecentContacts().get(0).getFirstName()).isEqualTo("Alice");
|
||||
assertThat(dto.getRecentContacts().get(1).getFirstName()).isEqualTo("Bob");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.DB;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.dtomapping.model.Contact;
|
||||
import org.tests.dtomapping.model.Customer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Regression test for {@link ComputedNestedDto} - see its javadoc.
|
||||
*/
|
||||
class TestComputedNestedPath {
|
||||
|
||||
@Test
|
||||
void mapTo_whenComputedGetterTargetsNestedDto_requiresFetchesItsDependency() {
|
||||
Customer customer = new Customer("ComputedNestedCo");
|
||||
customer.save();
|
||||
Contact contact = new Contact("Bob", "Jones", customer);
|
||||
contact.save();
|
||||
|
||||
ComputedNestedDto dto = DB.find(Customer.class)
|
||||
.where().idEq(customer.getId())
|
||||
.mapTo(ComputedNestedDto.class)
|
||||
.findOne();
|
||||
|
||||
assertThat(dto).isNotNull();
|
||||
assertThat(dto.getPrimaryContact()).isNotNull();
|
||||
assertThat(dto.getPrimaryContact().getFirstName()).isEqualTo("Bob");
|
||||
assertThat(dto.getPrimaryContact().getLastName()).isEqualTo("Jones");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.DB;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.dtomapping.model.Contact;
|
||||
import org.tests.dtomapping.model.Customer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Regression test for {@link ComputedPathDto} (the {@code @DtoPath#requires()} happy path) - see
|
||||
* its javadoc, and {@code DtoMapperComputedPathTest} (querybean-generator module) for the
|
||||
* companion negative/compile-error case.
|
||||
*/
|
||||
class TestComputedPath {
|
||||
|
||||
@Test
|
||||
void mapTo_whenPathTraversesComputedGetter_requiresFetchesItsDependency() {
|
||||
Customer customer = new Customer("ComputedPathCo");
|
||||
customer.save();
|
||||
Contact contact = new Contact("Alice", "Smith", customer);
|
||||
contact.save();
|
||||
|
||||
ComputedPathDto dto = DB.find(Customer.class)
|
||||
.where().idEq(customer.getId())
|
||||
.mapTo(ComputedPathDto.class)
|
||||
.findOne();
|
||||
|
||||
assertThat(dto).isNotNull();
|
||||
assertThat(dto.getPrimaryContactLastName()).isEqualTo("Smith");
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.DB;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.dtomapping.model.Customer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Regression test for {@link ComputedPathNoFetchDto} (the {@code @DtoPath(requires = {})} explicit
|
||||
* empty case) - see its javadoc.
|
||||
*/
|
||||
class TestComputedPathNoFetch {
|
||||
|
||||
@Test
|
||||
void mapTo_whenComputedGetterNeedsNoExtraFetch_explicitEmptyRequiresWorks() {
|
||||
Customer customer = new Customer("NoExtraFetchCo");
|
||||
customer.save();
|
||||
|
||||
ComputedPathNoFetchDto dto = DB.find(Customer.class)
|
||||
.where().idEq(customer.getId())
|
||||
.mapTo(ComputedPathNoFetchDto.class)
|
||||
.findOne();
|
||||
|
||||
assertThat(dto).isNotNull();
|
||||
assertThat(dto.getIdBadge()).isEqualTo("CUST-" + customer.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.DB;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.dtomapping.model.Contact;
|
||||
import org.tests.dtomapping.model.Customer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Regression test for {@link ComputedRefDto} - see its javadoc.
|
||||
*/
|
||||
class TestComputedRefPath {
|
||||
|
||||
@Test
|
||||
void mapTo_whenDtoRefTraversesComputedGetter_requiresFetchesItsDependency() {
|
||||
Customer customer = new Customer("ComputedRefCo");
|
||||
customer.save();
|
||||
Contact contact = new Contact("Alice", "Smith", customer);
|
||||
contact.save();
|
||||
|
||||
ComputedRefDto dto = DB.find(Customer.class)
|
||||
.where().idEq(customer.getId())
|
||||
.mapTo(ComputedRefDto.class)
|
||||
.findOne();
|
||||
|
||||
assertThat(dto).isNotNull();
|
||||
assertThat(dto.getPrimaryContactId()).isEqualTo(contact.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.DB;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.dtomapping.model.Address;
|
||||
import org.tests.dtomapping.model.Customer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Regression test for {@link FetchCollisionDto} - see its javadoc. Proves the fix for the
|
||||
* priority between a bare, full {@code requires()} fetch and a sibling property's narrowed
|
||||
* {@code @DtoPath} fetch of the exact same path: without it, this test fails with
|
||||
* {@code LazyInitialisationException} ("line1" unfetched) rather than the assertions below.
|
||||
*/
|
||||
class TestFetchCollisionPath {
|
||||
|
||||
@Test
|
||||
void mapTo_whenBareRequiresPathCollidesWithNarrowedSiblingFetch_fullFetchWins() {
|
||||
Address address = new Address("221B Baker Street", "London");
|
||||
address.save();
|
||||
Customer customer = new Customer("FetchCollisionCo");
|
||||
customer.setBillingAddress(address);
|
||||
customer.save();
|
||||
|
||||
FetchCollisionDto dto = DB.find(Customer.class)
|
||||
.where().idEq(customer.getId())
|
||||
.mapTo(FetchCollisionDto.class)
|
||||
.findOne();
|
||||
|
||||
assertThat(dto).isNotNull();
|
||||
assertThat(dto.getBillingCity()).isEqualTo("London");
|
||||
// only satisfiable if billingAddress was fully (not narrowly) fetched - line1 isn't part of
|
||||
// the sibling billingCity property's own narrowed fetch("billingAddress", "city")
|
||||
assertThat(dto.getBillingSummary()).isEqualTo("221B Baker Street, London");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.DB;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.dtomapping.model.Contact;
|
||||
import org.tests.dtomapping.model.Customer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Regression test for {@link PrimitiveNullPathDto} - see its javadoc.
|
||||
*/
|
||||
class TestPrimitiveNullPath {
|
||||
|
||||
@Test
|
||||
void mapTo_whenNullableRelationHopIsNull_defaultsPrimitiveToZero() {
|
||||
Customer customer = new Customer("NoBillingAddressCo");
|
||||
// deliberately leave billingAddress null
|
||||
customer.save();
|
||||
Contact contact = new Contact("Jane", "Doe", customer);
|
||||
contact.save();
|
||||
|
||||
PrimitiveNullPathDto dto = DB.find(Contact.class)
|
||||
.where().idEq(contact.getId())
|
||||
.mapTo(PrimitiveNullPathDto.class)
|
||||
.findOne();
|
||||
|
||||
assertThat(dto).isNotNull();
|
||||
assertThat(dto.getBillingAddressId()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapTo_whenNullableRelationHopIsNull_andFailOnNull_throws() {
|
||||
Customer customer = new Customer("NoBillingAddressCo2");
|
||||
customer.save();
|
||||
Contact contact = new Contact("John", "Doe", customer);
|
||||
contact.save();
|
||||
|
||||
assertThatThrownBy(() -> DB.find(Contact.class)
|
||||
.where().idEq(contact.getId())
|
||||
.mapTo(PrimitiveNullPathFailOnNullDto.class)
|
||||
.findOne())
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("customer.billingAddress.id");
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,15 @@
|
||||
@DtoMapping(source = Contact.class, target = ContactConversionDto.class)
|
||||
@DtoMapping(source = Contact.class, target = ContactMixinDto.class)
|
||||
@DtoMapping(source = Contact.class, target = ContactBuilderDto.class, builder = DtoMapping.Builder.ALWAYS)
|
||||
@DtoMapping(source = Contact.class, target = PrimitiveNullPathDto.class)
|
||||
@DtoMapping(source = Contact.class, target = PrimitiveNullPathFailOnNullDto.class)
|
||||
@DtoMapping(source = Customer.class, target = ComputedPathDto.class)
|
||||
@DtoMapping(source = Customer.class, target = ComputedPathNoFetchDto.class)
|
||||
@DtoMapping(source = Contact.class, target = ContactLeafDto.class)
|
||||
@DtoMapping(source = Customer.class, target = ComputedNestedDto.class)
|
||||
@DtoMapping(source = Customer.class, target = ComputedNestedListDto.class)
|
||||
@DtoMapping(source = Customer.class, target = ComputedRefDto.class)
|
||||
@DtoMapping(source = Customer.class, target = FetchCollisionDto.class)
|
||||
package org.tests.dtomapping;
|
||||
|
||||
import io.ebean.annotation.DtoMapping;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>tests</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>test-java16</artifactId>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>tests</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>test-kotlin</artifactId>
|
||||
@@ -40,7 +40,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -53,7 +53,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>18.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user