mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 11:17:36 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
805e309fb0 | ||
|
|
adb2e6a5d2 | ||
|
|
cbd67a6c22 | ||
|
|
893dc599cd |
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-net-postgis-types</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,7 +41,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -60,13 +60,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>composites</artifactId>
|
||||
|
||||
+1
-282
@@ -136,31 +136,8 @@ 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).
|
||||
|
||||
**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
|
||||
@@ -724,264 +701,6 @@ 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
|
||||
|
||||
|
||||
@@ -139,35 +139,6 @@ while keeping DTOs as plain, framework-unattached classes.
|
||||
querybean-generator codegen support (static/instance dispatch, constructor wiring deduplicated by converter
|
||||
type). Test coverage: `tests/test-dto-mapping` `TestDtoConvert`.*
|
||||
|
||||
- **Type-pair (package-level) custom scalar conversion**
|
||||
Motivated by real hand-written mapper code (`EboxMapper`, central-access): the same conversion repeats
|
||||
across many unrelated properties on one target - `DateUtils.toCalendar(...)` on ~9 fields,
|
||||
`parseEnum(EnumType.class, value)` on ~3 - under today's `@DtoConvert` every one of those properties must
|
||||
carry its own repeated annotation. MapStruct solves this by letting a conversion method be defined once
|
||||
(in the mapper or a `uses = {...}` helper) and auto-applying it to *every* property whose source/target
|
||||
types match that method's signature - no per-field wiring. Proposed: a package-level, repeatable
|
||||
`@DtoConverters({ConverterType.class, ...})` (sibling to `@DtoMapping` in `package-info.java`) - the
|
||||
generator indexes every public static/instance method on the referenced type(s) by `(paramType ->
|
||||
returnType)`, then for any property whose source getter type doesn't already match the target field type
|
||||
and which carries no explicit per-property `@DtoConvert`, looks up that type pair and wires it in
|
||||
automatically (same static-vs-instance/`DtoConverterManager` dispatch rules as `@DtoConvert` today). An
|
||||
explicit per-property `@DtoConvert` always overrides the type-level default. Deliberately no built-in
|
||||
conversions shipped by Ebean itself (no implicit `Enum.valueOf`/`.name()`) - the app still owns
|
||||
exception/null-handling semantics (e.g. `parseEnum`'s catch-and-null-on-bad-value), just declares it once
|
||||
instead of per-field.
|
||||
**Status: implemented.** `@DtoConverters(ConverterType.class, ...)` (a single non-repeatable annotation
|
||||
taking a `Class<?>[]`, `@Target({PACKAGE, MODULE})`) is registered once per package/module alongside
|
||||
`@DtoMapping`. The generator indexes every public, single-arg, non-void method on each referenced type by
|
||||
exact `(paramType -> returnType)`; any SCALAR property (plain or `@DtoPath`-renamed) with no explicit
|
||||
`@DtoConvert` and a source/target type mismatch is auto-wired to the matching method (a duplicate/ambiguous
|
||||
type pair across the registered types is a compile-time processor error). List-element-wise conversion and
|
||||
`@DtoRef` (FK-id) properties are out of scope. Test coverage:
|
||||
`tests/test-dto-mapping/.../TestDtoConverters.java` (`UuidConverters`/`UuidShortCodeConverter`,
|
||||
`ContactTypeConverterDto`) - covers same-name auto-dispatch, `@DtoPath`-renamed auto-dispatch, and explicit
|
||||
`@DtoConvert` overriding the registered default.
|
||||
*Inspiration: `EboxMapper` (central-access) hand-written pattern; MapStruct type-signature-matched
|
||||
conversion methods.*
|
||||
|
||||
- **`@DtoMixin` for DTOs that cannot be annotated directly**
|
||||
Some DTOs are generated (e.g. from an OpenAPI spec) and not editable/annotatable, so `@DtoPath`/
|
||||
`@DtoConvert`/`@DtoRef` cannot always be placed directly on the DTO. Introduce a `@DtoMixin(Target.class)`
|
||||
@@ -258,44 +229,6 @@ instead (see "Recipe: adding extra caller-supplied fields after mapping" in
|
||||
*Inspiration: `UserService`/`User` (central-access).*
|
||||
*Status: implemented.*
|
||||
|
||||
- **Setter-based (mutable JavaBean) target construction**
|
||||
Motivated by `EboxMapper` (central-access): its target types (`Ebox`, `MachineSummaryInfo`, from
|
||||
`nz.co.eroad.schema.eroadtypes`, JAXB/XSD-generated legacy SOAP shapes) are plain mutable JavaBeans - a
|
||||
public no-arg constructor plus a `void setXxx(...)` setter per property - neither a positional constructor
|
||||
match nor a RecordBuilder-style fluent builder (see section G above). The generator currently only
|
||||
recognizes those two construction strategies, so this common third shape (typical of JAXB/XSD-generated
|
||||
and many hand-written mutable POJOs) can't be targeted by `@DtoMapping` at all today. Proposed: detect a
|
||||
no-arg constructor plus a `void setXxx(propertyType)` setter per mapped property as a third construction
|
||||
strategy, generating `Target target = new Target(); target.setX(...); ...; return target;` (mirroring the
|
||||
existing `build = AUTO | ALWAYS | NEVER` override precedent from section G for explicit control over which
|
||||
strategy applies). Would also unblock the `mapToBuilder()`-style "populate ignored/derived properties after
|
||||
the generated mapping, before finishing construction" pattern for these targets (currently only available
|
||||
for builder-shaped targets) - relevant to `EboxMapper`'s `machineSummaryInfo` (a genuinely composite,
|
||||
multi-association derived value, out of reach of `@DtoConvert`/`@DtoPath` regardless of this gap, but a
|
||||
natural fit for the same "map base fields via codegen, then set the derived one by hand" pattern already
|
||||
used for `Fleet.assignedMachines`/`assignedDrivers`).
|
||||
*Inspiration: `EboxMapper` (central-access); JAXB/XSD-generated SOAP DTO shapes generally.*
|
||||
**Status: implemented.** `@DtoMapping(setter = AUTO | ALWAYS | NEVER)` mirrors `builder()`'s override
|
||||
precedent. Detection requires a public no-arg constructor plus a public `setXxx(...)` setter for every
|
||||
mapped property - either `void` or fluent-style (returning the target type itself, e.g. `public Target
|
||||
setXxx(...) { ...; return this; }`); the generated code always calls the setter as a bare statement and
|
||||
discards any return value, so either shape works identically. A builder, when selected, always takes
|
||||
priority over setter-based construction. Under the default `AUTO`, setter-based construction is only
|
||||
attempted when the target has no positional constructor matching the mapped properties (arity-based) and
|
||||
no builder was selected - existing positional-constructor and builder-shaped targets are entirely
|
||||
unaffected. `ALWAYS` requires the shape (codegen-time error otherwise); `NEVER` always uses a positional
|
||||
constructor. Generated shape: `Target target = new Target(); target.setX(...); ...; return target;` (a
|
||||
`computeIfAbsent(...)`-wrapped block-lambda variant when the target is nested elsewhere in the graph).
|
||||
Deliberately **no** `mapToBuilder(...)`-style post-construction accessor is generated for this strategy -
|
||||
the returned target is already the final, fully mutable instance (setters are required to be `public`), so
|
||||
a caller can already call e.g. `dto.setExternalRef(...)` directly on the mapped result, exactly the pattern
|
||||
`EboxMapper` already uses by hand; this is unlike the builder strategy, where the intermediate builder is
|
||||
otherwise unreachable after its one-shot `build()` call. Test coverage:
|
||||
`tests/test-dto-mapping/.../TestDtoSetterConstruction.java` (`ContactSetterDto`) - covers auto-detected
|
||||
setter-chain construction plus post-construction population of two `@DtoIgnore` properties (a plain scalar
|
||||
and a `List`) via their public setters; plus `ContactSetterFluentDto` - covers the fluent-setter-return-shape
|
||||
variant.
|
||||
|
||||
### H. Record entity sources
|
||||
|
||||
- **Record-style (bare/fluent) accessors on the source (entity) side**
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean api</name>
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
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,9 +1,16 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Query for performing native SQL queries that return DTO Bean's.
|
||||
@@ -36,7 +43,12 @@ import java.util.List;
|
||||
* }</pre>
|
||||
*/
|
||||
@NullMarked
|
||||
public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
|
||||
public interface DtoQuery<T> extends CancelableQuery {
|
||||
|
||||
/**
|
||||
* Execute the query returning a list.
|
||||
*/
|
||||
List<T> findList();
|
||||
|
||||
/**
|
||||
* Execute the query iterating a row at a time.
|
||||
@@ -47,6 +59,56 @@ public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
|
||||
*/
|
||||
QueryIterator<T> findIterate();
|
||||
|
||||
/**
|
||||
* Execute the query returning a Stream.
|
||||
* <p>
|
||||
* Note that the Stream holds resources related to the underlying
|
||||
* resultSet and potentially connection and MUST be closed. We should use
|
||||
* the Stream in a <em>try with resource block</em>.
|
||||
*/
|
||||
Stream<T> findStream();
|
||||
|
||||
/**
|
||||
* Execute the query iterating a row at a time.
|
||||
* <p>
|
||||
* This streaming type query is useful for large query execution as only 1 row needs to be held in memory.
|
||||
* </p>
|
||||
*/
|
||||
void findEach(Consumer<T> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query iterating the results and batching them for the consumer.
|
||||
* <p>
|
||||
* This runs like findEach streaming results from the database but just collects the results
|
||||
* into batches to pass to the consumer.
|
||||
*
|
||||
* @param batch The number of dto beans to collect before given them to the consumer
|
||||
* @param consumer The consumer to process the batch of DTO beans
|
||||
*/
|
||||
void findEach(int batch, Consumer<List<T>> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query iterating a row at a time with the ability to stop consuming part way through.
|
||||
* <p>
|
||||
* Returning false after processing a row stops the iteration through the query results.
|
||||
* </p>
|
||||
* <p>
|
||||
* This streaming type query is useful for large query execution as only 1 row needs to be held in memory.
|
||||
* </p>
|
||||
*/
|
||||
void findEachWhile(Predicate<T> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean.
|
||||
*/
|
||||
@Nullable
|
||||
T findOne();
|
||||
|
||||
/**
|
||||
* Execute the query returning an optional bean.
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Bind all the parameters using index positions.
|
||||
* <p>
|
||||
@@ -152,6 +214,38 @@ public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
|
||||
*/
|
||||
DtoQuery<T> setBufferFetchSizeHint(int bufferFetchSizeHint);
|
||||
|
||||
/**
|
||||
* Use the explicit transaction to execute the query.
|
||||
*/
|
||||
DtoQuery<T> usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
DtoQuery<T> usingConnection(Connection connection);
|
||||
|
||||
/**
|
||||
* Ensure that the master DataSource is used if there is a read only data source
|
||||
* being used (that is using a read replica database potentially with replication lag).
|
||||
* <p>
|
||||
* When the database is configured with a read-only DataSource via
|
||||
* say {@link io.ebean.DatabaseBuilder#readOnlyDataSource(DataSource)} then
|
||||
* by default when a query is run without an active transaction, it uses the read-only data
|
||||
* source. We use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* against the master data source.
|
||||
*/
|
||||
default DtoQuery<T> usingMaster() {
|
||||
return usingMaster(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see #usingMaster()
|
||||
*/
|
||||
DtoQuery<T> usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Return a PagedList for this query using firstRow and maxRows.
|
||||
* <p>
|
||||
@@ -187,7 +281,6 @@ public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
|
||||
*
|
||||
* @return The PagedList
|
||||
*/
|
||||
@Override
|
||||
PagedList<T> findPagedList();
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import java.sql.Timestamp;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* List of Expressions that make up a where or having clause.
|
||||
@@ -422,26 +421,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing a {@link jakarta.persistence.EntityNotFoundException}
|
||||
* if there is no matching bean.
|
||||
*
|
||||
* @see Query#findOneOrThrow()
|
||||
*/
|
||||
default T findOneOrThrow() {
|
||||
return query().findOneOrThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing the exception produced by the
|
||||
* given supplier if there is no matching bean.
|
||||
*
|
||||
* @see Query#findOneOrThrow(Supplier)
|
||||
*/
|
||||
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
||||
return query().findOneOrThrow(exceptionSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute find row count query in a background thread.
|
||||
* <p>
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Common find operations shared by the query types that can execute and return
|
||||
* results - {@link SqlQuery}, {@link DtoQuery}, {@link MappedQuery} and {@link QueryBuilder}.
|
||||
*
|
||||
* @param <SELF> The query type (used for method chaining)
|
||||
* @param <T> The type of the result
|
||||
*/
|
||||
@NullMarked
|
||||
public interface FindableQuery<SELF extends FindableQuery<SELF, T>, T> extends CancelableQuery {
|
||||
|
||||
/**
|
||||
* Execute the query returning the list of results.
|
||||
*/
|
||||
List<T> findList();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single result, or {@code null} if there is no matching row.
|
||||
* <p>
|
||||
* If more than 1 row is found for this query then a PersistenceException is thrown.
|
||||
*/
|
||||
@Nullable
|
||||
T findOne();
|
||||
|
||||
/**
|
||||
* Execute the query returning an optional result.
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single result or throwing a
|
||||
* {@link jakarta.persistence.EntityNotFoundException} if there is no matching row.
|
||||
*/
|
||||
default T findOneOrThrow() {
|
||||
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException("Not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query returning a single result or throwing the exception produced
|
||||
* by the given supplier if there is no matching row.
|
||||
*/
|
||||
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
||||
return findOneOrEmpty().orElseThrow(exceptionSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query using the given transaction.
|
||||
*/
|
||||
SELF usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
SELF usingConnection(Connection connection);
|
||||
|
||||
/**
|
||||
* Ensure that the master DataSource is used if there is a read only data source
|
||||
* being used (that is using a read replica database potentially with replication lag).
|
||||
* <p>
|
||||
* When the database is configured with a read-only DataSource via
|
||||
* say {@link DatabaseBuilder#readOnlyDataSource(DataSource)} then
|
||||
* by default when a query is run without an active transaction, it uses the read-only data
|
||||
* source. We use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* against the master data source.
|
||||
*/
|
||||
default SELF usingMaster() {
|
||||
return usingMaster(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see #usingMaster()
|
||||
*/
|
||||
SELF usingMaster(boolean useMaster);
|
||||
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
@@ -21,12 +22,11 @@ import java.util.stream.Stream;
|
||||
* @param <D> the target DTO type
|
||||
*/
|
||||
@NullMarked
|
||||
public interface MappedQuery<D> extends StreamableQuery<MappedQuery<D>, D> {
|
||||
public interface MappedQuery<D> {
|
||||
|
||||
/**
|
||||
* Execute the query returning the mapped DTO list.
|
||||
*/
|
||||
@Override
|
||||
List<D> findList();
|
||||
|
||||
/**
|
||||
@@ -38,7 +38,6 @@ public interface MappedQuery<D> extends StreamableQuery<MappedQuery<D>, D> {
|
||||
* ({@link PagedList#getTotalCount()}, {@link PagedList#hasNext()}, etc.) reflects the
|
||||
* underlying entity query and is unaffected by the DTO mapping.
|
||||
*/
|
||||
@Override
|
||||
PagedList<D> findPagedList();
|
||||
|
||||
/**
|
||||
@@ -61,51 +60,33 @@ public interface MappedQuery<D> extends StreamableQuery<MappedQuery<D>, D> {
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@Override
|
||||
Stream<D> findStream();
|
||||
|
||||
/**
|
||||
* Execute the query processing the mapped DTOs one at a time.
|
||||
* <p>
|
||||
* Mirrors {@link QueryBuilder#findEach(Consumer)} - the underlying entity graph query is
|
||||
* streamed one entity at a time and each entity is mapped to its target DTO lazily as it is
|
||||
* consumed, sharing one {@link DtoMapContext} across the whole callback so that repeated
|
||||
* references to the same source entity still de-duplicate to the same DTO instance.
|
||||
* <p>
|
||||
* This method is appropriate to process very large query results as the mapped DTOs are
|
||||
* consumed one at a time and do not need to be held in memory (unlike {@link #findList()}).
|
||||
*
|
||||
* @param consumer the consumer used to process the mapped DTOs.
|
||||
* Execute the query returning a single mapped DTO, or {@code null} if there is no matching row.
|
||||
*/
|
||||
@Override
|
||||
void findEach(Consumer<D> consumer);
|
||||
/**
|
||||
* Execute findEach streaming query batching the mapped DTOs for consuming.
|
||||
* <p>
|
||||
* Mirrors {@link QueryBuilder#findEach(int, Consumer)} - typically used when we want to do
|
||||
* further processing on the mapped DTOs in batch form, for example 100 at a time. Each batch
|
||||
* shares one {@link DtoMapContext} with the rest of the query so that repeated references to
|
||||
* the same source entity still de-duplicate to the same DTO instance.
|
||||
*
|
||||
* @param batch The number of mapped DTOs processed in the batch
|
||||
* @param consumer Process the batch of mapped DTOs
|
||||
*/
|
||||
@Override
|
||||
void findEach(int batch, Consumer<List<D>> consumer);
|
||||
@Nullable
|
||||
D findOne();
|
||||
|
||||
/**
|
||||
* Execute the query using callbacks to process the resulting mapped DTOs one at a time,
|
||||
* with the ability to stop processing part way through.
|
||||
* <p>
|
||||
* Mirrors {@link QueryBuilder#findEachWhile(Predicate)} - returning {@code false} after
|
||||
* processing a DTO stops the iteration through the query results. Sharing one
|
||||
* {@link DtoMapContext} across the whole callback so that repeated references to the same
|
||||
* source entity still de-duplicate to the same DTO instance.
|
||||
*
|
||||
* @param consumer the consumer used to process the mapped DTOs, returning {@code false} to
|
||||
* stop processing.
|
||||
* Execute the query returning an optional mapped DTO.
|
||||
*/
|
||||
@Override
|
||||
void findEachWhile(Predicate<D> consumer);
|
||||
Optional<D> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*/
|
||||
MappedQuery<D> usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Use the explicit transaction to execute the query.
|
||||
*/
|
||||
MappedQuery<D> usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
MappedQuery<D> usingConnection(Connection connection);
|
||||
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ import org.jspecify.annotations.Nullable;
|
||||
* @param <T> the type of Entity bean this query will fetch.
|
||||
*/
|
||||
@NullMarked
|
||||
public interface Query<T> extends QueryBuilder<Query<T>, T> {
|
||||
public interface Query<T> extends CancelableQuery, QueryBuilder<Query<T>, T> {
|
||||
|
||||
/**
|
||||
* The lock type (strength) to use with query FOR UPDATE row locking.
|
||||
|
||||
@@ -2,7 +2,8 @@ package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -10,6 +11,8 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Build and execute an ORM query.
|
||||
@@ -17,7 +20,7 @@ import java.util.function.Consumer;
|
||||
* @param <SELF> The type of the builder
|
||||
* @param <T> The entity bean type
|
||||
*/
|
||||
public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends QueryBuilderProjection<SELF, T>, StreamableQuery<SELF, T> {
|
||||
public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends QueryBuilderProjection<SELF, T> {
|
||||
|
||||
/**
|
||||
* Set root table alias.
|
||||
@@ -143,16 +146,48 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF copy();
|
||||
|
||||
/**
|
||||
* Execute the query using the given transaction.
|
||||
*/
|
||||
SELF usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute this query using immutable bean cache values for matching bean types.
|
||||
*/
|
||||
SELF using(ImmutableBeanCache<?> beanCache);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
SELF usingConnection(Connection connection);
|
||||
|
||||
/**
|
||||
* Execute the query using the given database.
|
||||
*/
|
||||
SELF usingDatabase(Database database);
|
||||
|
||||
/**
|
||||
* Ensure that the master DataSource is used if there is a read only data source
|
||||
* being used (that is using a read replica database potentially with replication lag).
|
||||
* <p>
|
||||
* When the database is configured with a read-only DataSource via
|
||||
* say {@link io.ebean.config.DatabaseConfig#setReadOnlyDataSource(DataSource)} then
|
||||
* by default when a query is run without an active transaction, it uses the read-only data
|
||||
* source. We we use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* against the master data source.
|
||||
*/
|
||||
default SELF usingMaster() {
|
||||
return usingMaster(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see #usingMaster()
|
||||
*/
|
||||
SELF usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Set the base table to use for this query.
|
||||
* <p>
|
||||
@@ -644,24 +679,88 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
boolean exists();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing a {@link jakarta.persistence.EntityNotFoundException}
|
||||
* if there is no matching bean.
|
||||
* Execute the query returning either a single bean or null (if no matching
|
||||
* bean is found).
|
||||
* <p>
|
||||
* If more than 1 row is found for this query then a PersistenceException is
|
||||
* thrown.
|
||||
* <p>
|
||||
* This is useful when your predicates dictate that your query should only
|
||||
* return 0 or 1 results.
|
||||
* <p>
|
||||
* This is a convenience alternative to:
|
||||
* <pre>{@code
|
||||
* query.findOneOrEmpty()
|
||||
* .orElseThrow(() -> new EntityNotFoundException(...));
|
||||
*
|
||||
* // assuming the sku of products is unique...
|
||||
* Product product =
|
||||
* new QProduct()
|
||||
* .sku.equalTo("aa113")
|
||||
* .findOne();
|
||||
* ...
|
||||
* }</pre>
|
||||
* <p>
|
||||
* The exception message is a best effort - it uses the id when this is effectively a
|
||||
* find-by-id query, or the single equality predicate when the query is filtered by what
|
||||
* looks like a natural/unique key, otherwise a generic "not found" message.
|
||||
* It is also useful with finding objects by their id when you want to specify
|
||||
* further join information to optimise the query.
|
||||
* <p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // Fetch order 42 and additionally fetch join its order details...
|
||||
* Order order =
|
||||
* new QOrder()
|
||||
* .fetch("details") // eagerly load the order details
|
||||
* .id.equalTo(42)
|
||||
* .findOne();
|
||||
*
|
||||
* // the order details were eagerly loaded
|
||||
* List<OrderDetail> details = order.getDetails();
|
||||
* ...
|
||||
* }</pre>
|
||||
*/
|
||||
@Override
|
||||
default T findOneOrThrow() {
|
||||
return findOneOrEmpty().orElseThrow(() ->
|
||||
new EntityNotFoundException(getBeanType().getSimpleName() + " not found"));
|
||||
}
|
||||
@Nullable
|
||||
T findOne();
|
||||
|
||||
/**
|
||||
* Execute the query returning an optional bean.
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Execute the query returning the list of objects.
|
||||
* <p>
|
||||
* This query will execute against the EbeanServer that was used to create it.
|
||||
* <p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* List<Customer> customers =
|
||||
* new QCustomer()
|
||||
* .name.ilike("rob%")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @see Query#findList()
|
||||
*/
|
||||
List<T> findList();
|
||||
|
||||
/**
|
||||
* Execute the query returning the result as a Stream.
|
||||
* <p>
|
||||
* Note that this can support very large queries iterating
|
||||
* any number of results. To do so internally it can use
|
||||
* multiple persistence contexts.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // use try with resources to ensure Stream is closed
|
||||
*
|
||||
* try (Stream<Customer> stream = query.findStream()) {
|
||||
* stream
|
||||
* .map(...)
|
||||
* .collect(...);
|
||||
* }
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
Stream<T> findStream();
|
||||
|
||||
/**
|
||||
* Execute the query returning the set of objects.
|
||||
@@ -806,6 +905,84 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
<A> Set<A> findSingleAttributeSet();
|
||||
|
||||
/**
|
||||
* Execute the query processing the beans one at a time.
|
||||
* <p>
|
||||
* This method is appropriate to process very large query results as the
|
||||
* beans are consumed one at a time and do not need to be held in memory
|
||||
* (unlike #findList #findSet etc)
|
||||
* <p>
|
||||
* Note that internally Ebean can inform the JDBC driver that it is expecting larger
|
||||
* resultSet and specifically for MySQL this hint is required to stop it's JDBC driver
|
||||
* from buffering the entire resultSet. As such, for smaller resultSets findList() is
|
||||
* generally preferable.
|
||||
* <p>
|
||||
* Compared with #findEachWhile this will always process all the beans where as
|
||||
* #findEachWhile provides a way to stop processing the query result early before
|
||||
* all the beans have been read.
|
||||
* <p>
|
||||
* This method is functionally equivalent to findIterate() but instead of using an
|
||||
* iterator uses the Consumer interface which is better suited to use with closures.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* new QCustomer()
|
||||
* .status.equalTo(Status.NEW)
|
||||
* .orderBy().id.asc()
|
||||
* .findEach((Customer customer) -> {
|
||||
*
|
||||
* // do something with customer
|
||||
* System.out.println("-- visit " + customer);
|
||||
* });
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param consumer the consumer used to process the queried beans.
|
||||
*/
|
||||
void findEach(Consumer<T> consumer);
|
||||
|
||||
/**
|
||||
* Execute findEach streaming query batching the results for consuming.
|
||||
* <p>
|
||||
* This query execution will stream the results and is suited to consuming
|
||||
* large numbers of results from the database.
|
||||
* <p>
|
||||
* Typically, we use this batch consumer when we want to do further processing on
|
||||
* the beans and want to do that processing in batch form, for example - 100 at
|
||||
* a time.
|
||||
*
|
||||
* @param batch The number of beans processed in the batch
|
||||
* @param consumer Process the batch of beans
|
||||
*/
|
||||
void findEach(int batch, Consumer<List<T>> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query using callbacks to a visitor to process the resulting
|
||||
* beans one at a time.
|
||||
* <p>
|
||||
* This method is functionally equivalent to findIterate() but instead of using an
|
||||
* iterator uses the Predicate interface which is better suited to use with closures.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* new QCustomer()
|
||||
* .status.equalTo(Status.NEW)
|
||||
* .orderBy().id.asc()
|
||||
* .findEachWhile((Customer customer) -> {
|
||||
*
|
||||
* // do something with customer
|
||||
* System.out.println("-- visit " + customer);
|
||||
*
|
||||
* // return true to continue processing or false to stop
|
||||
* return (customer.getId() < 40);
|
||||
* });
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param consumer the consumer used to process the queried beans.
|
||||
*/
|
||||
void findEachWhile(Predicate<T> consumer);
|
||||
|
||||
/**
|
||||
* Return versions of a @History entity bean.
|
||||
* <p>
|
||||
@@ -871,4 +1048,33 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
<K> FutureMap<K,T> findFutureMap();
|
||||
|
||||
/**
|
||||
* Return a PagedList for this query using firstRow and maxRows.
|
||||
* <p>
|
||||
* The benefit of using this over findList() is that it provides functionality to get the
|
||||
* total row count etc.
|
||||
* <p>
|
||||
* If maxRows is not set on the query prior to calling findPagedList() then a
|
||||
* PersistenceException is thrown.
|
||||
* <p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* PagedList<Order> pagedList =
|
||||
* new QOrder()
|
||||
* .setFirstRow(50)
|
||||
* .setMaxRows(20)
|
||||
* .findPagedList();
|
||||
*
|
||||
* // fetch the total row count in the background
|
||||
* pagedList.loadRowCount();
|
||||
*
|
||||
* List<Order> orders = pagedList.getList();
|
||||
* int totalRowCount = pagedList.getTotalRowCount();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @return The PagedList
|
||||
*/
|
||||
PagedList<T> findPagedList();
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.ebean;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.Serializable;
|
||||
import java.sql.Connection;
|
||||
import java.util.Collection;
|
||||
@@ -40,7 +41,44 @@ import java.util.function.Predicate;
|
||||
* }</pre>
|
||||
*/
|
||||
@NullMarked
|
||||
public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow> {
|
||||
public interface SqlQuery extends Serializable, CancelableQuery {
|
||||
|
||||
/**
|
||||
* Execute the query using the given transaction.
|
||||
*/
|
||||
SqlQuery usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
SqlQuery usingConnection(Connection connection);
|
||||
|
||||
/**
|
||||
* Ensure that the master DataSource is used if there is a read only data source
|
||||
* being used (that is using a read replica database potentially with replication lag).
|
||||
* <p>
|
||||
* When the database is configured with a read-only DataSource via
|
||||
* say {@link io.ebean.DatabaseBuilder#readOnlyDataSource(DataSource)}then
|
||||
* by default when a query is run without an active transaction, it uses the read-only data
|
||||
* source. We use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* against the master data source.
|
||||
*/
|
||||
default SqlQuery usingMaster() {
|
||||
return usingMaster(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see #usingMaster()
|
||||
*/
|
||||
SqlQuery usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Execute the query returning a list.
|
||||
*/
|
||||
List<SqlRow> findList();
|
||||
|
||||
/**
|
||||
* Execute the SqlQuery iterating a row at a time.
|
||||
@@ -61,6 +99,16 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
|
||||
*/
|
||||
void findEachWhile(Predicate<SqlRow> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query returning a single row or null.
|
||||
* <p>
|
||||
* If this query finds 2 or more rows then it will throw a
|
||||
* PersistenceException.
|
||||
* </p>
|
||||
*/
|
||||
@Nullable
|
||||
SqlRow findOne();
|
||||
|
||||
/**
|
||||
* Execute the query reading each row from ResultSet using the RowConsumer.
|
||||
* <p>
|
||||
@@ -91,6 +139,11 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
|
||||
*/
|
||||
void findEachRow(RowConsumer consumer);
|
||||
|
||||
/**
|
||||
* Execute the query returning an optional row.
|
||||
*/
|
||||
Optional<SqlRow> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Set one of more positioned parameters.
|
||||
* <p>
|
||||
@@ -312,7 +365,7 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
|
||||
*
|
||||
* @param <T> The type of the scalar values
|
||||
*/
|
||||
interface TypeQuery<T> extends FindableQuery<TypeQuery<T>, T> {
|
||||
interface TypeQuery<T> {
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
@@ -320,38 +373,27 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
|
||||
*
|
||||
* @see SqlQuery#usingMaster(boolean)
|
||||
*/
|
||||
@Override
|
||||
TypeQuery<T> usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Execute the query using the given transaction.
|
||||
*/
|
||||
@Override
|
||||
TypeQuery<T> usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
@Override
|
||||
TypeQuery<T> usingConnection(Connection connection);
|
||||
|
||||
/**
|
||||
* Return the single value.
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
T findOne();
|
||||
|
||||
/**
|
||||
* Return the single value that is optional.
|
||||
*/
|
||||
@Override
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Return the list of values.
|
||||
*/
|
||||
@Override
|
||||
List<T> findList();
|
||||
|
||||
/**
|
||||
|
||||
@@ -158,15 +158,6 @@ public interface SqlUpdate {
|
||||
*/
|
||||
int executeNow();
|
||||
|
||||
/**
|
||||
* Set an explicit transaction to use to execute this statement.
|
||||
* <p>
|
||||
* When not set, {@link #execute()} and {@link #executeNow()} use whatever transaction
|
||||
* is currently active on the thread (or auto-commit if none is active) - consistent
|
||||
* with {@link Database#execute(SqlUpdate, Transaction)}.
|
||||
*/
|
||||
SqlUpdate usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute when addBatch() has been used to batch multiple bind executions.
|
||||
*
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A {@link FindableQuery} that additionally supports streaming results and paging.
|
||||
*
|
||||
* @param <SELF> The query type (used for method chaining)
|
||||
* @param <T> The type of the result
|
||||
*/
|
||||
@NullMarked
|
||||
public interface StreamableQuery<SELF extends StreamableQuery<SELF, T>, T> extends FindableQuery<SELF, T> {
|
||||
|
||||
/**
|
||||
* Execute the query returning the result as a Stream.
|
||||
* <p>
|
||||
* Note that this can support very large queries iterating any number of results.
|
||||
* To do so internally it can use multiple persistence contexts.
|
||||
* <p>
|
||||
* Note that the Stream holds resources related to the underlying resultSet and
|
||||
* potentially connection and MUST be closed. We should use the Stream in a
|
||||
* <em>try with resource block</em>.
|
||||
* <pre>{@code
|
||||
*
|
||||
* // use try with resources to ensure Stream is closed
|
||||
*
|
||||
* try (Stream<T> stream = query.findStream()) {
|
||||
* stream
|
||||
* .map(...)
|
||||
* .collect(...);
|
||||
* }
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
Stream<T> findStream();
|
||||
|
||||
/**
|
||||
* Return a PagedList for this query using firstRow and maxRows.
|
||||
* <p>
|
||||
* The benefit of using this over findList() is that it provides functionality to get the
|
||||
* total row count etc.
|
||||
* <p>
|
||||
* If maxRows is not set on the query prior to calling findPagedList() then a
|
||||
* PersistenceException is thrown.
|
||||
*
|
||||
* @return The PagedList
|
||||
*/
|
||||
PagedList<T> findPagedList();
|
||||
|
||||
/**
|
||||
* Execute the query processing the results one at a time.
|
||||
* <p>
|
||||
* This method is appropriate to process very large query results as the results are
|
||||
* consumed one at a time and do not need to be held in memory (unlike {@link #findList()}).
|
||||
* <p>
|
||||
* Note that internally Ebean can inform the JDBC driver that it is expecting a larger
|
||||
* resultSet and specifically for MySQL this hint is required to stop its JDBC driver
|
||||
* from buffering the entire resultSet. As such, for smaller resultSets findList() is
|
||||
* generally preferable.
|
||||
* <p>
|
||||
* Compared with {@link #findEachWhile(Predicate)} this will always process all the results
|
||||
* whereas findEachWhile() provides a way to stop processing the query result early before
|
||||
* all the results have been read.
|
||||
*
|
||||
* @param consumer the consumer used to process the queried results.
|
||||
*/
|
||||
void findEach(Consumer<T> consumer);
|
||||
|
||||
/**
|
||||
* Execute findEach streaming query batching the results for consuming.
|
||||
* <p>
|
||||
* This query execution will stream the results and is suited to consuming
|
||||
* large numbers of results from the database.
|
||||
* <p>
|
||||
* Typically, we use this batch consumer when we want to do further processing on
|
||||
* the results and want to do that processing in batch form, for example - 100 at
|
||||
* a time.
|
||||
*
|
||||
* @param batch The number of results processed in the batch
|
||||
* @param consumer Process the batch of results
|
||||
*/
|
||||
void findEach(int batch, Consumer<List<T>> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query using callbacks to process the resulting results one at a time,
|
||||
* with the ability to stop processing part way through.
|
||||
* <p>
|
||||
* Returning {@code false} after processing a result stops the iteration through the
|
||||
* query results.
|
||||
*
|
||||
* @param consumer the consumer used to process the queried results, returning
|
||||
* {@code false} to stop processing.
|
||||
*/
|
||||
void findEachWhile(Predicate<T> consumer);
|
||||
|
||||
}
|
||||
@@ -84,15 +84,6 @@ public interface Update<T> {
|
||||
*/
|
||||
int execute();
|
||||
|
||||
/**
|
||||
* Set an explicit transaction to use to execute this statement.
|
||||
* <p>
|
||||
* When not set, {@link #execute()} uses whatever transaction is currently active on
|
||||
* the thread (or auto-commit if none is active) - consistent with
|
||||
* {@link Database#execute(Update, Transaction)}.
|
||||
*/
|
||||
Update<T> usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Set an ordered bind parameter.
|
||||
* <p>
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-bench</artifactId>
|
||||
|
||||
+28
-28
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean bom</name>
|
||||
@@ -89,25 +89,25 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -125,13 +125,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -155,37 +155,37 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>kotlin-querybean-generator</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-redis</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-spring-txn</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- platforms -->
|
||||
@@ -193,91 +193,91 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-clickhouse</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-db2</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-h2</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-hana</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mariadb</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mysql</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-nuodb</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-oracle</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgres</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlite</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlserver</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.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.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
+7
-7
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-core</artifactId>
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-json</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -52,7 +52,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -157,21 +157,21 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -334,12 +334,6 @@ public interface SpiEbeanServer extends SpiServer, BeanCollectionLoader {
|
||||
*/
|
||||
int executeNow(SpiSqlUpdate sqlUpdate);
|
||||
|
||||
/**
|
||||
* Execute the sql update regardless of transaction batch mode using the given
|
||||
* explicit transaction (or the current ambient transaction when null).
|
||||
*/
|
||||
int executeNow(SpiSqlUpdate sqlUpdate, @Nullable Transaction transaction);
|
||||
|
||||
/**
|
||||
* Create a query bind capture for the given query plan.
|
||||
*/
|
||||
|
||||
@@ -1938,12 +1938,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
|
||||
@Override
|
||||
public int executeNow(SpiSqlUpdate sqlUpdate) {
|
||||
return executeNow(sqlUpdate, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int executeNow(SpiSqlUpdate sqlUpdate, @Nullable Transaction transaction) {
|
||||
return persister.executeSqlUpdateNow(sqlUpdate, transaction);
|
||||
return persister.executeSqlUpdateNow(sqlUpdate, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,7 +2,6 @@ package io.ebeaninternal.server.core;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.SqlUpdate;
|
||||
import io.ebean.Transaction;
|
||||
import io.ebean.Update;
|
||||
import io.ebeaninternal.api.BindParams;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
@@ -141,7 +140,7 @@ public final class DefaultSqlUpdate implements Serializable, SpiSqlUpdate {
|
||||
server.executeBatch(this, transaction);
|
||||
return -1;
|
||||
}
|
||||
return server.execute(this, transaction);
|
||||
return server.execute(this);
|
||||
} else {
|
||||
// Hopefully this doesn't catch anyone out...
|
||||
return DB.getDefault().execute(this);
|
||||
@@ -151,18 +150,12 @@ public final class DefaultSqlUpdate implements Serializable, SpiSqlUpdate {
|
||||
@Override
|
||||
public int executeNow() {
|
||||
if (server != null) {
|
||||
return server.executeNow(this, transaction);
|
||||
return server.executeNow(this);
|
||||
} else {
|
||||
throw new IllegalStateException("server is null?");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlUpdate usingTransaction(Transaction transaction) {
|
||||
this.transaction = (SpiTransaction) transaction;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] executeBatch() {
|
||||
if (server == null) {
|
||||
|
||||
-18
@@ -1335,24 +1335,6 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a "propertyName: value" description when this expression list is a single
|
||||
* simple equality predicate (a candidate natural/unique key), otherwise null.
|
||||
* <p>
|
||||
* Used to build a decent default message for {@code findOneOrThrow()} when the query
|
||||
* isn't a simple find-by-id.
|
||||
*/
|
||||
@Nullable
|
||||
public String singleEqDescription() {
|
||||
if (list.size() == 1 && list.get(0) instanceof SimpleExpression) {
|
||||
SimpleExpression simple = (SimpleExpression) list.get(0);
|
||||
if (simple.isOpEquals()) {
|
||||
return simple.getPropName() + ": " + simple.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> clear() {
|
||||
list.clear();
|
||||
|
||||
+6
-4
@@ -4,9 +4,11 @@ import io.ebean.InsertOptions;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.BeanProperty;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Postgres specific generation of insert on conflict.
|
||||
@@ -16,14 +18,12 @@ 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
|
||||
@@ -49,7 +49,10 @@ final class InsertMetaOptionsPostgres implements InsertMetaOptions {
|
||||
meta.sql(request, !withId, baseTable, false);
|
||||
request.append(" on conflict ");
|
||||
|
||||
List<String> uniqueColumns = InsertMetaOptionsSupport.uniqueColumns(desc, withId);
|
||||
List<String> uniqueColumns = desc.uniqueProps().stream()
|
||||
.flatMap(Arrays::stream)
|
||||
.map(BeanProperty::dbColumn)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
String constraintName = options.constraint();
|
||||
if (constraintName != null) {
|
||||
@@ -81,7 +84,6 @@ 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()) {
|
||||
|
||||
+6
-4
@@ -4,9 +4,11 @@ import io.ebean.InsertOptions;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.BeanProperty;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* SQLite specific generation of insert on conflict.
|
||||
@@ -19,14 +21,12 @@ 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
|
||||
@@ -55,7 +55,10 @@ final class InsertMetaOptionsSqlite implements InsertMetaOptions {
|
||||
meta.sql(request, !withId, baseTable, false);
|
||||
request.append(" on conflict (");
|
||||
|
||||
List<String> uniqueColumns = InsertMetaOptionsSupport.uniqueColumns(desc, withId);
|
||||
List<String> uniqueColumns = desc.uniqueProps().stream()
|
||||
.flatMap(Arrays::stream)
|
||||
.map(BeanProperty::dbColumn)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
String cols = options.uniqueColumns();
|
||||
if (cols != null) {
|
||||
@@ -82,7 +85,6 @@ 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()) {
|
||||
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
package io.ebeaninternal.server.persist.dml;
|
||||
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.BeanProperty;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocOne;
|
||||
import io.ebeaninternal.server.deploy.generatedproperty.GeneratedProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Shared support for the platform specific insert on conflict do update generation.
|
||||
*/
|
||||
final class InsertMetaOptionsSupport {
|
||||
|
||||
private InsertMetaOptionsSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the columns to use as the on conflict target.
|
||||
* <p>
|
||||
* Uses columns explicitly mapped as unique via {@code @Column(unique=true)} or
|
||||
* {@code @Index(unique=true)}. When there are none of those, falls back to the primary
|
||||
* key column(s) given the primary key constraint is inherently unique. This fallback only
|
||||
* applies when the id value is included in the insert (withId) as an insert relying on a
|
||||
* database generated id (identity/sequence) is never going to naturally conflict on that id.
|
||||
*/
|
||||
static List<String> uniqueColumns(BeanDescriptor<?> desc, boolean withId) {
|
||||
List<String> uniqueColumns = desc.uniqueProps().stream()
|
||||
.flatMap(Arrays::stream)
|
||||
.map(BeanProperty::dbColumn)
|
||||
.collect(Collectors.toList());
|
||||
if (uniqueColumns.isEmpty() && withId) {
|
||||
uniqueColumns = idColumns(desc);
|
||||
}
|
||||
return uniqueColumns;
|
||||
}
|
||||
|
||||
private static List<String> idColumns(BeanDescriptor<?> desc) {
|
||||
BeanProperty idProperty = desc.idProperty();
|
||||
if (idProperty == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (idProperty.isEmbedded() && idProperty instanceof BeanPropertyAssocOne) {
|
||||
List<String> columns = new ArrayList<>();
|
||||
for (BeanProperty embedded : ((BeanPropertyAssocOne<?>) idProperty).properties()) {
|
||||
columns.add(embedded.dbColumn());
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
return List.of(idProperty.dbColumn());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import io.ebeaninternal.api.SpiQuery;
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
@@ -92,11 +90,6 @@ public final class DefaultMappedQuery<T, D> implements MappedQuery<D> {
|
||||
return Optional.ofNullable(findOne());
|
||||
}
|
||||
|
||||
@Override
|
||||
public D findOneOrThrow() {
|
||||
return mapper().map(query.findOneOrThrow());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PagedList<D> findPagedList() {
|
||||
DtoMapper<T, D> m = mapper();
|
||||
@@ -110,27 +103,6 @@ public final class DefaultMappedQuery<T, D> implements MappedQuery<D> {
|
||||
return query.findStream().map(source -> m.map(source, context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findEach(Consumer<D> consumer) {
|
||||
DtoMapper<T, D> m = mapper();
|
||||
DtoMapContext context = new DtoMapContext();
|
||||
query.findEach(source -> consumer.accept(m.map(source, context)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findEach(int batch, Consumer<List<D>> consumer) {
|
||||
DtoMapper<T, D> m = mapper();
|
||||
DtoMapContext context = new DtoMapContext();
|
||||
query.findEach(batch, sourceBatch -> consumer.accept(m.mapList(sourceBatch, context)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findEachWhile(Predicate<D> consumer) {
|
||||
DtoMapper<T, D> m = mapper();
|
||||
DtoMapContext context = new DtoMapContext();
|
||||
query.findEachWhile(source -> consumer.test(m.map(source, context)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MappedQuery<D> usingMaster(boolean useMaster) {
|
||||
query.usingMaster(useMaster);
|
||||
@@ -148,9 +120,4 @@ public final class DefaultMappedQuery<T, D> implements MappedQuery<D> {
|
||||
query.usingConnection(connection);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
query.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import io.ebeaninternal.server.query.NativeSqlQueryPlanKey;
|
||||
import io.ebeaninternal.server.rawsql.SpiRawSql;
|
||||
import io.ebeaninternal.server.transaction.ExternalJdbcTransaction;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Timestamp;
|
||||
@@ -1657,30 +1656,6 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
|
||||
return server.findOneOrEmpty(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final T findOneOrThrow() {
|
||||
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException(notFoundMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a decent default "not found" message using the id (if this is effectively a
|
||||
* find-by-id query) or, failing that, a single simple equality predicate (a likely
|
||||
* natural/unique key). Falls back to a generic message for anything more complex.
|
||||
*/
|
||||
private String notFoundMessage() {
|
||||
String type = beanDescriptor.type().getSimpleName();
|
||||
if (isFindById()) {
|
||||
return type + " not found for id: " + id;
|
||||
}
|
||||
if (whereExpressions != null) {
|
||||
String desc = whereExpressions.singleEqDescription();
|
||||
if (desc != null) {
|
||||
return type + " not found for " + desc;
|
||||
}
|
||||
}
|
||||
return type + " not found";
|
||||
}
|
||||
|
||||
@Override
|
||||
public final FutureIds<T> findFutureIds() {
|
||||
return server.findFutureIds(this);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebean.Database;
|
||||
import io.ebean.Transaction;
|
||||
import io.ebean.Update;
|
||||
import io.ebeaninternal.api.BindParams;
|
||||
import io.ebeaninternal.api.SpiUpdate;
|
||||
@@ -31,7 +30,6 @@ public final class DefaultOrmUpdate<T> implements SpiUpdate<T>, Serializable {
|
||||
private String generatedSql;
|
||||
private final String baseTable;
|
||||
private final OrmUpdateType type;
|
||||
private transient Transaction transaction;
|
||||
|
||||
/**
|
||||
* Create with a specific server. This means you can use the
|
||||
@@ -90,13 +88,7 @@ public final class DefaultOrmUpdate<T> implements SpiUpdate<T>, Serializable {
|
||||
|
||||
@Override
|
||||
public int execute() {
|
||||
return server.execute(this, transaction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultOrmUpdate<T> usingTransaction(Transaction transaction) {
|
||||
this.transaction = transaction;
|
||||
return this;
|
||||
return server.execute(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
-22
@@ -281,17 +281,6 @@ public final class DefaultRelationalQuery extends AbstractQuery implements SpiSq
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeQuery<T> usingConnection(Connection connection) {
|
||||
DefaultRelationalQuery.this.usingConnection(connection);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
DefaultRelationalQuery.this.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T findOne() {
|
||||
return findSingleAttribute(type);
|
||||
@@ -337,17 +326,6 @@ public final class DefaultRelationalQuery extends AbstractQuery implements SpiSq
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeQuery<T> usingConnection(Connection connection) {
|
||||
DefaultRelationalQuery.this.usingConnection(connection);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
DefaultRelationalQuery.this.cancel();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public T findOne() {
|
||||
|
||||
@@ -7,7 +7,6 @@ import io.ebeaninternal.api.SpiExpressionList;
|
||||
import io.ebeaninternal.api.SpiQueryManyJoin;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssoc;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocOne;
|
||||
import io.ebeaninternal.server.el.ElPropertyDeploy;
|
||||
import io.ebeaninternal.server.el.ElPropertyValue;
|
||||
|
||||
@@ -359,54 +358,6 @@ public final class OrmQueryDetail implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After sorting, fold any fetch path that only fetches a *ToOne association's id
|
||||
* into a plain select of the parent path instead - the foreign key column is already
|
||||
* present on the owning table so the join can be avoided entirely.
|
||||
* <p>
|
||||
* Iterates the fetch paths backwards (deepest first) so that a path already folded
|
||||
* away does not stop a shallower ancestor also being folded, while a path that must
|
||||
* remain a real join protects its own parent (added to {@code nonRemovable}) from
|
||||
* being folded, as the parent's join is required to reach it.
|
||||
*/
|
||||
private void convertIdFetches(BeanDescriptor<?> desc) {
|
||||
Set<String> nonRemovable = new HashSet<>();
|
||||
String[] paths = fetchPaths.keySet().toArray(new String[0]);
|
||||
int i = paths.length;
|
||||
while (i-- > 0) {
|
||||
String path = paths[i];
|
||||
ElPropertyDeploy el = desc.elPropertyDeploy(path);
|
||||
OrmQueryProperties prop = fetchPaths.get(path);
|
||||
if (nonRemovable.contains(path)) {
|
||||
// a still-existing child fetch depends on this join, don't remove
|
||||
} else if (el == null) {
|
||||
throw new PersistenceException("Invalid fetch path " + path + " from " + desc.fullName());
|
||||
} else if (el.beanProperty() instanceof BeanPropertyAssocOne) {
|
||||
BeanPropertyAssocOne<?> assoc = (BeanPropertyAssocOne<?>) el.beanProperty();
|
||||
// exported (mappedBy) one-to-one has no local foreign key column - it always needs the join
|
||||
if (assoc.hasForeignKeyConstraint() && !assoc.isOneToOneExported()) {
|
||||
if (prop.includesExactly(assoc.targetDescriptor().idName())) {
|
||||
String parentPath = prop.getParentPath();
|
||||
OrmQueryProperties parentProp = parentPath == null ? baseProps : fetchPaths.get(parentPath);
|
||||
if (parentProp != null && parentProp.hasProperties()) {
|
||||
OrmQueryProperties newParentProp = parentProp.withAddedInclude(assoc.name());
|
||||
if (parentPath == null) {
|
||||
baseProps = newParentProp;
|
||||
} else {
|
||||
fetchPaths.put(parentPath, newParentProp);
|
||||
}
|
||||
fetchPaths.remove(path);
|
||||
prop = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prop != null && prop.getParentPath() != null) {
|
||||
nonRemovable.add(prop.getParentPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark 'fetch joins' to 'many' properties over to 'query joins' where needed.
|
||||
*
|
||||
@@ -424,7 +375,6 @@ public final class OrmQueryDetail implements Serializable {
|
||||
boolean fetchJoinFirstMany = allowOne;
|
||||
|
||||
sortFetchPaths(beanDescriptor, addIds);
|
||||
convertIdFetches(beanDescriptor);
|
||||
List<FetchEntry> pairs = sortByFetchPreference(beanDescriptor);
|
||||
|
||||
for (FetchEntry pair : pairs) {
|
||||
|
||||
@@ -151,22 +151,6 @@ public final class OrmQueryProperties implements Serializable {
|
||||
: buildImmutableQueryPlanHashSuffix(sourceFetchConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor with a replacement included set (used by {@link #withAddedInclude(String)}).
|
||||
*/
|
||||
private OrmQueryProperties(OrmQueryProperties source, Set<String> replacementIncluded) {
|
||||
this.fetchConfig = source.fetchConfig;
|
||||
this.parentPath = source.parentPath;
|
||||
this.path = source.path;
|
||||
this.allProperties = source.allProperties;
|
||||
this.cache = source.cache;
|
||||
this.filterMany = source.filterMany;
|
||||
this.markForQueryJoin = source.markForQueryJoin;
|
||||
this.included = immutableIncluded(replacementIncluded);
|
||||
this.immutableHashPrefix = buildImmutableQueryPlanHashPrefix(path, this.included);
|
||||
this.immutableHashSuffix = source.immutableHashSuffix;
|
||||
}
|
||||
|
||||
private static Set<String> immutableIncluded(Set<String> included) {
|
||||
if (included == null) {
|
||||
return null;
|
||||
@@ -428,37 +412,6 @@ public final class OrmQueryProperties implements Serializable {
|
||||
return included == null || included.contains(propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the included properties are exactly the single given property.
|
||||
* <p>
|
||||
* Used to detect a fetch/select of a *ToOne association that only includes the
|
||||
* target's id property - a candidate for folding into the parent select as a plain
|
||||
* foreign key property (avoiding an unnecessary join).
|
||||
*/
|
||||
boolean includesExactly(String property) {
|
||||
return included != null && included.size() == 1 && included.contains(property);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new instance with the given property added to the included set.
|
||||
* <p>
|
||||
* Used to fold an id-only *ToOne fetch into this select as a plain foreign key
|
||||
* property. A new instance is returned (rather than mutating {@link #included} in
|
||||
* place) as this instance's included set is immutable and may be shared/cached
|
||||
* (e.g. via FetchGroup reuse).
|
||||
*/
|
||||
OrmQueryProperties withAddedInclude(String property) {
|
||||
if (allProperties) {
|
||||
return this;
|
||||
}
|
||||
Set<String> newIncluded = new LinkedHashSet<>();
|
||||
if (included != null) {
|
||||
newIncluded.addAll(included);
|
||||
}
|
||||
newIncluded.add(property);
|
||||
return new OrmQueryProperties(this, newIncluded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this path as needing to be a query join.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean ddl generation</name>
|
||||
@@ -28,14 +28,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.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.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- provided scope -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-opentelemetry</artifactId>
|
||||
@@ -28,7 +28,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -71,21 +71,21 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean pgvector types</name>
|
||||
@@ -19,14 +19,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- provided scope -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean postgis types</name>
|
||||
@@ -19,14 +19,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- provided scope -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean querybean</name>
|
||||
@@ -17,7 +17,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -59,14 +59,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -738,11 +738,6 @@ public abstract class QueryBean<T, R extends QueryBean<T, R>> implements IQueryB
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
query.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final R usingTransaction(Transaction transaction) {
|
||||
query.usingTransaction(transaction);
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-redis</artifactId>
|
||||
@@ -29,35 +29,35 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-redisson</artifactId>
|
||||
@@ -29,35 +29,35 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-spring-txn</artifactId>
|
||||
@@ -28,14 +28,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.22.1</version>
|
||||
<version>2.22.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean test</name>
|
||||
@@ -33,20 +33,20 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -149,14 +149,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -849,11 +849,6 @@ public class TDSpiEbeanServer extends TDSpiServer implements SpiEbeanServer {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int executeNow(SpiSqlUpdate sqlUpdate, Transaction transaction) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addBatch(SpiSqlUpdate sqlUpdate, SpiTransaction transaction) {
|
||||
}
|
||||
|
||||
@@ -629,7 +629,7 @@ class EqlParserTest extends BaseTestCase {
|
||||
List<OrderDetail> details = query.findList();
|
||||
|
||||
assertThat(details).isNotEmpty();
|
||||
assertSql(query).contains("select sum(t0.order_qty), t0.order_id from o_order_detail t0 group by t0.order_id");
|
||||
assertSql(query).contains("select sum(t0.order_qty), t1.id from o_order_detail t0 join o_order t1 on t1.id = t0.order_id group by t1.id");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.tests.basic;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.Transaction;
|
||||
import io.ebean.Update;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@@ -73,29 +72,4 @@ public class TestUpdate extends BaseTestCase {
|
||||
Customer cust = DB.find(Customer.class).where().eq("name", "testUpdate3").findOne();
|
||||
assertThat(cust.getSmallnote()).isEqualTo("Note #3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUsingTransaction_explicitNonAmbientTransaction() {
|
||||
Update<Customer> update = DB.createUpdate(Customer.class,
|
||||
"update customer set smallnote = :smallnote where name = :name")
|
||||
.setParameter("name", "testUpdate1")
|
||||
.setParameter("smallnote", "explicit txn note");
|
||||
|
||||
// ambient/current transaction on the thread - deliberately rolled back
|
||||
try (Transaction ambient = DB.beginTransaction()) {
|
||||
// explicit transaction - NOT the ambient/current one on the thread
|
||||
try (Transaction explicit = DB.createTransaction()) {
|
||||
update.usingTransaction(explicit);
|
||||
int rows = update.execute();
|
||||
assertThat(rows).isEqualTo(1);
|
||||
explicit.commit();
|
||||
}
|
||||
// rolling back the ambient transaction does not affect the update - it ran
|
||||
// against the explicit transaction which has already been committed above
|
||||
ambient.rollback();
|
||||
}
|
||||
|
||||
Customer cust = DB.find(Customer.class).where().eq("name", "testUpdate1").findOne();
|
||||
assertThat(cust.getSmallnote()).isEqualTo("explicit txn note");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -43,34 +43,6 @@ class TestInsertOnConflict extends BaseTestCase {
|
||||
.build());
|
||||
}
|
||||
|
||||
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE, Platform.SQLITE})
|
||||
@Test
|
||||
void insertOnConflictUpdate_fallsBackToPrimaryKey_whenNoUniqueColumnsMapped() {
|
||||
Database db = DB.getDefault();
|
||||
LoggedSql.start();
|
||||
|
||||
var entity1 = new EStrIdBean();
|
||||
entity1.setId("fallback-1");
|
||||
entity1.setName("Example");
|
||||
// no uniqueColumns()/constraint() explicitly set - id is not mapped @Column(unique=true)
|
||||
// or @Index(unique=true) so this should fall back to using the primary key (id) column
|
||||
db.insert(entity1, ON_CONFLICT_UPDATE);
|
||||
|
||||
var entity2 = new EStrIdBean();
|
||||
entity2.setId("fallback-1");
|
||||
entity2.setName("Updated");
|
||||
db.insert(entity2, ON_CONFLICT_UPDATE);
|
||||
|
||||
var sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(2);
|
||||
assertThat(sql.get(0)).contains("on conflict (id) do update set");
|
||||
assertThat(sql.get(1)).contains("on conflict (id) do update set");
|
||||
|
||||
EStrIdBean found = db.find(EStrIdBean.class, "fallback-1");
|
||||
assertThat(found).isNotNull();
|
||||
assertThat(found.name()).isEqualTo("Updated");
|
||||
}
|
||||
|
||||
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE, Platform.SQLITE})
|
||||
@Test
|
||||
void insertOnConflictUpdateExplicitTransaction() {
|
||||
@@ -335,35 +307,6 @@ 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 {
|
||||
|
||||
@@ -93,7 +93,7 @@ public class TestMergeCustomer extends BaseTestCase {
|
||||
|
||||
List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(6);
|
||||
assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?");
|
||||
assertSql(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?");
|
||||
assertSql(sql.get(1)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
|
||||
assertSqlBind(sql, 2, 3);
|
||||
assertThat(sql.get(5)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
|
||||
@@ -122,7 +122,7 @@ public class TestMergeCustomer extends BaseTestCase {
|
||||
|
||||
List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(8);
|
||||
assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?");
|
||||
assertSql(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?");
|
||||
assertSql(sql.get(1)).contains("insert into maddress (id, street, city, version) values (?,?,?,?)");
|
||||
assertSqlBind(sql.get(2));
|
||||
assertThat(sql.get(4)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
|
||||
@@ -155,7 +155,7 @@ public class TestMergeCustomer extends BaseTestCase {
|
||||
|
||||
List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(9);
|
||||
assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?");
|
||||
assertSql(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?");
|
||||
|
||||
// Additional check to see if the address with the unknown UUID is 'insert' or 'update'
|
||||
assertSql(sql.get(1)).contains("select t0.id from maddress t0 where t0.id = ?");
|
||||
@@ -317,7 +317,7 @@ public class TestMergeCustomer extends BaseTestCase {
|
||||
List<String> sql = LoggedSql.stop();
|
||||
if (isPersistBatchOnCascade()) {
|
||||
|
||||
assertSql(sql.get(0)).contains("select t0.id, t0.shipping_address_id, t0.billing_address_id, t1.id from mcustomer t0 left join mcontact t1 on t1.customer_id = t0.id where t0.id = ?");
|
||||
assertSql(sql.get(0)).contains("select t0.id, t3.id, t1.id, t2.id from mcustomer t0 left join maddress t3 on t3.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id left join mcontact t2 on t2.customer_id = t0.id where t0.id = ?");
|
||||
if (isH2() || isHana()) {
|
||||
// with nested OneToMany .. we need a second query to read the contact message ids
|
||||
assertSql(sql.get(1)).contains("select t0.contact_id, t0.id from mcontact_message t0 where (t0.contact_id) in (?,?,?,?,?,?,?,?,?,?)");
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package org.tests.query;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.Query;
|
||||
import io.ebean.text.PathProperties;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.model.basic.Order;
|
||||
import org.tests.model.basic.ResetBasicData;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Fetching only the id of a *ToOne association should not require a join - the
|
||||
* foreign key column is already present on the owning table (issue #3643).
|
||||
*/
|
||||
public class TestFetchIdOnly extends BaseTestCase {
|
||||
|
||||
@Test
|
||||
public void test_withFetchPath() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
Query<Order> query = DB.find(Order.class)
|
||||
.apply(PathProperties.parse("status,customer(id)"));
|
||||
|
||||
query.findList();
|
||||
assertSql(query.getGeneratedSql()).contains("select t0.id, t0.status, t0.kcustomer_id from o_order t0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_withSelect() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
Query<Order> query = DB.find(Order.class)
|
||||
.select("status, customer");
|
||||
|
||||
query.findList();
|
||||
assertSql(query.getGeneratedSql()).contains("select t0.id, t0.status, t0.kcustomer_id from o_order t0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_withFetch() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
Query<Order> query = DB.find(Order.class)
|
||||
.select("status")
|
||||
.fetch("customer", "id");
|
||||
|
||||
query.findList();
|
||||
assertSql(query.getGeneratedSql()).contains("select t0.id, t0.status, t0.kcustomer_id from o_order t0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_withFetch_whenIncludesMoreThanId_expectJoin() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
Query<Order> query = DB.find(Order.class)
|
||||
.select("status")
|
||||
.fetch("customer", "id, name");
|
||||
|
||||
query.findList();
|
||||
assertThat(query.getGeneratedSql()).contains("join");
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package org.tests.query;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.model.basic.Customer;
|
||||
import org.tests.model.basic.ResetBasicData;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@code findOneOrThrow()} - a convenience alternative to
|
||||
* {@code findOneOrEmpty().orElseThrow(...)} that produces a decent default message
|
||||
* when the query is effectively a find-by-id or single natural/unique key lookup.
|
||||
*/
|
||||
class TestFindOneOrThrow extends BaseTestCase {
|
||||
|
||||
@Test
|
||||
void findOneOrThrow_whenFound_returnsBean() {
|
||||
ResetBasicData.reset();
|
||||
Customer existing = DB.find(Customer.class).setMaxRows(1).findList().get(0);
|
||||
|
||||
Customer found = DB.find(Customer.class).setId(existing.getId()).findOneOrThrow();
|
||||
|
||||
assertThat(found.getId()).isEqualTo(existing.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOneOrThrow_whenNotFoundById_messageIncludesId() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
assertThatThrownBy(() -> DB.find(Customer.class).setId(999999).findOneOrThrow())
|
||||
.isInstanceOf(EntityNotFoundException.class)
|
||||
.hasMessage("Customer not found for id: 999999");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOneOrThrow_whenNotFoundBySingleEqPredicate_messageIncludesPredicate() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
assertThatThrownBy(() -> DB.find(Customer.class)
|
||||
.where().eq("name", "NonExistentCustomerXYZ")
|
||||
.findOneOrThrow())
|
||||
.isInstanceOf(EntityNotFoundException.class)
|
||||
.hasMessage("Customer not found for name: NonExistentCustomerXYZ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOneOrThrow_whenNotFoundByMultiplePredicates_fallsBackToGenericMessage() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
assertThatThrownBy(() -> DB.find(Customer.class)
|
||||
.where().eq("name", "NonExistentCustomerXYZ").eq("id", 999999)
|
||||
.findOneOrThrow())
|
||||
.isInstanceOf(EntityNotFoundException.class)
|
||||
.hasMessage("Customer not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOneOrThrow_withSupplier_usesSuppliedException() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
assertThatThrownBy(() -> DB.find(Customer.class).setId(999999)
|
||||
.findOneOrThrow(() -> new IllegalStateException("custom message")))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("custom message");
|
||||
}
|
||||
}
|
||||
@@ -462,11 +462,7 @@ public class TestQueryFilterMany extends BaseTestCase {
|
||||
// nested "group.name" reference forces this to a query join so the filter is applied
|
||||
// as a genuine WHERE clause (not misapplied to a LEFT JOIN's ON clause)
|
||||
assertThat(sql).hasSize(2);
|
||||
if (isPostgresCompatible()) {
|
||||
assertSql(sql.get(1)).contains(" from contact t0 left join contact_group t1 on t1.id = t0.group_id where (t0.customer_id) = any(");
|
||||
} else {
|
||||
assertSql(sql.get(1)).contains(" from contact t0 left join contact_group t1 on t1.id = t0.group_id where (t0.customer_id) in (");
|
||||
}
|
||||
assertSql(sql.get(1)).contains(" from contact t0 left join contact_group t1 on t1.id = t0.group_id where (t0.customer_id) in (");
|
||||
assertSql(sql.get(1)).contains(" and t1.name = ? and t0.cretime is not null");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +58,7 @@ public class TestQueryFilterManySimple extends BaseTestCase {
|
||||
// nested "customer.status" reference forces this to a query join so the filter is
|
||||
// applied as a genuine WHERE clause (not misapplied to a LEFT JOIN's ON clause)
|
||||
assertThat(sql).hasSize(2);
|
||||
if (isPostgresCompatible()) {
|
||||
assertThat(sql.get(1)).contains("from o_order t0 join o_customer t1 on t1.id = t0.kcustomer_id where t0.order_date is not null and (t0.kcustomer_id) = any(");
|
||||
} else {
|
||||
assertThat(sql.get(1)).contains("from o_order t0 join o_customer t1 on t1.id = t0.kcustomer_id where t0.order_date is not null and (t0.kcustomer_id) in (");
|
||||
}
|
||||
assertThat(sql.get(1)).contains("from o_order t0 join o_customer t1 on t1.id = t0.kcustomer_id where t0.order_date is not null and (t0.kcustomer_id) in (");
|
||||
assertThat(sql.get(1)).contains(" and t1.status = ?");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +75,9 @@ public class TestSubQuery extends BaseTestCase {
|
||||
Query<OrderDetail> debugSq = sq.copy();
|
||||
debugSq.findSingleAttribute();
|
||||
if (isPostgresCompatible()) {
|
||||
assertThat(debugSq.getGeneratedSql()).isEqualTo("select t0.order_id from o_order_detail t0 where t0.product_id = any(?)");
|
||||
assertThat(debugSq.getGeneratedSql()).isEqualTo("select t1.id from o_order_detail t0 join o_order t1 on t1.id = t0.order_id where t0.product_id = any(?)");
|
||||
} else {
|
||||
assertSql(debugSq.getGeneratedSql()).isEqualTo("select t0.order_id from o_order_detail t0 where t0.product_id in (?)");
|
||||
assertSql(debugSq.getGeneratedSql()).isEqualTo("select t1.id from o_order_detail t0 join o_order t1 on t1.id = t0.order_id where t0.product_id in (?)");
|
||||
}
|
||||
|
||||
Query<Order> query = DB.find(Order.class).select("shipDate").where().isIn("id", sq).query();
|
||||
|
||||
@@ -195,58 +195,4 @@ public class TestSqlUpdateInTxn extends BaseTestCase {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUsingTransaction_explicitNonAmbientTransaction() {
|
||||
|
||||
AuditLog log = new AuditLog();
|
||||
log.setDescription("initial");
|
||||
DB.save(log);
|
||||
|
||||
SqlUpdate update = DB.sqlUpdate("update audit_log set description = ? where id = ?")
|
||||
.setParameter(1, "explicit")
|
||||
.setParameter(2, log.getId());
|
||||
|
||||
// ambient/current transaction on the thread - deliberately rolled back
|
||||
try (Transaction ambient = DB.beginTransaction()) {
|
||||
// explicit transaction - NOT the ambient/current one on the thread
|
||||
try (Transaction explicit = DB.createTransaction()) {
|
||||
update.usingTransaction(explicit);
|
||||
int rows = update.execute();
|
||||
assertThat(rows).isEqualTo(1);
|
||||
explicit.commit();
|
||||
}
|
||||
// rolling back the ambient transaction does not affect the update - it ran
|
||||
// against the explicit transaction which has already been committed above
|
||||
ambient.rollback();
|
||||
}
|
||||
|
||||
AuditLog reloaded = DB.find(AuditLog.class, log.getId());
|
||||
assertThat(reloaded.getDescription()).isEqualTo("explicit");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUsingTransaction_executeNow_explicitNonAmbientTransaction() {
|
||||
|
||||
AuditLog log = new AuditLog();
|
||||
log.setDescription("initial");
|
||||
DB.save(log);
|
||||
|
||||
SqlUpdate update = DB.sqlUpdate("update audit_log set description = ? where id = ?")
|
||||
.setParameter(1, "explicitNow")
|
||||
.setParameter(2, log.getId());
|
||||
|
||||
try (Transaction ambient = DB.beginTransaction()) {
|
||||
try (Transaction explicit = DB.createTransaction()) {
|
||||
update.usingTransaction(explicit);
|
||||
int rows = update.executeNow();
|
||||
assertThat(rows).isEqualTo(1);
|
||||
explicit.commit();
|
||||
}
|
||||
ambient.rollback();
|
||||
}
|
||||
|
||||
AuditLog reloaded = DB.find(AuditLog.class, log.getId());
|
||||
assertThat(reloaded.getDescription()).isEqualTo("explicitNow");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ mvn -T 4 clean package
|
||||
mvn -T 4 deploy -pl '!composites,!platforms' -Pcentral -DskipTests
|
||||
|
||||
## git commit, git tag, git push --tags
|
||||
git commit -am 'Version 18.3.0'
|
||||
git tag 18.3.0
|
||||
git commit -am 'Version 18.2.0'
|
||||
git tag 18.2.0
|
||||
git push --tags
|
||||
|
||||
### convert to javax
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>kotlin querybean generator</name>
|
||||
@@ -21,7 +21,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -56,14 +56,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
+14
-14
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,67 +16,67 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hsqldb</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlanywhere</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.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.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>platforms</artifactId>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.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.11</ebean-annotation.version>
|
||||
<ebean-annotation.version>8.6</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.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>querybean generator</name>
|
||||
@@ -45,7 +45,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -34,7 +34,6 @@ interface Constants {
|
||||
String DTO_PATH = "io.ebean.annotation.DtoPath";
|
||||
String DTO_REF = "io.ebean.annotation.DtoRef";
|
||||
String DTO_CONVERT = "io.ebean.annotation.DtoConvert";
|
||||
String DTO_CONVERTERS = "io.ebean.annotation.DtoConverters";
|
||||
String DTO_MIXIN = "io.ebean.annotation.DtoMixin";
|
||||
String DTO_CONVERTER_MANAGER = "io.ebean.DtoConverterManager";
|
||||
String METAINF_SERVICES_DTOMAPPERREGISTER = "META-INF/services/io.ebean.config.DtoMapperRegister";
|
||||
|
||||
@@ -18,7 +18,6 @@ class DtoBeanMeta {
|
||||
private final List<DtoPropertyMeta> properties = new ArrayList<>();
|
||||
private final List<DtoMapperVariant> variants = new ArrayList<>();
|
||||
private DtoBuilderMeta builderMeta;
|
||||
private DtoSetterMeta setterMeta;
|
||||
|
||||
/** Cycle detection state - see {@link DtoMappingReader#checkForCycles()}. */
|
||||
enum Visit { WHITE, GRAY, BLACK }
|
||||
@@ -33,13 +32,11 @@ class DtoBeanMeta {
|
||||
*/
|
||||
private boolean nestedElsewhere;
|
||||
|
||||
DtoBeanMeta(TypeElement source, TypeElement target, String mapperPackage, String mapperName) {
|
||||
DtoBeanMeta(TypeElement source, TypeElement target, String mapperPackage) {
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
this.mapperPackage = mapperPackage;
|
||||
this.mapperShortName = mapperName != null && !mapperName.isEmpty()
|
||||
? mapperName
|
||||
: target.getSimpleName() + "Mapper";
|
||||
this.mapperShortName = target.getSimpleName().toString() + "Mapper";
|
||||
this.mapperFullName = mapperPackage + "." + mapperShortName;
|
||||
}
|
||||
|
||||
@@ -100,20 +97,6 @@ class DtoBeanMeta {
|
||||
this.builderMeta = builderMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* The detected+selected setter-based construction path for this target, or {@code null} if not
|
||||
* applicable - see {@link DtoMappingReader#resolveSetter(DtoBeanMeta, String)}. Mutually
|
||||
* exclusive with {@link #builderMeta()} - a builder, when selected, always takes priority (see
|
||||
* {@link DtoMappingReader#resolveAndValidate()}).
|
||||
*/
|
||||
DtoSetterMeta setterMeta() {
|
||||
return setterMeta;
|
||||
}
|
||||
|
||||
void setterMeta(DtoSetterMeta setterMeta) {
|
||||
this.setterMeta = setterMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the distinct instance-dispatch {@code @DtoConvert} converters used by this mapper's
|
||||
* properties (static-dispatch converters need no field/constructor wiring, so are excluded),
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ final class DtoMapperVariant {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
/** Dto field names ({@code NESTED_ONE}/{@code NESTED_MANY}, or non-primitive {@code SCALAR}) this variant excludes. */
|
||||
/** Dto field names (of {@code NESTED_ONE}/{@code NESTED_MANY} properties) this variant excludes. */
|
||||
Set<String> excludedProperties() {
|
||||
return excludedProperties;
|
||||
}
|
||||
|
||||
+32
-263
@@ -41,7 +41,6 @@ class DtoMapperWriter {
|
||||
writeConstructors();
|
||||
writeFetchGroupMethod();
|
||||
writeMapMethod();
|
||||
writeMapToBuilderMethod();
|
||||
if (!meta.variants().isEmpty()) {
|
||||
writeBuildMethod();
|
||||
writeVariantAccessors();
|
||||
@@ -61,9 +60,6 @@ class DtoMapperWriter {
|
||||
Set<String> allReferenced = new LinkedHashSet<>();
|
||||
allReferenced.add(meta.sourceFullName());
|
||||
allReferenced.add(meta.targetFullName());
|
||||
if (meta.builderMeta() != null) {
|
||||
allReferenced.add(meta.builderMeta().builderTypeFullName());
|
||||
}
|
||||
for (DtoPropertyMeta property : meta.properties()) {
|
||||
if (property.nested() != null) {
|
||||
allReferenced.add(property.nested().sourceFullName());
|
||||
@@ -108,16 +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(DtoPropertyMeta::isListTarget)
|
||||
|| meta.properties().stream().anyMatch(p -> p.isIgnored() && p.isListTarget())) {
|
||||
if (variableProperties().stream().anyMatch(p -> p.kind() == DtoPropertyMeta.Kind.NESTED_MANY)) {
|
||||
imports.add("java.util.List");
|
||||
}
|
||||
if (meta.builderMeta() != null) {
|
||||
addImportIfNeeded(imports, meta.builderMeta().builderTypeFullName());
|
||||
}
|
||||
for (String imp : imports) {
|
||||
writer.append("import %s;", imp).eol();
|
||||
}
|
||||
@@ -227,66 +216,30 @@ class DtoMapperWriter {
|
||||
private List<String> fetchGroupChainCalls(Set<String> excludedFieldNames) {
|
||||
List<DtoPropertyMeta> activeProperties = new ArrayList<>();
|
||||
for (DtoPropertyMeta property : meta.properties()) {
|
||||
if (!excludedFieldNames.contains(property.dtoFieldName()) && !property.isIgnored()) {
|
||||
if (!excludedFieldNames.contains(property.dtoFieldName())) {
|
||||
activeProperties.add(property);
|
||||
}
|
||||
}
|
||||
Set<String> nestedAssocPaths = new LinkedHashSet<>();
|
||||
for (DtoPropertyMeta property : activeProperties) {
|
||||
if ((property.kind() == DtoPropertyMeta.Kind.NESTED_ONE || property.kind() == DtoPropertyMeta.Kind.NESTED_MANY)
|
||||
&& !property.hasComputedSegment()) {
|
||||
nestedAssocPaths.add(property.sourcePropertyPath().get(0));
|
||||
} else if (property.kind() == DtoPropertyMeta.Kind.SCALAR && property.isListTarget()
|
||||
&& !property.hasComputedSegment() && property.sourcePropertyPath().size() == 1) {
|
||||
// a single-segment SCALAR property whose DTO field is a List with no registered nested
|
||||
// DTO mapping of its own (e.g. @DtoConvert reducing a ToMany association) - still fully
|
||||
// fetches its association path via a bare fetch(path) call below, so any other property
|
||||
// targeting a sub-path of the same association must not also try a narrower select.
|
||||
if (property.kind() == DtoPropertyMeta.Kind.NESTED_ONE || property.kind() == DtoPropertyMeta.Kind.NESTED_MANY) {
|
||||
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) {
|
||||
if (property.isListTarget()) {
|
||||
// a single-segment path whose DTO field type is a List, but with no registered
|
||||
// nested DTO mapping of its own (e.g. a @DtoConvert-backed property reducing a
|
||||
// ToMany association to a simpler element type) - the source side is still a real
|
||||
// ToMany association, which needs an actual fetch/join (like NESTED_MANY below),
|
||||
// not a plain root select() (that only works for scalar columns on the base table).
|
||||
fetchCalls.add(String.format("fetch(\"%s\")", path.get(0)));
|
||||
} else {
|
||||
rootSelect.add(path.get(0));
|
||||
}
|
||||
rootSelect.add(path.get(0));
|
||||
} else {
|
||||
String fetchPath = String.join(".", path.subList(0, path.size() - 1));
|
||||
if (nestedAssocPaths.contains(fetchPath)) {
|
||||
@@ -301,15 +254,6 @@ 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);
|
||||
@@ -318,27 +262,8 @@ 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)));
|
||||
@@ -415,18 +340,7 @@ class DtoMapperWriter {
|
||||
writer.append(" if (source == null) {").eol();
|
||||
writer.append(" return null;").eol();
|
||||
writer.append(" }").eol();
|
||||
if (meta.builderMeta() != null) {
|
||||
// the base (full) mapping is always exactly mapToBuilder(...).build() - shares that one
|
||||
// chain rather than re-emitting a second copy of it here, whether or not named variants
|
||||
// exist (build(...), below, is only ever needed for a named variant's own partial view)
|
||||
if (meta.nestedElsewhere()) {
|
||||
writer.append(" // dedup using DtoMapContext, same %s instance can be reached via more than one path in the graph", sourceShort).eol();
|
||||
writer.append(" return context.computeIfAbsent(%s.class, source, s -> mapToBuilder(s, context).build());", targetShort).eol();
|
||||
} else {
|
||||
writer.append(" // DtoMapContext for nested mappers only").eol();
|
||||
writer.append(" return mapToBuilder(source, context).build();").eol();
|
||||
}
|
||||
} else if (!variableProps.isEmpty()) {
|
||||
if (!variableProps.isEmpty()) {
|
||||
// named variants exist - route construction through the shared build(...) method, this
|
||||
// (base) mapping includes every variable property (each still evaluated inline, at its own
|
||||
// declared position, inside build() - see buildCallArgs())
|
||||
@@ -440,139 +354,45 @@ class DtoMapperWriter {
|
||||
}
|
||||
} else if (meta.nestedElsewhere()) {
|
||||
writer.append(" // dedup using DtoMapContext, same %s instance can be reached via more than one path in the graph", sourceShort).eol();
|
||||
writeConstructionExpression(true, variableProps);
|
||||
writeConstructionExpression(
|
||||
String.format(" return context.computeIfAbsent(%s.class, source, s -> ", targetShort),
|
||||
"s", variableProps, ")");
|
||||
} else if (nestedProperties().isEmpty()) {
|
||||
writer.append(" // skip DtoMapContext, only ever a top-level mapping").eol();
|
||||
writeConstructionExpression(false, variableProps);
|
||||
writeConstructionExpression(" return ", "source", variableProps, "");
|
||||
} else {
|
||||
writer.append(" // DtoMapContext for nested mappers only").eol();
|
||||
writeConstructionExpression(false, variableProps);
|
||||
writeConstructionExpression(" return ", "source", variableProps, "");
|
||||
}
|
||||
writer.append(" }").eol();
|
||||
}
|
||||
|
||||
/**
|
||||
* Additionally expose a {@code mapToBuilder(source)} accessor - returning the target's detected
|
||||
* builder (see {@link DtoBeanMeta#builderMeta()}) one step before its final {@code build()}
|
||||
* call - only when the target is constructed via a builder in the first place. Lets a caller set
|
||||
* an {@code @DtoIgnore} property's real value (typically sourced from another query/service call
|
||||
* entirely outside this mapper's own fetch graph) before finishing construction, e.g.
|
||||
* {@code mapper.mapToBuilder(source).assignedMachines(loadMachines(source)).build()}.
|
||||
* <p>
|
||||
* When named variants exist, the actual guarded builder chain (each variable property behind
|
||||
* its own {@code includeXxx} ternary) is written exactly once, in a private flagged overload -
|
||||
* both this public (always-full) accessor and the shared {@link #writeBuildMethod()} (used by
|
||||
* each variant's own partial view) delegate to it, rather than each keeping their own copy.
|
||||
* Emit the target construction expression - either a positional constructor call or, when
|
||||
* {@link DtoBeanMeta#builderMeta()} is present (see {@code @DtoMapping(builder = ...)}), a
|
||||
* {@code Target.builder()....build()} fluent chain. {@code linePrefix} is written before the
|
||||
* expression starts (e.g. {@code " return "} or a {@code computeIfAbsent(...)} lambda
|
||||
* opener); {@code wrapperClosing} is any additional closing needed for an enclosing call this
|
||||
* expression is nested inside (e.g. {@code ")"} to close an enclosing
|
||||
* {@code computeIfAbsent(...)} call, or {@code ""} for a plain {@code return}) - the trailing
|
||||
* {@code ";"} is always added here. Every property - including ones in {@code variableProps} -
|
||||
* is evaluated inline, in true {@link DtoBeanMeta#properties()} declared order; a variable
|
||||
* property's expression is simply guarded by its {@code includeXxx} boolean parameter (see
|
||||
* {@link #writeBuildMethod()}), so no property's evaluation is ever hoisted out of its declared
|
||||
* position (only non-empty when named variants exist - see {@link #variableProperties()}).
|
||||
*/
|
||||
private void writeMapToBuilderMethod() {
|
||||
if (meta.builderMeta() == null) {
|
||||
return;
|
||||
}
|
||||
String sourceShort = Split.shortName(meta.sourceFullName());
|
||||
private void writeConstructionExpression(String linePrefix, String rootVariable, Set<DtoPropertyMeta> variableProps, String wrapperClosing) {
|
||||
String targetShort = Split.shortName(meta.targetFullName());
|
||||
String builderShort = meta.builderMeta().builderTypeShortName();
|
||||
Set<DtoPropertyMeta> variableProps = variableProperties();
|
||||
writer.append(" /**").eol();
|
||||
writer.append(" * Map to the target's builder, one step before its final build() call - lets a caller").eol();
|
||||
writer.append(" * set an @DtoIgnore property's real value before finishing construction.").eol();
|
||||
writer.append(" */").eol();
|
||||
writer.append(" public %s mapToBuilder(%s source) {", builderShort, sourceShort).eol();
|
||||
writer.append(" return mapToBuilder(source, new DtoMapContext());").eol();
|
||||
writer.append(" }").eol().eol();
|
||||
|
||||
writer.append(" public %s mapToBuilder(%s source, DtoMapContext context) {", builderShort, sourceShort).eol();
|
||||
writer.append(" if (source == null) {").eol();
|
||||
writer.append(" return null;").eol();
|
||||
writer.append(" }").eol();
|
||||
if (variableProps.isEmpty()) {
|
||||
writer.append(" return %s.builder()", targetShort).eol();
|
||||
writeBuilderChain("source", variableProps);
|
||||
writer.append(" ;").eol();
|
||||
} else {
|
||||
// this (base) mapping always includes every variable property - delegate to the shared
|
||||
// flagged overload below rather than re-emitting the chain here
|
||||
writer.append(" return mapToBuilder(source, context%s);", buildCallArgs(variableProps)).eol();
|
||||
}
|
||||
writer.append(" }").eol().eol();
|
||||
|
||||
if (!variableProps.isEmpty()) {
|
||||
writer.append(" private %s mapToBuilder(%s source, DtoMapContext context%s) {",
|
||||
builderShort, sourceShort, buildMethodParams(variableProps)).eol();
|
||||
writer.append(" return %s.builder()", targetShort).eol();
|
||||
writeBuilderChain("source", variableProps);
|
||||
writer.append(" ;").eol();
|
||||
writer.append(" }").eol().eol();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the target construction expression - a positional constructor call, a
|
||||
* {@code Target.builder()....build()} fluent chain (when {@link DtoBeanMeta#builderMeta()} is
|
||||
* present, see {@code @DtoMapping(builder = ...)}), or a setter-based
|
||||
* {@code Target target = new Target(); target.setX(...); ...; return target;} statement block
|
||||
* (when {@link DtoBeanMeta#setterMeta()} is present, see {@code @DtoMapping(setter = ...)}).
|
||||
* <p>
|
||||
* {@code computeIfAbsent} selects between a plain {@code return EXPR;} (the common case) and a
|
||||
* {@code return context.computeIfAbsent(Target.class, source, s -> EXPR)} wrapper (only used
|
||||
* when {@link DtoBeanMeta#nestedElsewhere()}, so the same source instance reached via more than
|
||||
* one path in the graph maps to the same target instance) - the setter-based case needs its own
|
||||
* block-lambda form (see {@link #writeSetterConstruction}) since, unlike the other two
|
||||
* strategies, it's a multi-statement construction rather than a single expression. Every
|
||||
* property - including ones in {@code variableProps} - is evaluated inline, in true
|
||||
* {@link DtoBeanMeta#properties()} declared order; a variable property's expression is simply
|
||||
* guarded by its {@code includeXxx} ternary (see {@link #writeBuildMethod()}), so no property's
|
||||
* evaluation is ever hoisted out of its declared position (only non-empty when named variants
|
||||
* exist - see {@link #variableProperties()}).
|
||||
*/
|
||||
private void writeConstructionExpression(boolean computeIfAbsent, Set<DtoPropertyMeta> variableProps) {
|
||||
String targetShort = Split.shortName(meta.targetFullName());
|
||||
String rootVariable = computeIfAbsent ? "s" : "source";
|
||||
if (meta.setterMeta() != null) {
|
||||
writeSetterConstruction(computeIfAbsent, targetShort, rootVariable, variableProps);
|
||||
return;
|
||||
}
|
||||
String returnPrefix = computeIfAbsent
|
||||
? String.format(" return context.computeIfAbsent(%s.class, source, s -> ", targetShort)
|
||||
: " return ";
|
||||
String closing = computeIfAbsent ? ")" : "";
|
||||
if (meta.builderMeta() == null) {
|
||||
writer.append("%snew %s(", returnPrefix, targetShort).eol();
|
||||
writeConstructionArgs(rootVariable, variableProps, ")" + closing + ";");
|
||||
writer.append("%snew %s(", linePrefix, targetShort).eol();
|
||||
writeConstructionArgs(rootVariable, variableProps, ")" + wrapperClosing + ";");
|
||||
} else {
|
||||
writer.append("%s%s.builder()", returnPrefix, targetShort).eol();
|
||||
writer.append("%s%s.builder()", linePrefix, targetShort).eol();
|
||||
writeBuilderChain(rootVariable, variableProps);
|
||||
writer.append(" .build()%s;", closing).eol();
|
||||
writer.append(" .build()%s;", wrapperClosing).eol();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit setter-based (mutable JavaBean) construction - see {@code @DtoMapping(setter = ...)},
|
||||
* {@link DtoSetterMeta}. Unlike a positional constructor call or a builder chain (both single
|
||||
* expressions), this is a multi-statement local-variable-then-setter-calls block, so the
|
||||
* {@code computeIfAbsent(...)}-wrapped (nested-elsewhere) case needs an explicit block lambda
|
||||
* (curly braces + its own {@code return}) rather than a single continued expression.
|
||||
*/
|
||||
private void writeSetterConstruction(boolean computeIfAbsent, String targetShort, String rootVariable, Set<DtoPropertyMeta> variableProps) {
|
||||
String indent = computeIfAbsent ? " " : " ";
|
||||
if (computeIfAbsent) {
|
||||
writer.append(" return context.computeIfAbsent(%s.class, source, s -> {", targetShort).eol();
|
||||
}
|
||||
writer.append("%s%s target = new %s();", indent, targetShort, targetShort).eol();
|
||||
for (DtoPropertyMeta property : meta.properties()) {
|
||||
writer.append("%starget.%s(%s);", indent, setterName(property.dtoFieldName()),
|
||||
constructionValueExpression(property, rootVariable, variableProps)).eol();
|
||||
}
|
||||
writer.append("%sreturn target;", indent).eol();
|
||||
if (computeIfAbsent) {
|
||||
writer.append(" });").eol();
|
||||
}
|
||||
}
|
||||
|
||||
/** {@code referenceCode} -> {@code setReferenceCode} (standard JavaBean setter naming convention) - mirrors {@code DtoMappingReader.setterName}. */
|
||||
private String setterName(String propertyName) {
|
||||
return "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
|
||||
}
|
||||
|
||||
private void writeConstructionArgs(String rootVariable, Set<DtoPropertyMeta> variableProps, String closing) {
|
||||
var properties = meta.properties();
|
||||
for (int i = 0; i < properties.size(); i++) {
|
||||
@@ -600,10 +420,6 @@ class DtoMapperWriter {
|
||||
* to be excluded by a variant.
|
||||
*/
|
||||
private String constructionValueExpression(DtoPropertyMeta property, String rootVariable, Set<DtoPropertyMeta> variableProps) {
|
||||
if (property.isIgnored()) {
|
||||
// permanently excluded (see DtoIgnore) - never resolved from source, regardless of variant
|
||||
return defaultValueFor(property);
|
||||
}
|
||||
String expression = propertyValueExpression(property, rootVariable);
|
||||
return variableProps.contains(property)
|
||||
? includeFlagName(property) + " ? " + expression + " : " + defaultValueFor(property)
|
||||
@@ -654,25 +470,10 @@ class DtoMapperWriter {
|
||||
String targetShort = Split.shortName(meta.targetFullName());
|
||||
writer.append(" private %s build(%s source, DtoMapContext context%s) {",
|
||||
targetShort, sourceShort, buildMethodParams(variableProps)).eol();
|
||||
if (meta.builderMeta() != null) {
|
||||
// reuse the very same flagged builder chain as the private mapToBuilder(...) overload
|
||||
// (see writeMapToBuilderMethod()) - no second copy of it here
|
||||
writer.append(" return mapToBuilder(source, context%s).build();", buildMethodArgs(variableProps)).eol();
|
||||
} else {
|
||||
writeConstructionExpression(false, variableProps);
|
||||
}
|
||||
writeConstructionExpression(" return ", "source", variableProps, "");
|
||||
writer.append(" }").eol().eol();
|
||||
}
|
||||
|
||||
/** Forward each variable property's own {@code includeXxx} parameter unchanged - used when one method delegates its flags to another (e.g. {@code build()} delegating to the flagged {@code mapToBuilder(...)} overload). */
|
||||
private String buildMethodArgs(Set<DtoPropertyMeta> variableProps) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (DtoPropertyMeta property : variableProps) {
|
||||
sb.append(", ").append(includeFlagName(property));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String buildMethodParams(Set<DtoPropertyMeta> variableProps) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (DtoPropertyMeta property : variableProps) {
|
||||
@@ -688,13 +489,12 @@ class DtoMapperWriter {
|
||||
}
|
||||
|
||||
/**
|
||||
* The empty/default value a variant supplies for a property it excludes - {@code List.of()}
|
||||
* when the property's DTO field type is a {@code List} ({@link DtoPropertyMeta.Kind#NESTED_MANY},
|
||||
* always list-shaped, or a {@link DtoPropertyMeta.Kind#SCALAR}/{@code @DtoConvert}-backed
|
||||
* property whose field happens to be a {@code List}), {@code null} otherwise.
|
||||
* The empty/default value a variant supplies for a property it excludes - {@code null} for a
|
||||
* {@code NESTED_ONE}, {@code List.of()} for a {@code NESTED_MANY} (always valid since
|
||||
* {@code NESTED_MANY} only ever derives from a {@code java.util.List<X>} target property).
|
||||
*/
|
||||
private String defaultValueFor(DtoPropertyMeta property) {
|
||||
return property.isListTarget() ? "List.of()" : "null";
|
||||
return property.kind() == DtoPropertyMeta.Kind.NESTED_MANY ? "List.of()" : "null";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -704,13 +504,6 @@ class DtoMapperWriter {
|
||||
* {@code DtoMapContext} identity-dedup wrapper used by the base mapping when
|
||||
* {@link DtoBeanMeta#nestedElsewhere()} - named variants are only ever used as an independent,
|
||||
* top-level {@code query.mapTo(...)} result, never nested inside another DTO graph.
|
||||
* <p>
|
||||
* When the target uses a builder, each variant additionally gets its own named
|
||||
* {@code mapToBuilder<Suffix>(source, context)} accessor (e.g. {@code mapToBuilderNoFleets(...)})
|
||||
* - not a raw flags parameter, and not a method on the variant's own (deliberately {@code
|
||||
* private}, inaccessible-by-type-from-outside) inner class - {@code mapToBuilder(...)} isn't
|
||||
* part of the {@link DtoMapper} interface (builder types vary per target), so there'd be no way
|
||||
* for external code to call it on a {@code DtoMapper<SOURCE, TARGET>}-typed variant view at all.
|
||||
*/
|
||||
private void writeVariantAccessors() {
|
||||
Set<DtoPropertyMeta> variableProps = variableProperties();
|
||||
@@ -723,35 +516,11 @@ class DtoMapperWriter {
|
||||
writer.append(" return %s;", variant.fieldName()).eol();
|
||||
writer.append(" }").eol().eol();
|
||||
}
|
||||
if (meta.builderMeta() != null) {
|
||||
for (DtoMapperVariant variant : meta.variants()) {
|
||||
writeVariantMapToBuilderMethods(variant, variableProps, sourceShort);
|
||||
}
|
||||
}
|
||||
for (DtoMapperVariant variant : meta.variants()) {
|
||||
writeVariantInnerClass(variant, variableProps, sourceShort, targetShort);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write one named variant's {@code mapToBuilder<Suffix>(source, context)} (+ single-arg
|
||||
* convenience overload) - delegates to the same shared, once-written flagged
|
||||
* {@code mapToBuilder(source, context, includeXxx...)} overload (see
|
||||
* {@link #writeMapToBuilderMethod()}) with this variant's own excluded properties fixed to
|
||||
* {@code false}, so no property's chain expression is ever duplicated per variant.
|
||||
*/
|
||||
private void writeVariantMapToBuilderMethods(DtoMapperVariant variant, Set<DtoPropertyMeta> variableProps, String sourceShort) {
|
||||
String builderShort = meta.builderMeta().builderTypeShortName();
|
||||
writer.append(" /** {@code mapToBuilder(...)} for the {%s} variant - see {@link #%s()}. */",
|
||||
variant.name(), variant.name()).eol();
|
||||
writer.append(" public %s mapToBuilder%s(%s source, DtoMapContext context) {", builderShort, variant.suffix(), sourceShort).eol();
|
||||
writer.append(" return mapToBuilder(source, context%s);", variantBuildArgs(variant, variableProps)).eol();
|
||||
writer.append(" }").eol().eol();
|
||||
writer.append(" public %s mapToBuilder%s(%s source) {", builderShort, variant.suffix(), sourceShort).eol();
|
||||
writer.append(" return mapToBuilder%s(source, new DtoMapContext());", variant.suffix()).eol();
|
||||
writer.append(" }").eol().eol();
|
||||
}
|
||||
|
||||
private void writeVariantInnerClass(DtoMapperVariant variant, Set<DtoPropertyMeta> variableProps, String sourceShort, String targetShort) {
|
||||
writer.append(" private final class %s implements DtoMapper<%s, %s> {", variant.innerClassName(), sourceShort, targetShort).eol().eol();
|
||||
writer.append(" @Override").eol();
|
||||
|
||||
+20
-512
@@ -52,16 +52,6 @@ class DtoMappingReader {
|
||||
/** {@code builder()} attribute value of each target's base (unnamed) registration. */
|
||||
private final Map<String, String> builderModeByTarget = new LinkedHashMap<>();
|
||||
|
||||
/** {@code setter()} attribute value of each target's base (unnamed) registration. */
|
||||
private final Map<String, String> setterModeByTarget = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* {@code @DtoConverters}-registered type-pair conversion methods, keyed by
|
||||
* {@link #converterKey(TypeMirror, TypeMirror)} (exact parameter type -> exact return type) -
|
||||
* see {@link #collectTypeConverters(RoundEnvironment)}/{@link #autoTypeConverter}.
|
||||
*/
|
||||
private final Map<String, ExecutableElement> typeConverters = new LinkedHashMap<>();
|
||||
|
||||
private static final class RawVariant {
|
||||
private final Element declaringElement;
|
||||
private final TypeElement source;
|
||||
@@ -118,7 +108,6 @@ class DtoMappingReader {
|
||||
}
|
||||
}
|
||||
collectMixins(roundEnv);
|
||||
collectTypeConverters(roundEnv);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,100 +135,10 @@ class DtoMappingReader {
|
||||
ctx.logError(element, "@DtoMixin value() must be a class type");
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
mixinsByTarget.put(target.getQualifiedName().toString(), (TypeElement) element);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover package/module-level {@code @DtoConverters({ConverterType.class, ...})}
|
||||
* declarations (proactively, via {@code roundEnv.getElementsAnnotatedWith(...)} - same
|
||||
* rationale as {@link #collectMixins}: a package-level annotation isn't tied to any specific
|
||||
* field being resolved, so it can't be discovered lazily the way a per-property
|
||||
* {@code @DtoConvert} is). Indexes every public, single-parameter, non-{@code void}-returning
|
||||
* method declared directly on each referenced type by its exact (parameter type, return type)
|
||||
* pair - see {@link #autoTypeConverter}.
|
||||
*/
|
||||
private void collectTypeConverters(RoundEnvironment roundEnv) {
|
||||
TypeElement annotationType = ctx.elementUtils().getTypeElement(Constants.DTO_CONVERTERS);
|
||||
if (annotationType == null) {
|
||||
return;
|
||||
}
|
||||
for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) {
|
||||
DtoConvertersPrism prism = DtoConvertersPrism.getInstanceOn(element);
|
||||
if (prism == null) {
|
||||
continue;
|
||||
}
|
||||
for (TypeMirror converterTypeMirror : prism.value()) {
|
||||
TypeElement converterType = asTypeElement(converterTypeMirror);
|
||||
if (converterType == null) {
|
||||
ctx.logError(element, "@DtoConverters value() must be class types");
|
||||
continue;
|
||||
}
|
||||
registerTypeConverterMethods(converterType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Index every public, single-parameter, non-{@code void}-returning method declared directly on
|
||||
* {@code converterType} by its exact (parameter type, return type) pair, logging an error for
|
||||
* any pair that would otherwise ambiguously match more than one registered method (across this
|
||||
* or any other {@code @DtoConverters}-registered type).
|
||||
*/
|
||||
private void registerTypeConverterMethods(TypeElement converterType) {
|
||||
for (ExecutableElement method : ElementFilter.methodsIn(converterType.getEnclosedElements())) {
|
||||
if (!method.getModifiers().contains(Modifier.PUBLIC) || method.getParameters().size() != 1
|
||||
|| method.getReturnType().getKind() == TypeKind.VOID) {
|
||||
continue;
|
||||
}
|
||||
TypeMirror paramType = method.getParameters().get(0).asType();
|
||||
TypeMirror returnType = method.getReturnType();
|
||||
String key = converterKey(paramType, returnType);
|
||||
ExecutableElement existing = typeConverters.get(key);
|
||||
if (existing != null) {
|
||||
ctx.logError(method, "@DtoConverters: ambiguous type-pair conversion (%s -> %s) - %s.%s and %s.%s"
|
||||
+ " both match this exact type pair; only one registered method per type pair is allowed",
|
||||
paramType, returnType, ((TypeElement) existing.getEnclosingElement()).getQualifiedName(),
|
||||
existing.getSimpleName(), converterType.getQualifiedName(), method.getSimpleName());
|
||||
continue;
|
||||
}
|
||||
typeConverters.put(key, method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-apply a {@code @DtoConverters}-registered type-pair conversion when {@code explicit} is
|
||||
* {@code null} (no per-property {@code @DtoConvert} present) and {@code sourceType} doesn't
|
||||
* already exactly match {@code targetType}. Matched by exact type equality on both sides - no
|
||||
* widening/supertype matching - so which method (if any) applies stays fully predictable. Never
|
||||
* overrides an explicit per-property {@code @DtoConvert} - only ever consulted when
|
||||
* {@code explicit} is already {@code null}.
|
||||
*/
|
||||
private DtoConverterMeta autoTypeConverter(DtoConverterMeta explicit, TypeMirror sourceType, TypeMirror targetType) {
|
||||
if (explicit != null || sourceType == null || typeConverters.isEmpty()
|
||||
|| ctx.typeUtils().isSameType(sourceType, targetType)) {
|
||||
return explicit;
|
||||
}
|
||||
ExecutableElement method = typeConverters.get(converterKey(sourceType, targetType));
|
||||
if (method == null) {
|
||||
return null;
|
||||
}
|
||||
TypeElement converterType = (TypeElement) method.getEnclosingElement();
|
||||
boolean isStatic = method.getModifiers().contains(Modifier.STATIC);
|
||||
return new DtoConverterMeta(converterType.getQualifiedName().toString(), method.getSimpleName().toString(), isStatic);
|
||||
}
|
||||
|
||||
private String converterKey(TypeMirror paramType, TypeMirror returnType) {
|
||||
return paramType.toString() + "->" + returnType.toString();
|
||||
}
|
||||
|
||||
private void addAnnotatedElements(RoundEnvironment roundEnv, String annotationTypeName, Set<Element> elements) {
|
||||
TypeElement annotationType = ctx.elementUtils().getTypeElement(annotationTypeName);
|
||||
if (annotationType != null) {
|
||||
@@ -288,7 +187,6 @@ class DtoMappingReader {
|
||||
resolveVariants();
|
||||
for (DtoBeanMeta meta : byTargetName.values()) {
|
||||
resolveBuilder(meta, builderModeByTarget.get(meta.targetFullName()));
|
||||
resolveSetter(meta, setterModeByTarget.get(meta.targetFullName()));
|
||||
}
|
||||
List<DtoBeanMeta> result = excludeCycles();
|
||||
markNestedElsewhere(result);
|
||||
@@ -299,8 +197,7 @@ class DtoMappingReader {
|
||||
* Attach each raw {@code @DtoMapping(name = ..., exclude = ...)} variant registration (see
|
||||
* {@link #register}) to its base mapping's {@link DtoBeanMeta}, validating that a base exists,
|
||||
* variant names are unique per target, the variant's source matches the base's source, and
|
||||
* each excluded name resolves to a {@code NESTED_ONE}/{@code NESTED_MANY} property, or a
|
||||
* non-primitive {@code SCALAR} property.
|
||||
* each excluded name resolves to a {@code NESTED_ONE}/{@code NESTED_MANY} property.
|
||||
*/
|
||||
private void resolveVariants() {
|
||||
for (var entry : variantsByTarget.entrySet()) {
|
||||
@@ -335,12 +232,9 @@ class DtoMappingReader {
|
||||
|
||||
/**
|
||||
* Validate and resolve one variant's {@code exclude()} property names against {@code meta}'s
|
||||
* already-resolved properties - a {@code NESTED_ONE}/{@code NESTED_MANY} property can always be
|
||||
* excluded (falls back to {@code null}/{@code List.of()}); a plain {@link DtoPropertyMeta.Kind#SCALAR}
|
||||
* property (e.g. a {@code @DtoConvert}-backed property with no registered nested DTO mapping of
|
||||
* its own) can also be excluded as long as its DTO field type isn't a Java primitive - there's
|
||||
* no type-safe "absent" value for those. Returns {@code null} (with error(s) already logged) if
|
||||
* anything doesn't resolve.
|
||||
* already-resolved properties - only {@code NESTED_ONE}/{@code NESTED_MANY} properties can be
|
||||
* excluded (there's no type-safe "absent" value for an arbitrary scalar type). Returns
|
||||
* {@code null} (with error(s) already logged) if anything doesn't resolve.
|
||||
*/
|
||||
private Set<String> resolveExcludedProperties(DtoBeanMeta meta, RawVariant raw) {
|
||||
if (raw.exclude().isEmpty()) {
|
||||
@@ -356,36 +250,18 @@ class DtoMappingReader {
|
||||
ctx.logError(raw.declaringElement(), "@DtoMapping(name = \"%s\") exclude(\"%s\") does not match"
|
||||
+ " any property on target %s", raw.name(), propName, meta.targetFullName());
|
||||
ok = false;
|
||||
} else if (property.isIgnored()) {
|
||||
} else if (property.kind() != DtoPropertyMeta.Kind.NESTED_ONE && property.kind() != DtoPropertyMeta.Kind.NESTED_MANY) {
|
||||
ctx.logError(raw.declaringElement(), "@DtoMapping(name = \"%s\") exclude(\"%s\") on target %s is"
|
||||
+ " already @DtoIgnore - it's permanently excluded from every mapping already, remove it from"
|
||||
+ " this variant's exclude() list", raw.name(), propName, meta.targetFullName());
|
||||
ok = false;
|
||||
} else if (isExcludable(property)) {
|
||||
result.add(propName);
|
||||
} else if (property.kind() == DtoPropertyMeta.Kind.SCALAR) {
|
||||
ctx.logError(raw.declaringElement(), "@DtoMapping(name = \"%s\") exclude(\"%s\") on target %s"
|
||||
+ " is a primitive property - there's no type-safe \"absent\" value for it, so it can't be"
|
||||
+ " excluded from a named variant", raw.name(), propName, meta.targetFullName());
|
||||
+ " not a nested ToOne/ToMany DTO property - only nested properties can be excluded (there's"
|
||||
+ " no type-safe \"absent\" value for an arbitrary scalar type)", raw.name(), propName, meta.targetFullName());
|
||||
ok = false;
|
||||
} else {
|
||||
ctx.logError(raw.declaringElement(), "@DtoMapping(name = \"%s\") exclude(\"%s\") on target %s is"
|
||||
+ " not a nested ToOne/ToMany DTO property or a non-primitive scalar - only those can be"
|
||||
+ " excluded (there's no type-safe \"absent\" value for an arbitrary scalar type)", raw.name(), propName, meta.targetFullName());
|
||||
ok = false;
|
||||
result.add(propName);
|
||||
}
|
||||
}
|
||||
return ok ? result : null;
|
||||
}
|
||||
|
||||
/** {@code true} if {@code property} can be excluded from a named variant - see {@link #resolveExcludedProperties}. */
|
||||
private boolean isExcludable(DtoPropertyMeta property) {
|
||||
if (property.kind() == DtoPropertyMeta.Kind.NESTED_ONE || property.kind() == DtoPropertyMeta.Kind.NESTED_MANY) {
|
||||
return true;
|
||||
}
|
||||
return property.kind() == DtoPropertyMeta.Kind.SCALAR && !property.isPrimitiveTarget();
|
||||
}
|
||||
|
||||
private DtoPropertyMeta findProperty(DtoBeanMeta meta, String name) {
|
||||
for (DtoPropertyMeta property : meta.properties()) {
|
||||
if (property.dtoFieldName().equals(name)) {
|
||||
@@ -461,111 +337,6 @@ class DtoMappingReader {
|
||||
return new DtoBuilderMeta(builderType.getQualifiedName().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide (and, if applicable, detect) the setter-based construction path for {@code meta} - see
|
||||
* {@code @DtoMapping(setter = ...)} and {@link DtoSetterMeta}. A no-op if a builder was already
|
||||
* selected for {@code meta} (see {@link #resolveBuilder}) - a builder always takes priority.
|
||||
* {@code NEVER} always uses a positional constructor. {@code ALWAYS} requires a matching
|
||||
* no-arg-constructor-plus-setters shape to be found (a codegen error otherwise). {@code AUTO}
|
||||
* (the default) only attempts detection when the target has no positional constructor matching
|
||||
* the mapped properties (arity-based - see {@link #hasPositionalConstructor}), silently falling
|
||||
* back to a positional constructor call if no matching setter shape is found either.
|
||||
*/
|
||||
private void resolveSetter(DtoBeanMeta meta, String setterMode) {
|
||||
if (meta.builderMeta() != null) {
|
||||
return;
|
||||
}
|
||||
String mode = (setterMode == null || setterMode.isEmpty()) ? "AUTO" : setterMode;
|
||||
if ("NEVER".equals(mode)) {
|
||||
return;
|
||||
}
|
||||
boolean required = "ALWAYS".equals(mode);
|
||||
if (!required && hasPositionalConstructor(meta.target(), meta.properties().size())) {
|
||||
return;
|
||||
}
|
||||
DtoSetterMeta setter = detectSetter(meta, required);
|
||||
if (setter != null) {
|
||||
meta.setterMeta(setter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect a plain mutable-JavaBean shape on {@code meta.target()}: a public no-arg constructor
|
||||
* plus a void {@code setXxx(...)} setter for every one of {@code meta}'s properties. Returns
|
||||
* {@code null} if any part of that shape is missing - logging a codegen error only when
|
||||
* {@code required} (i.e. {@code setter = ALWAYS}); silent otherwise (i.e. {@code setter = AUTO},
|
||||
* where a missing shape just means "use a positional constructor instead").
|
||||
*/
|
||||
private DtoSetterMeta detectSetter(DtoBeanMeta meta, boolean required) {
|
||||
TypeElement target = meta.target();
|
||||
if (!hasPublicNoArgConstructor(target)) {
|
||||
if (required) {
|
||||
ctx.logError(target, "@DtoMapping(setter = ALWAYS) on target %s but no public no-arg"
|
||||
+ " constructor was found", meta.targetFullName());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
for (DtoPropertyMeta property : meta.properties()) {
|
||||
if (findSetter(target, property.dtoFieldName()) == null) {
|
||||
if (required) {
|
||||
ctx.logError(target, "@DtoMapping(setter = ALWAYS) on target %s but no public"
|
||||
+ " \"%s(...)\" setter method was found", meta.targetFullName(), setterName(property.dtoFieldName()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return new DtoSetterMeta();
|
||||
}
|
||||
|
||||
private boolean hasPublicNoArgConstructor(TypeElement type) {
|
||||
for (ExecutableElement ctor : ElementFilter.constructorsIn(type.getEnclosedElements())) {
|
||||
if (ctor.getParameters().isEmpty() && ctor.getModifiers().contains(Modifier.PUBLIC)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** {@code true} if {@code type} has any declared constructor with exactly {@code arity} parameters - the shape a positional constructor call assumes. */
|
||||
private boolean hasPositionalConstructor(TypeElement type, int arity) {
|
||||
for (ExecutableElement ctor : ElementFilter.constructorsIn(type.getEnclosedElements())) {
|
||||
if (ctor.getParameters().size() == arity) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a public single-arg {@code setXxx(...)} method on {@code type} - either {@code void}
|
||||
* or fluent-style (returning {@code type} itself, e.g. {@code public Target setXxx(...) { ...;
|
||||
* return this; }}). Either shape is accepted since the generated code always calls the setter
|
||||
* as a bare statement and discards any return value.
|
||||
*/
|
||||
private ExecutableElement findSetter(TypeElement type, String propertyName) {
|
||||
String setterName = setterName(propertyName);
|
||||
for (ExecutableElement method : ElementFilter.methodsIn(type.getEnclosedElements())) {
|
||||
if (method.getSimpleName().contentEquals(setterName) && method.getParameters().size() == 1
|
||||
&& method.getModifiers().contains(Modifier.PUBLIC) && isVoidOrFluentReturn(method, type)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** {@code true} if {@code method} returns {@code void}, or returns {@code declaringType} itself (fluent-style setter). */
|
||||
private boolean isVoidOrFluentReturn(ExecutableElement method, TypeElement declaringType) {
|
||||
TypeMirror returnType = method.getReturnType();
|
||||
return returnType.getKind() == TypeKind.VOID
|
||||
|| (returnType.getKind() == TypeKind.DECLARED
|
||||
&& ctx.typeUtils().isSameType(ctx.typeUtils().erasure(returnType), ctx.typeUtils().erasure(declaringType.asType())));
|
||||
}
|
||||
|
||||
/** {@code referenceCode} -> {@code setReferenceCode} (standard JavaBean setter naming convention). */
|
||||
private String setterName(String propertyName) {
|
||||
return "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
|
||||
}
|
||||
|
||||
private ExecutableElement findStaticNoArgMethod(TypeElement type, String name) {
|
||||
for (ExecutableElement method : ElementFilter.methodsIn(type.getEnclosedElements())) {
|
||||
if (method.getSimpleName().contentEquals(name) && method.getModifiers().contains(Modifier.STATIC)
|
||||
@@ -622,10 +393,8 @@ class DtoMappingReader {
|
||||
return;
|
||||
}
|
||||
String mapperPackage = resolveMapperPackage(prism, targetType, declaringElement);
|
||||
String mapperName = prism.mapperName() == null ? "" : prism.mapperName().trim();
|
||||
byTargetName.put(targetFqn, new DtoBeanMeta(sourceType, targetType, mapperPackage, mapperName));
|
||||
byTargetName.put(targetFqn, new DtoBeanMeta(sourceType, targetType, mapperPackage));
|
||||
builderModeByTarget.put(targetFqn, prism.builder());
|
||||
setterModeByTarget.put(targetFqn, prism.setter());
|
||||
} else {
|
||||
variantsByTarget.computeIfAbsent(targetFqn, t -> new ArrayList<>())
|
||||
.add(new RawVariant(declaringElement, sourceType, name, prism.exclude()));
|
||||
@@ -673,217 +442,63 @@ class DtoMappingReader {
|
||||
|
||||
private DtoPropertyMeta resolveProperty(VariableElement field, DtoBeanMeta meta) {
|
||||
String name = field.getSimpleName().toString();
|
||||
DtoIgnorePrism ignorePrism = prismOn(field, meta, DtoIgnorePrism::getInstanceOn);
|
||||
DtoConverterMeta converter = resolveConverter(field, meta);
|
||||
DtoRefPrism refPrism = prismOn(field, meta, DtoRefPrism::getInstanceOn);
|
||||
DtoPathPrism pathPrism = prismOn(field, meta, DtoPathPrism::getInstanceOn);
|
||||
if (ignorePrism != null) {
|
||||
return resolveIgnoredProperty(field, meta, name, converter, refPrism, pathPrism);
|
||||
}
|
||||
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");
|
||||
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, false, false);
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.REF, List.of(assocGetter, idGetter), List.of(assocName, "id"), null, converter);
|
||||
}
|
||||
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;
|
||||
TypeElement lastOwnerType = null;
|
||||
String lastGetter = 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;
|
||||
}
|
||||
lastOwnerType = currentType;
|
||||
lastGetter = getter;
|
||||
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. 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().
|
||||
// derivation for NESTED_ONE/MANY only supports a single association name.
|
||||
if (properties.size() == 1) {
|
||||
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, getters, properties, nested,
|
||||
computedFrom >= 0, requiredFetchPaths);
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_MANY, getters, properties, nested);
|
||||
}
|
||||
} else {
|
||||
DtoBeanMeta nested = lookupByTarget(fieldType);
|
||||
if (nested != null) {
|
||||
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.NESTED_ONE, getters, properties, nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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).
|
||||
boolean isListTarget = listElementType(field.asType()) != null;
|
||||
DtoConverterMeta pathConverter = isListTarget ? converter
|
||||
: autoTypeConverter(converter, lastOwnerType != null ? getterReturnTypeMirror(lastOwnerType, lastGetter) : null, field.asType());
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, getters, properties, null, pathConverter,
|
||||
field.asType().getKind().isPrimitive(), pathPrism.failOnNull(), computedFrom >= 0, requiredFetchPaths,
|
||||
isListTarget, false);
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, getters, properties, null, converter);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
String getter = getterName(meta.source(), name);
|
||||
DtoConverterMeta scalarConverter = listElementType != null ? converter
|
||||
: autoTypeConverter(converter, getterReturnTypeMirror(meta.source(), getter), fieldType);
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, List.of(getter), List.of(name), null, scalarConverter,
|
||||
fieldType.getKind().isPrimitive(), listElementType != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a {@code @DtoIgnore} property - permanently excluded from every mapping (base and
|
||||
* every named variant alike), always given its empty default rather than resolved from any
|
||||
* source getter/path at all (see {@code DtoIgnore}'s javadoc). Validates it isn't combined with
|
||||
* {@code @DtoRef}/{@code @DtoPath}/{@code @DtoConvert} (all pointless - there's no source
|
||||
* expression to convert/redirect when nothing is ever read from the source) and isn't a
|
||||
* primitive-typed field (no type-safe "absent" value for it).
|
||||
*/
|
||||
private DtoPropertyMeta resolveIgnoredProperty(VariableElement field, DtoBeanMeta meta, String name,
|
||||
DtoConverterMeta converter, DtoRefPrism refPrism, DtoPathPrism pathPrism) {
|
||||
if (converter != null || refPrism != null || pathPrism != null) {
|
||||
ctx.logError(field, "@DtoIgnore on %s.%s cannot be combined with @DtoConvert/@DtoRef/@DtoPath -"
|
||||
+ " an @DtoIgnore property is never resolved from the source at all, so there's no source"
|
||||
+ " expression for those to act on. Remove @DtoIgnore or the other annotation(s).",
|
||||
meta.targetFullName(), name);
|
||||
}
|
||||
if (field.asType().getKind().isPrimitive()) {
|
||||
ctx.logError(field, "@DtoIgnore cannot be applied to primitive-typed property %s.%s - there's"
|
||||
+ " no type-safe \"absent\" value for it", meta.targetFullName(), name);
|
||||
}
|
||||
return DtoPropertyMeta.ignored(name, listElementType(field.asType()) != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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);
|
||||
}
|
||||
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, List.of(getterName(meta.source(), name)), List.of(name), null, converter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -929,54 +544,15 @@ class DtoMappingReader {
|
||||
return null;
|
||||
}
|
||||
String methodName = prism.method();
|
||||
ExecutableElement method = findConverterMethod(field, converterType, methodName);
|
||||
ExecutableElement method = findMethod(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)) {
|
||||
@@ -1039,29 +615,6 @@ 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
|
||||
@@ -1089,61 +642,16 @@ 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 method.getReturnType();
|
||||
return asTypeElement(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());
|
||||
}
|
||||
|
||||
+1
-142
@@ -25,83 +25,18 @@ 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;
|
||||
private final boolean listTarget;
|
||||
private final boolean ignored;
|
||||
|
||||
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath, DtoBeanMeta nested) {
|
||||
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code @DtoIgnore} factory - a property permanently excluded from every mapping (base and
|
||||
* every named variant alike), always given its empty default rather than resolved from any
|
||||
* source getter/path at all - see {@code DtoIgnore}'s javadoc. Kept as {@link Kind#SCALAR} with
|
||||
* empty getter/property paths since {@link DtoMapperWriter} short-circuits on {@link #isIgnored()}
|
||||
* before ever consulting them.
|
||||
*/
|
||||
static DtoPropertyMeta ignored(String dtoFieldName, boolean listTarget) {
|
||||
return new DtoPropertyMeta(dtoFieldName, Kind.SCALAR, List.of(), List.of(), null, null, false, false, false, List.of(), listTarget, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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, false, false);
|
||||
}
|
||||
|
||||
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath, DtoBeanMeta nested, DtoConverterMeta converter) {
|
||||
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, converter, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plain (no {@code @DtoPath}) {@link Kind#SCALAR} constructor variant that also records whether
|
||||
* the DTO field type is a {@code java.util.List} - see {@code listTarget} on the full
|
||||
* constructor.
|
||||
*/
|
||||
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath,
|
||||
DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean listTarget) {
|
||||
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, converter, primitiveTarget, false, false, List.of(), listTarget, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. {@code listTarget} is {@code true} when this property's DTO
|
||||
* field type is a {@code java.util.List} - relevant only for {@link Kind#SCALAR} (e.g. a
|
||||
* {@code @DtoConvert}-backed {@code List} property with no registered nested DTO mapping of its
|
||||
* own, like a fleet list populated from ad-hoc SQL) - see
|
||||
* {@code DtoMapperWriter#defaultValueFor}.
|
||||
*/
|
||||
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath,
|
||||
DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean failOnNull,
|
||||
boolean computedSegment, List<String> requiredFetchPaths, boolean listTarget, boolean ignored) {
|
||||
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;
|
||||
this.listTarget = listTarget;
|
||||
this.ignored = ignored;
|
||||
}
|
||||
|
||||
String dtoFieldName() {
|
||||
@@ -144,70 +79,11 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code true} when this property's DTO field type is a Java primitive - a primitive property
|
||||
* has no type-safe "absent" value, so can never be excluded by a named
|
||||
* {@code @DtoMapping(name = ..., exclude = ...)} variant.
|
||||
*/
|
||||
boolean isPrimitiveTarget() {
|
||||
return primitiveTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code true} when this property's DTO field type is a {@code java.util.List} - set for
|
||||
* {@link Kind#NESTED_MANY} (always list-shaped) and also for a {@link Kind#SCALAR}/
|
||||
* {@code @DtoConvert}-backed property whose DTO field happens to be a {@code List} with no
|
||||
* registered nested DTO mapping of its own (e.g. a fleet list populated from ad-hoc SQL) -
|
||||
* used to pick {@code List.of()} rather than {@code null} as the excluded/empty default for a
|
||||
* named variant, see {@code DtoMapperWriter#defaultValueFor}.
|
||||
*/
|
||||
boolean isListTarget() {
|
||||
return listTarget || kind == Kind.NESTED_MANY;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code true} when this property is marked {@code @DtoIgnore} - permanently excluded from
|
||||
* every mapping (base and every named variant alike), always given its empty default rather
|
||||
* than resolved from any source getter/path.
|
||||
*/
|
||||
boolean isIgnored() {
|
||||
return ignored;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
@@ -215,23 +91,7 @@ class DtoPropertyMeta {
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
appendGuardedChain(sb, rootVariable, 0);
|
||||
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;
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void appendGuardedChain(StringBuilder sb, String prefix, int index) {
|
||||
@@ -245,4 +105,3 @@ class DtoPropertyMeta {
|
||||
sb.append(')');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package io.ebean.querybean.generator;
|
||||
|
||||
/**
|
||||
* Marks a detected setter-based (mutable JavaBean) construction path for a {@link DtoBeanMeta}'s
|
||||
* target - see {@code @DtoMapping(setter = ...)}.
|
||||
* <p>
|
||||
* Detected via the plain JavaBean convention (the shape JAXB/XSD-generated legacy SOAP types
|
||||
* commonly follow): a public no-arg constructor plus a public {@code setXxx(propertyType)} setter
|
||||
* for every one of the target's mapped properties - either {@code void} or fluent-style
|
||||
* (returning the target type itself, e.g. {@code public Target setXxx(...) { ...; return this; }})
|
||||
* are both accepted, since the generated code always calls the setter as a bare statement and
|
||||
* discards any return value. When present (and either explicitly
|
||||
* requested via {@code setter = ALWAYS}, or no builder was selected and the target has no
|
||||
* positional constructor matching the mapped properties under {@code setter = AUTO}), the
|
||||
* generated mapper constructs the target via {@code Target target = new Target();
|
||||
* target.setX(...); ...; return target;} instead of a positional constructor call or a builder
|
||||
* chain.
|
||||
* <p>
|
||||
* A pure marker (mirrors {@link DtoBuilderMeta}) - unlike a builder, a setter-constructed target
|
||||
* needs no separate intermediate type name, since the setters are called directly on the target
|
||||
* type itself.
|
||||
* <p>
|
||||
* Deliberately no {@code mapToBuilder(...)}-style post-construction accessor is generated for this
|
||||
* strategy: the returned target is already the final, fully mutable instance (its setters are
|
||||
* required to be {@code public} - see above), so a caller can already override an
|
||||
* {@code @DtoIgnore}/derived property directly, e.g.
|
||||
* {@code Ebox ebox = mapper.map(source); ebox.setMachineSummaryInfo(loadSummary(source));} -
|
||||
* exactly the pattern already used by hand-written mappers like central-access's
|
||||
* {@code EboxMapper}. This is unlike the builder strategy, where the intermediate builder object
|
||||
* is distinct from (and otherwise inaccessible after) the final, one-shot-{@code build()}ed,
|
||||
* often-immutable target - {@code mapToBuilder(...)} exists there specifically to expose that
|
||||
* otherwise-unreachable intermediate step.
|
||||
*/
|
||||
final class DtoSetterMeta {
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user