mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 11:17:36 +00:00
Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56894400f4 | ||
|
|
32931d6db0 | ||
|
|
73c9ff8d80 | ||
|
|
bdbe743f56 | ||
|
|
ed2026b858 | ||
|
|
8d21f91f62 | ||
|
|
6d1d58b8c2 | ||
|
|
a943ee9225 | ||
|
|
94f252f423 | ||
|
|
275f8ad9f9 | ||
|
|
9ac3bd71f6 | ||
|
|
bb46c88db1 | ||
|
|
28e6108315 | ||
|
|
c20d1cdf9b | ||
|
|
007409d21c | ||
|
|
8b61069b3f | ||
|
|
a82dcb2509 | ||
|
|
08bb170cb2 | ||
|
|
a2f954a60e | ||
|
|
1d654e350b | ||
|
|
6dd1763e54 | ||
|
|
b32f3bcad6 | ||
|
|
e525d4e159 | ||
|
|
4e639cb88a | ||
|
|
fc78b2af80 | ||
|
|
29647ebe1a | ||
|
|
38146fdce5 | ||
|
|
864aff6c3b | ||
|
|
a161a42742 | ||
|
|
24da374aa8 | ||
|
|
398848378d | ||
|
|
688eee532c | ||
|
|
88d7b5e85a | ||
|
|
985e3b12b1 | ||
|
|
942bab5efb | ||
|
|
c114ebaec5 | ||
|
|
9acc555e8e | ||
|
|
73a646e7dc | ||
|
|
bdda502d55 | ||
|
|
6d447a6c1a | ||
|
|
16872fdb30 | ||
|
|
ec9303762e | ||
|
|
a59c70054b | ||
|
|
25bce83afc | ||
|
|
e372579b15 | ||
|
|
f470782481 | ||
|
|
d7b9f6688a | ||
|
|
66bcfd107d | ||
|
|
1c0a811c01 |
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<properties>
|
||||
<postgis.jdbc.version>2023.1.0</postgis.jdbc.version>
|
||||
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
|
||||
<postgres.jdbc.version>42.7.11</postgres.jdbc.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-net-postgis-types</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -60,13 +60,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>composites</artifactId>
|
||||
|
||||
@@ -48,6 +48,7 @@ existing Maven project. Complete the steps in order.
|
||||
|-------|-------------|
|
||||
| [Write Ebean queries with query beans](writing-ebean-query-beans.md) | Step-by-step guidance for AI agents to write type-safe Ebean queries; choose the right terminal method; tune `select()` / `fetch()` / `fetchQuery()`; and project to DTOs when entity beans are not the right output |
|
||||
| [Immutable bean cache for read-only references](immutable-bean-cache.md) | Use `ImmutableBeanCache` and `ImmutableBeanCaches.loading(...)` to resolve assoc-one references in read-only/unmodifiable queries, including secondary `fetchQuery`/`fetchLazy` loads |
|
||||
| [Using `RawSql` with Ebean](using-rawsql-with-ebean.md) | Choose between `RawSqlBuilder.parse()`, `unparsed()`, and `withPlaceholders()`; the `${where}`/`${andWhere}`/`${having}`/`${andHaving}` placeholder reference for CTEs, window functions, and subqueries; column mapping; and using `RawSql` with query beans |
|
||||
|
||||
## Persisting & transactions
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ Inside the `<dependencies>` block, add the PostgreSQL JDBC driver:
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.8</version>
|
||||
<version>42.7.11</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
# Guide: Using `RawSql` with Ebean
|
||||
|
||||
## Purpose
|
||||
|
||||
`RawSql` lets you back an Ebean bean with a **hand-written SQL query** instead of
|
||||
Ebean generating the SQL from the entity mapping. Ebean still handles object
|
||||
mapping (result set columns → bean properties), lazy loading of associated beans,
|
||||
and - depending on how the `RawSql` is built - dynamic `WHERE`/`HAVING` predicates
|
||||
added through the normal query API.
|
||||
|
||||
Use this guide when you need to:
|
||||
|
||||
- run vendor-specific SQL, complex aggregation, or reporting queries that don't
|
||||
map cleanly to an ORM query
|
||||
- reuse a hand-tuned query but still want typed/dynamic predicates, paging, or
|
||||
`ORDER BY` added by the caller
|
||||
- back a query bean (`Q*`) or DTO-like bean with SQL containing a CTE, window
|
||||
function, or subquery in the `FROM` clause
|
||||
|
||||
Prefer ordinary query bean queries first - see
|
||||
[Write Ebean queries with query beans](writing-ebean-query-beans.md), Step 9,
|
||||
for the full decision order (query bean → `asDto()` → DTO query → raw SQL).
|
||||
This guide covers raw SQL once you've decided it's the right tool.
|
||||
|
||||
---
|
||||
|
||||
## The bean behind a `RawSql` query
|
||||
|
||||
A bean queried with `RawSql` is not necessarily backed by a physical table. Annotate
|
||||
it `@Entity @Sql` to tell Ebean it is mapped via `RawSql` rather than table DDL:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Sql
|
||||
public class OrderAggregate {
|
||||
|
||||
@OneToOne
|
||||
Order order;
|
||||
|
||||
Double totalAmount;
|
||||
Long totalItems;
|
||||
|
||||
// getters/setters
|
||||
}
|
||||
```
|
||||
|
||||
`@Sql` beans still get a generated query bean (`QOrderAggregate`) if the
|
||||
querybean-generator annotation processor is configured - see
|
||||
[Using `RawSql` with query beans](#using-rawsql-with-query-beans) below.
|
||||
|
||||
You can also query an ordinary table-backed `@Entity` with `RawSql` - the column
|
||||
mapping just needs to line up with that entity's properties.
|
||||
|
||||
---
|
||||
|
||||
## Building a `RawSql` - three factory methods
|
||||
|
||||
`RawSqlBuilder` has three ways to construct a `RawSql`, depending on how much of
|
||||
the SQL Ebean needs to understand:
|
||||
|
||||
| Method | SELECT columns parsed? | Dynamic WHERE/HAVING/ORDER BY? | Use for |
|
||||
|--------|------------------------|------------------------|---------|
|
||||
| `RawSqlBuilder.parse(sql)` | Yes | Yes | Ordinary `SELECT ... FROM ... WHERE ...` statements |
|
||||
| `RawSqlBuilder.unparsed(sql)` | No | No | Fixed SQL that never needs additional predicates |
|
||||
| `RawSqlBuilder.withPlaceholders(sql)` | No (explicit `columnMapping()` required) | Yes, via `${where}` / `${andWhere}` / `${having}` / `${andHaving}` / `${orderBy}` / `${andOrderBy}` | CTEs, window functions, subqueries - SQL that keyword-based parsing can't handle |
|
||||
|
||||
### `parse(sql)` - the common case
|
||||
|
||||
`parse(sql)` scans the SQL text for the `select` / `from` / `where` / `group by`
|
||||
/ `having` / `order by` keywords to work out the SELECT column list (so it can
|
||||
validate your column mappings) and the injection points for dynamic `WHERE`/
|
||||
`HAVING` expressions.
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.parse(
|
||||
"select c.id, c.name, c.status from customer c")
|
||||
.columnMapping("c.id", "id")
|
||||
.columnMapping("c.name", "name")
|
||||
.columnMapping("c.status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().eq("status", Customer.Status.ACTIVE)
|
||||
.orderBy("name")
|
||||
.findList();
|
||||
```
|
||||
|
||||
Because the SQL is parsed, mistakes in `columnMapping()` (unknown column, wrong
|
||||
order for `unparsed`-style mappings) are caught early. **This fails on SQL the
|
||||
keyword parser can't make sense of** - a `WITH` CTE, a window function, a
|
||||
subquery in `FROM`, etc. - because the keyword positions found don't correspond
|
||||
to the outer query's real structure. Use `withPlaceholders(sql)` for that SQL
|
||||
instead (see below).
|
||||
|
||||
### `unparsed(sql)` - fixed queries
|
||||
|
||||
`unparsed(sql)` skips all parsing. The SQL is used exactly as written, and **no
|
||||
further `WHERE`/`HAVING`/`ORDER BY` can be added** by the caller - useful for a
|
||||
completely fixed reporting query with no caller-supplied filtering.
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.unparsed(
|
||||
"select id, name, status from customer where status = 'ACTIVE'")
|
||||
.columnMapping("id", "id")
|
||||
.columnMapping("name", "name")
|
||||
.columnMapping("status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setRawSql(rawSql)
|
||||
.findList();
|
||||
```
|
||||
|
||||
Column mappings for `unparsed(sql)` must be supplied **in the same order** as
|
||||
the columns appear in the SQL, since there's no parsing to match them by name.
|
||||
|
||||
### `withPlaceholders(sql)` - complex SQL (CTEs, window functions, subqueries)
|
||||
|
||||
`withPlaceholders(sql)` avoids keyword scanning entirely. You mark exactly where
|
||||
a dynamic `WHERE`/`HAVING`/`ORDER BY` expression should be injected using
|
||||
placeholder tokens, and column mappings are always explicit (as with `unparsed`).
|
||||
|
||||
#### Placeholder reference
|
||||
|
||||
| Placeholder | Meaning | Use when |
|
||||
|-------------|---------|----------|
|
||||
| `${where}` | Insert a new `WHERE <expr>` clause here | No static `WHERE` clause exists yet at this point in the SQL |
|
||||
| `${andWhere}` | Insert `AND <expr>` here | A static `WHERE ...` clause already exists in the SQL and you want to append to it |
|
||||
| `${having}` | Insert a new `HAVING <expr>` clause here | No static `HAVING` clause exists yet at this point in the SQL |
|
||||
| `${andHaving}` | Insert `AND <expr>` here | A static `HAVING ...` clause already exists in the SQL and you want to append to it |
|
||||
| `${orderBy}` | Insert a new `ORDER BY <expr>` clause here | No static `ORDER BY` clause exists yet at this point in the SQL, and callers may supply `.orderBy(...)` |
|
||||
| `${andOrderBy}` | Insert `, <expr>` here | A static `ORDER BY ...` clause already exists in the SQL and you want callers to be able to append extra sort columns to it |
|
||||
|
||||
Rules:
|
||||
|
||||
- At least one placeholder is required - `withPlaceholders(sql)` throws
|
||||
`IllegalArgumentException` if none of the six tokens are present.
|
||||
- Use only the placeholders you need. Omit `${where}`/`${andWhere}` entirely if
|
||||
the query never needs a dynamic `WHERE` (e.g. only a dynamic `HAVING` on an
|
||||
aggregate). Omit `${having}`/`${andHaving}` if there's no dynamic `HAVING`.
|
||||
Omit `${orderBy}`/`${andOrderBy}` if the ordering is always fixed.
|
||||
- Explicit `columnMapping()` is required for every returned column - there is no
|
||||
column-list parsing to infer names from.
|
||||
- **A caller-supplied `.orderBy(...)`/`.order(...)` is only applied if the SQL
|
||||
contains an `${orderBy}` or `${andOrderBy}` placeholder.** Without one of
|
||||
those placeholders there is no defined injection point for dynamic ordering,
|
||||
so any `.orderBy(...)` call on the query is safely ignored rather than risk
|
||||
producing invalid SQL - even if the template has a static trailing
|
||||
`ORDER BY ...` of its own. If you need callers to be able to influence
|
||||
ordering, add `${orderBy}` (no existing static order by) or `${andOrderBy}`
|
||||
(append after an existing static order by).
|
||||
- Any other static SQL that follows a `${where}`/`${having}` placeholder (e.g.
|
||||
a trailing `GROUP BY`) is preserved and correctly positioned **after**
|
||||
whatever dynamic expression gets injected at that placeholder.
|
||||
|
||||
#### Example - CTE with `${where}`
|
||||
|
||||
```java
|
||||
String sql = """
|
||||
with order_totals as (
|
||||
select o.id as order_id, sum(d.qty * d.unit_price) as total_amount
|
||||
from o_order o
|
||||
join o_order_detail d on d.order_id = o.id
|
||||
group by o.id
|
||||
)
|
||||
select order_id, total_amount
|
||||
from order_totals
|
||||
${where}
|
||||
order by order_id
|
||||
""";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
`total_amount` is a genuine column of the `order_totals` CTE here, so it's valid
|
||||
to filter on it in the outer `WHERE` - this only works because the aggregate is
|
||||
computed inside the CTE rather than as a same-level `SELECT` alias.
|
||||
|
||||
#### Example - static `WHERE` already present, append with `${andWhere}`
|
||||
|
||||
```java
|
||||
String sql = "... from order_totals where total_amount > 0 ${andWhere} order by order_id";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
// executed SQL: ... where total_amount > 0 and total_amount > ? order by order_id
|
||||
DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
#### Example - `${having}` only, filtering on an aggregate directly
|
||||
|
||||
No `WHERE` placeholder is needed if you only ever filter on the aggregate value:
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
|
||||
" from o_order o join o_order_detail d on d.order_id = o.id" +
|
||||
" group by o.id" +
|
||||
" ${having}" +
|
||||
" order by order_id";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.having().gt("totalAmount", 100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
The dynamic `HAVING` clause is injected before the static trailing `ORDER BY`,
|
||||
even though `${having}` is the only placeholder present. Because there's no
|
||||
`${orderBy}`/`${andOrderBy}` placeholder here, a caller-supplied `.orderBy(...)`
|
||||
would be ignored - the ordering stays fixed as `order by order_id`.
|
||||
|
||||
#### Example - both `${where}` and `${having}`
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
|
||||
" from o_order o join o_order_detail d on d.order_id = o.id" +
|
||||
" ${where}" +
|
||||
" group by o.id" +
|
||||
" ${having}" +
|
||||
" order by order_id";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("order.id", 0)
|
||||
.having().gt("totalAmount", 50)
|
||||
.findList();
|
||||
```
|
||||
|
||||
Both the dynamic `WHERE` and dynamic `HAVING` are injected at their respective
|
||||
placeholders, and the trailing `order by order_id` is preserved after the
|
||||
`HAVING` clause.
|
||||
|
||||
#### Example - `${orderBy}`, fully dynamic ordering
|
||||
|
||||
Use `${orderBy}` when there's no static default ordering and you want the
|
||||
caller's `.orderBy(...)` to control it entirely:
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"with order_totals as (" +
|
||||
" select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
|
||||
" from o_order o join o_order_detail d on d.order_id = o.id" +
|
||||
" group by o.id" +
|
||||
")" +
|
||||
" select order_id, total_amount from order_totals" +
|
||||
" ${where}" +
|
||||
" ${orderBy}";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
// executed SQL: ... where total_amount > ? order by total_amount desc
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 0)
|
||||
.orderBy("totalAmount desc")
|
||||
.findList();
|
||||
```
|
||||
|
||||
If the caller doesn't call `.orderBy(...)`, nothing is injected at `${orderBy}`
|
||||
and no `ORDER BY` clause is emitted at all.
|
||||
|
||||
#### Example - `${andOrderBy}`, appending to a static default ordering
|
||||
|
||||
Use `${andOrderBy}` when there's a sensible static default ordering but you
|
||||
want callers to be able to add extra tie-breaker sort columns:
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"... from order_totals" +
|
||||
" ${where}" +
|
||||
" order by total_amount desc ${andOrderBy}";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
// executed SQL: ... order by total_amount desc , order_id
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 0)
|
||||
.orderBy("order.id")
|
||||
.findList();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using `fetchQuery()` to build out more of the graph
|
||||
|
||||
A `RawSql` query can be the **root query** and still use `fetchQuery(path)` the
|
||||
same way an ordinary ORM query does - Ebean runs the raw SQL for the root rows,
|
||||
then runs additional secondary ORM queries to populate the requested paths. This
|
||||
lets you hand-write only the part of the query that needs raw SQL (e.g. an
|
||||
aggregate/CTE) and let the ORM build out the rest of the object graph normally.
|
||||
|
||||
```java
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql) // root query - runs the CTE/aggregate SQL
|
||||
.fetchQuery("order") // secondary query - loads the full Order
|
||||
.fetchQuery("order.details") // secondary query - loads Order.details
|
||||
.where().gt("totalAmount", 50)
|
||||
.findList();
|
||||
```
|
||||
|
||||
This executes **three** queries: the raw SQL root query, then one secondary
|
||||
query per `fetchQuery(path)` call.
|
||||
|
||||
**Important**: if the raw SQL's column mapping only populates part of an
|
||||
association (e.g. only `order.id`, as in the examples above), that association
|
||||
is a *partial reference*. To load a nested to-many under it (e.g.
|
||||
`order.details`), you must add an explicit `fetchQuery(...)` (or `fetch(...)`)
|
||||
for the **intermediate path** (`order`) as well as the nested path
|
||||
(`order.details`) - `fetchQuery("order.details")` alone will leave `details` as
|
||||
a deferred/lazy collection, because Ebean doesn't otherwise have a fetch node
|
||||
for `order` to hang the secondary query off. If the raw SQL already selects the
|
||||
full set of columns for an association directly (no partial reference), this
|
||||
extra step isn't needed.
|
||||
|
||||
This is the same `fetchQuery()` mechanism used for ordinary query bean queries -
|
||||
see [Use `fetchQuery()` for to-many paths](writing-ebean-query-beans.md#step-7---use-fetchquery-for-to-many-paths-and-fetchgroup-for-reusable-query-shapes)
|
||||
for background on why to-many paths are loaded via secondary queries rather than
|
||||
a single joined query.
|
||||
|
||||
---
|
||||
|
||||
## Column mapping
|
||||
|
||||
Every `RawSqlBuilder` (except a bare `unparsed(sql)` with implicit positional
|
||||
mapping) uses `columnMapping(dbColumn, propertyName)` to map SQL result columns
|
||||
to bean properties:
|
||||
|
||||
```java
|
||||
.columnMapping("order_id", "order.id") // maps to the "order" association's "id" property
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
```
|
||||
|
||||
- Dotted property paths (e.g. `"order.id"`) map a column into a nested/associated
|
||||
bean property.
|
||||
- `columnMappingIgnore(dbColumn)` marks a selected column as intentionally unmapped
|
||||
(present in the SQL but not needed on the bean).
|
||||
- `tableAliasMapping(tableAlias, path)` bulk-renames every mapping using a given
|
||||
SQL table alias to be prefixed with a bean property path - handy when a `parse()`
|
||||
query selects many columns from a joined table (e.g. alias `c` → path `customer`)
|
||||
and you don't want to repeat the prefix in every `columnMapping()` call.
|
||||
|
||||
---
|
||||
|
||||
## Using `RawSql` with query beans
|
||||
|
||||
`RawSql` is not limited to the plain `Query<T>` API - it also works with a
|
||||
generated query bean, giving type-safe `where()`/`having()`-equivalent
|
||||
expressions (as bean properties) over hand-written SQL. Every generated query
|
||||
bean exposes `setRawSql(...)`:
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.parse("select id, name, status from customer")
|
||||
.columnMapping("id", "id")
|
||||
.columnMapping("name", "name")
|
||||
.columnMapping("status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.setRawSql(rawSql)
|
||||
.status.equalTo(Customer.Status.ACTIVE) // typed expression, injected into the parsed WHERE clause
|
||||
.findList();
|
||||
```
|
||||
|
||||
This also works with `withPlaceholders(sql)` and an `@Sql` query bean:
|
||||
|
||||
```java
|
||||
List<OrderAggregate> list = new QOrderAggregate()
|
||||
.setRawSql(rawSql) // built with withPlaceholders() as shown above
|
||||
.totalAmount.gt(100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
The typed property expression (`.totalAmount.gt(100)`) is translated to a bound
|
||||
predicate and injected at the `${where}`/`${having}` placeholder position, exactly
|
||||
as `.where().gt("totalAmount", 100)` would be on the plain `Query<T>` API.
|
||||
|
||||
---
|
||||
|
||||
## Common anti-patterns
|
||||
|
||||
### Anti-pattern 1 - reaching for raw SQL before trying a query bean
|
||||
|
||||
Complex-looking joins are often just ordinary association traversal in a query
|
||||
bean. Don't use raw SQL just because a query touches several tables - see
|
||||
[Write Ebean queries with query beans](writing-ebean-query-beans.md).
|
||||
|
||||
### Anti-pattern 2 - using `parse(sql)` on a CTE or window-function query
|
||||
|
||||
`parse(sql)` will throw a parsing exception (or silently mis-locate the WHERE
|
||||
injection point) on SQL it can't understand structurally. If your SQL starts
|
||||
with `WITH ...` or has a subquery in `FROM`, use `withPlaceholders(sql)` instead.
|
||||
|
||||
### Anti-pattern 3 - filtering on a same-level SELECT alias
|
||||
|
||||
You cannot add a dynamic `WHERE` predicate on a `SELECT`-clause alias in the
|
||||
same query level (e.g. `select sum(x) as total ... ${where}` - `total` isn't a
|
||||
real column yet at the `WHERE` stage of that query level). Either:
|
||||
|
||||
- move the aggregation into a CTE and filter on the CTE's output column in the
|
||||
outer query (`WHERE` case), or
|
||||
- use `${having}`/`${andHaving}` to filter on the aggregate at the `HAVING` stage
|
||||
of the same query level, where the aggregate expression is valid.
|
||||
|
||||
### Anti-pattern 4 - forgetting `columnMapping()` with `unparsed()`/`withPlaceholders()`
|
||||
|
||||
Both `unparsed(sql)` and `withPlaceholders(sql)` require **every** returned
|
||||
column to be explicitly mapped (or explicitly ignored via
|
||||
`columnMappingIgnore(...)`) - there's no column-list parsing to infer them.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| `RuntimeException: Error parsing sql, can not find ... keyword` | `parse(sql)` used on SQL with a CTE, window function, or subquery in `FROM` | Use `RawSqlBuilder.withPlaceholders(sql)` instead |
|
||||
| `IllegalArgumentException: withPlaceholders() requires at least one of ${where}, ${andWhere}, ${having}, ${andHaving}, ${orderBy}, ${andOrderBy}...` | None of the six placeholder tokens were found in the SQL | Add the appropriate placeholder token at the injection point |
|
||||
| Dynamic `WHERE`/`HAVING` predicate silently has no effect, or query throws | Used `unparsed(sql)` and then tried to add a predicate | `unparsed(sql)` queries cannot be modified - switch to `parse(sql)` or `withPlaceholders(sql)` |
|
||||
| Generated SQL is invalid / clauses appear in the wrong order | Predicates added via `.where()`/`.having()` don't match the placeholders actually present in the SQL | Make sure `${where}`/`${having}` (or the `and` variants) exist at the point you expect predicates to be injected |
|
||||
| `.orderBy(...)`/`.order(...)` on the query silently has no effect | The SQL has no `${orderBy}`/`${andOrderBy}` placeholder | This is by design - without one of those placeholders there's no defined injection point, so the ordering is ignored rather than corrupting the SQL. Add `${orderBy}` or `${andOrderBy}` if you need caller-controlled ordering |
|
||||
| `Unknown column` / unmapped property error | Missing `columnMapping()` for a selected column | Add a `columnMapping(...)` or `columnMappingIgnore(...)` for every SQL column |
|
||||
| `fetchQuery("a.b")` collection stays deferred/lazy | `a` is a partial reference from the raw SQL column mapping (e.g. only `a.id` mapped), and there's no fetch node for `a` itself | Add `fetchQuery("a")` (or `fetch("a")`) alongside `fetchQuery("a.b")` |
|
||||
|
||||
---
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Write Ebean queries with query beans](writing-ebean-query-beans.md)
|
||||
- [Derived / formula properties (`@Formula`, `@Formula2`)](derived-formula-properties.md)
|
||||
- [Ebean query docs](https://ebean.io/docs/query/)
|
||||
@@ -483,6 +483,30 @@ Prefer the following order:
|
||||
Do **not** jump to raw SQL just because the query joins multiple tables. Query
|
||||
beans already handle ordinary relationship traversal well.
|
||||
|
||||
### Using `RawSql` with query beans
|
||||
|
||||
`RawSql` is not limited to the plain `Query<T>` API - it also works with a
|
||||
generated query bean, giving type-safe `where()`/`having()` expressions over
|
||||
hand-written SQL. Every generated query bean exposes `setRawSql(...)`:
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.parse("select id, name, status from customer")
|
||||
.columnMapping("id", "id")
|
||||
.columnMapping("name", "name")
|
||||
.columnMapping("status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.setRawSql(rawSql)
|
||||
.status.equalTo(Customer.Status.ACTIVE) // typed expression, injected into the parsed WHERE clause
|
||||
.findList();
|
||||
```
|
||||
|
||||
For the full guide to building `RawSql` - including `unparsed()`,
|
||||
`withPlaceholders()` for CTEs/window functions, the `${where}` / `${andWhere}`
|
||||
/ `${having}` / `${andHaving}` placeholder reference, and column mapping - see
|
||||
[Using `RawSql` with Ebean](using-rawsql-with-ebean.md).
|
||||
|
||||
---
|
||||
|
||||
## Common anti-patterns
|
||||
@@ -570,4 +594,5 @@ When asked to add or modify an Ebean query:
|
||||
- [Add Ebean Postgres Maven POM](add-ebean-postgres-maven-pom.md)
|
||||
- [Entity Bean Creation](entity-bean-creation.md)
|
||||
- [Immutable bean cache for read-only references](immutable-bean-cache.md)
|
||||
- [Using `RawSql` with Ebean](using-rawsql-with-ebean.md)
|
||||
- [Ebean query docs](https://ebean.io/docs/query/)
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean api</name>
|
||||
|
||||
@@ -457,6 +457,10 @@ public final class DB {
|
||||
|
||||
/**
|
||||
* Same as {@link #checkUniqueness(Object)} but with given transaction.
|
||||
* <p>
|
||||
* For control over query cache use and whether to skip the check when the bean's unique
|
||||
* properties are unchanged, use {@link Database#checkUniqueness(Object, Transaction, boolean, boolean)}
|
||||
* via {@link #getDefault()} instead.
|
||||
*/
|
||||
public static Set<Property> checkUniqueness(Object bean, Transaction transaction) {
|
||||
return getDefault().checkUniqueness(bean, transaction);
|
||||
|
||||
@@ -1078,12 +1078,21 @@ public interface Database {
|
||||
* @param bean The entity bean to check uniqueness on
|
||||
* @return a set of Properties if constraint validation was detected or empty list.
|
||||
*/
|
||||
Set<Property> checkUniqueness(Object bean);
|
||||
default Set<Property> checkUniqueness(Object bean) {
|
||||
return checkUniqueness(bean, null, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #checkUniqueness(Object)}. but with given transaction.
|
||||
*/
|
||||
Set<Property> checkUniqueness(Object bean, Transaction transaction);
|
||||
default Set<Property> checkUniqueness(Object bean, Transaction transaction) {
|
||||
return checkUniqueness(bean, transaction, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #checkUniqueness(Object)}. but with given transaction and extended search options.
|
||||
*/
|
||||
Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean);
|
||||
|
||||
/**
|
||||
* Marks the entity bean as dirty.
|
||||
|
||||
@@ -62,9 +62,10 @@ public interface DatabaseBuilder {
|
||||
/**
|
||||
* Build and return the Database instance.
|
||||
* <p>
|
||||
* When {@link #setRegister(boolean)} is set to true, and a database with the same
|
||||
* name is already registered, this may return the existing registered database
|
||||
* rather than creating a new one.
|
||||
* When {@link #setRegister(boolean)} is set to true (the default), and a database
|
||||
* with the same name is already registered, this throws an {@link IllegalStateException}.
|
||||
* Use a unique name, or use {@link #setRegister(boolean)} with {@code false} if the
|
||||
* Database instance is not intended to be registered/looked up by name.
|
||||
*/
|
||||
Database build();
|
||||
|
||||
@@ -887,6 +888,13 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setBackgroundExecutorWrapper(BackgroundExecutorWrapper backgroundExecutorWrapper);
|
||||
|
||||
/**
|
||||
* Enable tenant-partitioned caches. When enabled each tenant gets its own cache namespace,
|
||||
* improving cache-hit ratio by preventing cross-tenant key collisions.
|
||||
* Use {@link SpiCacheManager#clearTenant(Object)} when a tenant is deactivated.
|
||||
*/
|
||||
DatabaseBuilder tenantPartitionedCache(boolean tenantPartitionedCache);
|
||||
|
||||
/**
|
||||
* Set the L2 cache default max size.
|
||||
*/
|
||||
@@ -2567,6 +2575,11 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
boolean isAutoPersistUpdates();
|
||||
|
||||
/**
|
||||
* Return true if caches are partitioned by tenant.
|
||||
*/
|
||||
boolean isTenantPartitionedCache();
|
||||
|
||||
/**
|
||||
* Return the L2 cache default max size.
|
||||
*/
|
||||
|
||||
@@ -7,8 +7,6 @@ import jakarta.persistence.PersistenceException;
|
||||
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
|
||||
/**
|
||||
* Low-level factory for creating {@link Database} instances.
|
||||
* <p>
|
||||
@@ -81,10 +79,10 @@ public final class DatabaseFactory {
|
||||
// We're explicitly creating a database to be registered, so avoid
|
||||
// triggering DbContext static initialisation to auto-create a default one.
|
||||
DbPrimary.setSkip(true);
|
||||
Database existing = DbContext.getInstance().getRegistered(name);
|
||||
if (existing != null) {
|
||||
EbeanVersion.log.log(WARNING, "Using existing database with name:{0}", name);
|
||||
return existing;
|
||||
if (DbContext.getInstance().contains(name)) {
|
||||
throw new IllegalStateException("A Database with name [" + name + "] is already registered."
|
||||
+ " Use a unique DatabaseConfig name, or set DatabaseConfig.setRegister(false)"
|
||||
+ " if this Database instance is not intended to be registered/looked up by name.");
|
||||
}
|
||||
}
|
||||
Database server = createInternal(config);
|
||||
@@ -123,6 +121,24 @@ public final class DatabaseFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the registration of this Database.
|
||||
* <p>
|
||||
* This is invoked when a Database is shutdown so that its registered name
|
||||
* becomes available again for a subsequently created Database with the same name.
|
||||
*/
|
||||
public static void unregister(Database server) {
|
||||
lock.lock();
|
||||
try {
|
||||
DbContext.getInstance().deregister(server);
|
||||
if (server.name().equals(defaultServerName)) {
|
||||
defaultServerName = null;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown gracefully all Database instances cleaning up any resources as required.
|
||||
* <p>
|
||||
|
||||
@@ -4,7 +4,6 @@ import io.ebean.config.BeanNotEnhancedException;
|
||||
import io.ebean.datasource.DataSourceConfigurationException;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -77,9 +76,8 @@ final class DbContext {
|
||||
return defaultDatabase;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Database getRegistered(String name) {
|
||||
return concMap.get(name);
|
||||
boolean contains(String name) {
|
||||
return concMap.containsKey(name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,6 +120,27 @@ final class DbContext {
|
||||
registerWithName(server.name(), server, isDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the registration for this Database (typically on shutdown) so that
|
||||
* its name becomes available again for a subsequently created Database.
|
||||
* <p>
|
||||
* Only removes the registration if it currently maps to this exact instance
|
||||
* (avoids removing a different Database subsequently registered with the same name).
|
||||
*/
|
||||
void deregister(Database server) {
|
||||
lock.lock();
|
||||
try {
|
||||
String name = server.name();
|
||||
concMap.remove(name, server);
|
||||
syncMap.remove(name, server);
|
||||
if (defaultDatabase == server) {
|
||||
defaultDatabase = null;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void registerWithName(String name, Database server, boolean isDefault) {
|
||||
lock.lock();
|
||||
try {
|
||||
|
||||
@@ -246,4 +246,41 @@ public interface DtoQuery<T> extends CancelableQuery {
|
||||
*/
|
||||
DtoQuery<T> usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* 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>
|
||||
* This is only supported for a DtoQuery that is derived from an ORM query via
|
||||
* {@link Query#asDto(Class)} / {@link ExpressionList#asDto(Class)}. It is not supported
|
||||
* for a DtoQuery based on raw SQL (e.g. via {@link Database#findDto(Class, String)}) as
|
||||
* there is no query structure available from which to derive a matching row count query -
|
||||
* a PersistenceException is thrown in that case.
|
||||
* <pre>{@code
|
||||
*
|
||||
* PagedList<OrderDto> pagedList =
|
||||
* DB.find(Order.class)
|
||||
* .where().eq("status", Order.Status.NEW)
|
||||
* .orderBy().asc("id")
|
||||
* .setFirstRow(50)
|
||||
* .setMaxRows(20)
|
||||
* .asDto(OrderDto.class)
|
||||
* .findPagedList();
|
||||
*
|
||||
* // fetch the total row count in the background
|
||||
* pagedList.loadCount();
|
||||
*
|
||||
* List<OrderDto> orders = pagedList.getList();
|
||||
* int totalRowCount = pagedList.getTotalCount();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @return The PagedList
|
||||
*/
|
||||
PagedList<T> findPagedList();
|
||||
|
||||
}
|
||||
|
||||
@@ -205,6 +205,22 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
int delete();
|
||||
|
||||
/**
|
||||
* Execute as a delete query permanently deleting the 'root level' beans that match the
|
||||
* predicates in the query without soft delete.
|
||||
* <p>
|
||||
* This is the same as {@link #delete()} except that when the bean type uses soft delete
|
||||
* (e.g. {@code @SoftDelete}) the matching rows are permanently (hard) deleted rather than
|
||||
* being marked as deleted.
|
||||
* <p>
|
||||
* Note that if the query includes joins then the generated delete statement may not be
|
||||
* optimal depending on the database platform.
|
||||
* </p>
|
||||
*
|
||||
* @return the number of rows that were permanently deleted.
|
||||
*/
|
||||
int deletePermanent();
|
||||
|
||||
/**
|
||||
* Execute as a update query.
|
||||
*
|
||||
|
||||
@@ -607,6 +607,21 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
int delete();
|
||||
|
||||
/**
|
||||
* Execute as a delete query permanently deleting the 'root level' beans that match the
|
||||
* predicates in the query without soft delete.
|
||||
* <p>
|
||||
* This is the same as {@link #delete()} except that when the bean type uses soft delete
|
||||
* (e.g. {@code @SoftDelete}) the matching rows are permanently (hard) deleted rather than
|
||||
* being marked as deleted.
|
||||
* <p>
|
||||
* Note that if the query includes joins then the generated delete statement may not be
|
||||
* optimal depending on the database platform.
|
||||
*
|
||||
* @return the number of beans/rows that were permanently deleted.
|
||||
*/
|
||||
int deletePermanent();
|
||||
|
||||
/**
|
||||
* Execute the query returning true if a row is found.
|
||||
* <p>
|
||||
|
||||
@@ -41,6 +41,49 @@ public interface RawSqlBuilder {
|
||||
return XBootstrapService.rawSql().unparsed(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a RawSqlBuilder for SQL containing {@code ${where}}, {@code ${having}} and/or
|
||||
* {@code ${orderBy}} placeholder(s). Unlike {@link #parse(String)} this does NOT attempt to parse
|
||||
* the SELECT columns, so it supports complex SQL such as CTEs, subqueries, and window functions.
|
||||
* <p>
|
||||
* Explicit column mappings must be provided (as with {@link #unparsed(String)}), but
|
||||
* WHERE, HAVING and ORDER BY expressions can be added dynamically via the query API - provided
|
||||
* the corresponding placeholder is present in the SQL. If a query calls {@code .orderBy(...)}
|
||||
* on a template with no {@code ${orderBy}}/{@code ${andOrderBy}} placeholder, that ordering is
|
||||
* ignored (there is no injection point for it) rather than producing invalid SQL.
|
||||
* </p>
|
||||
* <p>
|
||||
* Available placeholders:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@code ${where}} / {@code ${andWhere}} - inject "where <expr>" / "and <expr>"</li>
|
||||
* <li>{@code ${having}} / {@code ${andHaving}} - inject "having <expr>" / "and <expr>"</li>
|
||||
* <li>{@code ${orderBy}} / {@code ${andOrderBy}} - inject "order by <expr>" / ", <expr>"</li>
|
||||
* </ul>
|
||||
* <h3>Example:</h3>
|
||||
* <pre>{@code
|
||||
*
|
||||
* String sql = """
|
||||
* with agg as (
|
||||
* select company_id, sum(amount) as total
|
||||
* from orders
|
||||
* ${where}
|
||||
* group by company_id
|
||||
* )
|
||||
* select company_id, total from agg ${orderBy}
|
||||
* """;
|
||||
*
|
||||
* RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
* .columnMapping("company_id", "companyId")
|
||||
* .columnMapping("total", "total")
|
||||
* .create();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
static RawSqlBuilder withPlaceholders(String sql) {
|
||||
return XBootstrapService.rawSql().withPlaceholders(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a RawSqlBuilder parsing the sql.
|
||||
* <p>
|
||||
|
||||
@@ -175,7 +175,7 @@ public final class InterceptReadOnly extends InterceptBase {
|
||||
|
||||
@Override
|
||||
public boolean isUpdate() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -432,6 +432,8 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
private int backgroundExecutorShutdownSecs = 30;
|
||||
private BackgroundExecutorWrapper backgroundExecutorWrapper = new MdcBackgroundExecutorWrapper();
|
||||
|
||||
private boolean tenantPartitionedCache;
|
||||
|
||||
// defaults for the L2 bean caching
|
||||
|
||||
private int cacheMaxSize = 10000;
|
||||
@@ -1175,6 +1177,17 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
return cacheMaxSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTenantPartitionedCache() {
|
||||
return tenantPartitionedCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig tenantPartitionedCache(boolean tenantPartitionedCache) {
|
||||
this.tenantPartitionedCache = tenantPartitionedCache;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig setCacheMaxSize(int cacheMaxSize) {
|
||||
this.cacheMaxSize = cacheMaxSize;
|
||||
@@ -2228,6 +2241,15 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
ddlPlaceholders = p.get("ddl.placeholders", ddlPlaceholders);
|
||||
ddlHeader = p.get("ddl.header", ddlHeader);
|
||||
|
||||
tenantPartitionedCache = p.getBoolean("tenantPartitionedCache", tenantPartitionedCache);
|
||||
|
||||
cacheMaxSize = p.getInt("cacheMaxSize", cacheMaxSize);
|
||||
cacheMaxIdleTime = p.getInt("cacheMaxIdleTime", cacheMaxIdleTime);
|
||||
cacheMaxTimeToLive = p.getInt("cacheMaxTimeToLive", cacheMaxTimeToLive);
|
||||
queryCacheMaxSize = p.getInt("queryCacheMaxSize", queryCacheMaxSize);
|
||||
queryCacheMaxIdleTime = p.getInt("queryCacheMaxIdleTime", queryCacheMaxIdleTime);
|
||||
queryCacheMaxTimeToLive = p.getInt("queryCacheMaxTimeToLive", queryCacheMaxTimeToLive);
|
||||
|
||||
// read tenant-configuration from config:
|
||||
// tenant.mode = NONE | DB | SCHEMA | CATALOG | PARTITION
|
||||
String mode = p.get("tenant.mode");
|
||||
|
||||
@@ -162,6 +162,18 @@ public class DatabasePlatform {
|
||||
protected boolean selectCountWithAlias;
|
||||
protected boolean selectCountWithColumnAlias;
|
||||
|
||||
/**
|
||||
* Set true for platforms where {@code exists(...)} can only be used as a predicate
|
||||
* and not as a directly selectable scalar boolean expression (e.g. SQL Server, Oracle).
|
||||
*/
|
||||
protected boolean existsWithCaseWhen;
|
||||
|
||||
/**
|
||||
* Clause appended after the {@code case when exists(...) then 1 else 0 end} exists query
|
||||
* for platforms that require a FROM clause on every select (e.g. {@code from dual} on Oracle).
|
||||
*/
|
||||
protected String existsFromClause = "";
|
||||
|
||||
/**
|
||||
* If set then use the FORWARD ONLY hint when creating ResultSets for
|
||||
* findIterate() and findVisit().
|
||||
@@ -660,6 +672,21 @@ public class DatabasePlatform {
|
||||
return selectCountWithColumnAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if a scalar boolean {@code exists(...)} expression is not supported
|
||||
* as a select expression and needs to be wrapped as {@code case when exists(...) then 1 else 0 end}.
|
||||
*/
|
||||
public boolean existsWithCaseWhen() {
|
||||
return existsWithCaseWhen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the clause to append after the exists case-when wrapping (e.g. {@code from dual} on Oracle).
|
||||
*/
|
||||
public String existsFromClause() {
|
||||
return existsFromClause;
|
||||
}
|
||||
|
||||
|
||||
public String completeSql(String sql, Query<?> query) {
|
||||
if (query.isForUpdate()) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.ebean.event;
|
||||
|
||||
import io.ebean.Database;
|
||||
import io.ebean.DatabaseFactory;
|
||||
import io.ebean.EbeanVersion;
|
||||
import io.ebean.service.SpiContainer;
|
||||
|
||||
@@ -188,6 +189,7 @@ public final class ShutdownManager {
|
||||
*/
|
||||
public static void unregisterDatabase(Database server) {
|
||||
databases.remove(server);
|
||||
DatabaseFactory.unregister(server);
|
||||
}
|
||||
|
||||
private static class ShutdownHook extends Thread {
|
||||
|
||||
@@ -44,6 +44,11 @@ public interface MetaQueryPlan {
|
||||
*/
|
||||
String plan();
|
||||
|
||||
/**
|
||||
* The tenant ID of the plan.
|
||||
*/
|
||||
Object tenantId();
|
||||
|
||||
/**
|
||||
* Return the query execution time associated with the bind values capture.
|
||||
*/
|
||||
|
||||
@@ -27,6 +27,13 @@ public interface SpiRawSqlService extends BootstrapService {
|
||||
*/
|
||||
RawSqlBuilder unparsed(String sql);
|
||||
|
||||
/**
|
||||
* SQL with ${where}/${having} placeholder(s) but no SELECT column parsing.
|
||||
* Supports complex SQL (CTEs, window functions) where keyword parsing would fail.
|
||||
* Explicit column mapping is required (as with unparsed).
|
||||
*/
|
||||
RawSqlBuilder withPlaceholders(String sql);
|
||||
|
||||
/**
|
||||
* Create based on a JDBC ResultSet.
|
||||
*
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -155,37 +155,37 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>kotlin-querybean-generator</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-redis</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-spring-txn</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- platforms -->
|
||||
@@ -193,91 +193,91 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-clickhouse</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-db2</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-h2</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-hana</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mariadb</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mysql</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-nuodb</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-oracle</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgres</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlite</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlserver</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -29,7 +29,7 @@
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.2</version>
|
||||
<version>42.7.11</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
|
||||
+8
-8
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-json</artifactId>
|
||||
<version>18.1.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.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -149,7 +149,7 @@
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.2</version>
|
||||
<version>42.7.11</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
@@ -157,21 +157,21 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>18.1.0</version>
|
||||
<version>18.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -100,7 +100,9 @@ public final class LoadBeanRequest extends LoadRequest {
|
||||
query.setLoadDescription(mode(), description());
|
||||
if (lazy) {
|
||||
query.setLazyLoadBatchSize(loadBuffer.batchSize());
|
||||
if (alreadyLoaded) {
|
||||
if (alreadyLoaded || !loadCache) {
|
||||
// alreadyLoaded: bean is being re-loaded, skip the cache to avoid a stale hit
|
||||
// !loadCache: parent context disabled cache (e.g. CacheMode.OFF or asOf query),
|
||||
query.setBeanCacheMode(CacheMode.OFF);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,6 @@ public interface SpiDbQueryPlan extends MetaQueryPlan {
|
||||
/**
|
||||
* Extend with queryTimeMicros, captureCount, captureMicros and when the bind values were captured.
|
||||
*/
|
||||
SpiDbQueryPlan with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured);
|
||||
SpiDbQueryPlan with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured, Object tenantId);
|
||||
|
||||
}
|
||||
|
||||
@@ -374,6 +374,8 @@ public interface SpiEbeanServer extends SpiServer, BeanCollectionLoader {
|
||||
|
||||
<T> int delete(SpiQuery<T> query);
|
||||
|
||||
<T> int deletePermanent(SpiQuery<T> query);
|
||||
|
||||
<T> int update(SpiQuery<T> query);
|
||||
|
||||
List<SqlRow> findList(SpiSqlQuery query);
|
||||
|
||||
@@ -15,4 +15,19 @@ public interface SpiQueryManyJoin {
|
||||
*/
|
||||
String fetchOrderBy();
|
||||
|
||||
/**
|
||||
* Return true if this many relationship has an order column stored on the
|
||||
* ManyToMany intersection table (rather than a target descriptor property).
|
||||
*/
|
||||
default boolean hasIntersectionOrderColumn() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the db column name of the ManyToMany intersection table order column (or null).
|
||||
*/
|
||||
default String intersectionOrderColumn() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -60,6 +60,6 @@ public interface SpiTransactionManager {
|
||||
/**
|
||||
* Return a connection used for query plan collection.
|
||||
*/
|
||||
Connection queryPlanConnection() throws SQLException;
|
||||
Connection queryPlanConnection(Object tenantId) throws SQLException;
|
||||
|
||||
}
|
||||
|
||||
+11
-4
@@ -13,8 +13,9 @@ import io.ebeaninternal.server.cluster.ClusterManager;
|
||||
public final class CacheManagerOptions {
|
||||
|
||||
private final ClusterManager clusterManager;
|
||||
private final DatabaseBuilder.Settings databaseBuilder;
|
||||
private final String serverName;
|
||||
private final boolean localL2Caching;
|
||||
private final boolean tenantPartitionedCache;
|
||||
private CurrentTenantProvider currentTenantProvider;
|
||||
private QueryCacheEntryValidate queryCacheEntryValidate;
|
||||
private ServerCacheFactory cacheFactory = new DefaultServerCacheFactory();
|
||||
@@ -24,7 +25,8 @@ public final class CacheManagerOptions {
|
||||
CacheManagerOptions() {
|
||||
this.localL2Caching = true;
|
||||
this.clusterManager = null;
|
||||
this.databaseBuilder = null;
|
||||
this.serverName = "db";
|
||||
this.tenantPartitionedCache = false;
|
||||
this.cacheFactory = new DefaultServerCacheFactory();
|
||||
this.beanDefault = new ServerCacheOptions();
|
||||
this.queryDefault = new ServerCacheOptions();
|
||||
@@ -32,9 +34,10 @@ public final class CacheManagerOptions {
|
||||
|
||||
public CacheManagerOptions(ClusterManager clusterManager, DatabaseBuilder.Settings config, boolean localL2Caching) {
|
||||
this.clusterManager = clusterManager;
|
||||
this.databaseBuilder = config;
|
||||
this.serverName = config.getName();
|
||||
this.localL2Caching = localL2Caching;
|
||||
this.currentTenantProvider = config.getCurrentTenantProvider();
|
||||
this.tenantPartitionedCache = config.isTenantPartitionedCache();
|
||||
}
|
||||
|
||||
public CacheManagerOptions with(ServerCacheOptions beanDefault, ServerCacheOptions queryDefault) {
|
||||
@@ -55,7 +58,7 @@ public final class CacheManagerOptions {
|
||||
}
|
||||
|
||||
public String getServerName() {
|
||||
return (databaseBuilder == null) ? "db" : databaseBuilder.getName();
|
||||
return serverName;
|
||||
}
|
||||
|
||||
public boolean isLocalL2Caching() {
|
||||
@@ -85,4 +88,8 @@ public final class CacheManagerOptions {
|
||||
public QueryCacheEntryValidate getQueryCacheEntryValidate() {
|
||||
return queryCacheEntryValidate;
|
||||
}
|
||||
|
||||
public boolean isTenantPartitionedCache() {
|
||||
return tenantPartitionedCache;
|
||||
}
|
||||
}
|
||||
|
||||
+92
-13
@@ -9,6 +9,7 @@ import io.ebean.config.CurrentTenantProvider;
|
||||
import io.ebean.meta.MetricVisitor;
|
||||
import io.ebean.util.AnnotationUtil;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
@@ -32,6 +33,7 @@ final class DefaultCacheHolder {
|
||||
private final ServerCacheOptions queryDefault;
|
||||
private final CurrentTenantProvider tenantProvider;
|
||||
private final QueryCacheEntryValidate queryCacheEntryValidate;
|
||||
private final boolean tenantPartitionedCache;
|
||||
|
||||
DefaultCacheHolder(CacheManagerOptions builder) {
|
||||
this.cacheFactory = builder.getCacheFactory();
|
||||
@@ -39,6 +41,7 @@ final class DefaultCacheHolder {
|
||||
this.queryDefault = builder.getQueryDefault();
|
||||
this.tenantProvider = builder.getCurrentTenantProvider();
|
||||
this.queryCacheEntryValidate = builder.getQueryCacheEntryValidate();
|
||||
this.tenantPartitionedCache = builder.isTenantPartitionedCache();
|
||||
}
|
||||
|
||||
void visitMetrics(MetricVisitor visitor) {
|
||||
@@ -56,16 +59,31 @@ final class DefaultCacheHolder {
|
||||
return getCacheInternal(beanType, ServerCacheType.COLLECTION_IDS, collectionProperty);
|
||||
}
|
||||
|
||||
private String tenantKey(String beanName) {
|
||||
if (tenantPartitionedCache) {
|
||||
return beanName + '.' + tenantProvider.currentId();
|
||||
}
|
||||
return beanName;
|
||||
}
|
||||
|
||||
private String key(String beanName, ServerCacheType type) {
|
||||
if (tenantPartitionedCache) {
|
||||
return beanName + '.' + tenantProvider.currentId() + type.code();
|
||||
}
|
||||
return beanName + type.code();
|
||||
}
|
||||
|
||||
private String key(String beanName, String collectionProperty, ServerCacheType type) {
|
||||
if (collectionProperty != null) {
|
||||
return beanName + "." + collectionProperty + type.code();
|
||||
} else {
|
||||
return beanName + type.code();
|
||||
StringBuilder sb = new StringBuilder(beanName.length() + 64);
|
||||
sb.append(beanName);
|
||||
if (tenantPartitionedCache) {
|
||||
sb.append('.').append(tenantProvider.currentId());
|
||||
}
|
||||
if (collectionProperty != null) {
|
||||
sb.append('.').append(collectionProperty);
|
||||
}
|
||||
sb.append(type.code());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,12 +100,15 @@ final class DefaultCacheHolder {
|
||||
if (type == ServerCacheType.COLLECTION_IDS) {
|
||||
lock.lock();
|
||||
try {
|
||||
collectIdCaches.computeIfAbsent(beanType.getName(), s -> new ConcurrentSkipListSet<>()).add(key);
|
||||
collectIdCaches.computeIfAbsent(tenantKey(beanType.getName()), s -> new ConcurrentSkipListSet<>()).add(key);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, tenantProvider, queryCacheEntryValidate));
|
||||
// in partitioned mode each ServerCache instance is already tenant-scoped via its key,
|
||||
// so the tenantProvider is not needed inside the cache itself
|
||||
CurrentTenantProvider cacheProvider = tenantPartitionedCache ? null : tenantProvider;
|
||||
return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, cacheProvider, queryCacheEntryValidate));
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
@@ -100,17 +121,75 @@ final class DefaultCacheHolder {
|
||||
|
||||
public void clear(String name) {
|
||||
log.log(DEBUG, "clear {0}", name);
|
||||
clearIfExists(key(name, ServerCacheType.QUERY));
|
||||
clearIfExists(key(name, ServerCacheType.BEAN));
|
||||
clearIfExists(key(name, ServerCacheType.NATURAL_KEY));
|
||||
Set<String> keys = collectIdCaches.get(name);
|
||||
if (keys != null) {
|
||||
for (String collectionIdKey : keys) {
|
||||
clearIfExists(collectionIdKey);
|
||||
if (tenantPartitionedCache) {
|
||||
// In partitioned mode, tenantProvider.currentId() may be null/wrong on a background
|
||||
// invalidation thread. Scan all cache entries for this entity type across all tenants.
|
||||
clearAllTenantsFor(name);
|
||||
} else {
|
||||
clearIfExists(key(name, ServerCacheType.QUERY));
|
||||
clearIfExists(key(name, ServerCacheType.BEAN));
|
||||
clearIfExists(key(name, ServerCacheType.NATURAL_KEY));
|
||||
Set<String> keys = collectIdCaches.get(name);
|
||||
if (keys != null) {
|
||||
for (String collectionIdKey : keys) {
|
||||
clearIfExists(collectionIdKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAllTenantsFor(String name) {
|
||||
// Keys in partitioned mode: beanName.tenantId_X or beanName.tenantId.prop_X
|
||||
// collectIdCaches keys: beanName.tenantId
|
||||
// Both start with beanName + '.' so we can use prefix scan.
|
||||
String prefix = name + '.';
|
||||
for (Map.Entry<String, ServerCache> entry : allCaches.entrySet()) {
|
||||
if (entry.getKey().startsWith(prefix)) {
|
||||
log.log(TRACE, "clear cache {0}", entry.getKey());
|
||||
entry.getValue().clear();
|
||||
}
|
||||
}
|
||||
lock.lock();
|
||||
try {
|
||||
for (Map.Entry<String, Set<String>> entry : collectIdCaches.entrySet()) {
|
||||
if (entry.getKey().startsWith(prefix)) {
|
||||
for (String collectionIdKey : entry.getValue()) {
|
||||
clearIfExists(collectionIdKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all cache entries belonging to the given tenant.
|
||||
* Call this when a tenant is deactivated to prevent unbounded memory growth.
|
||||
*/
|
||||
void clearTenant(Object tenantId) {
|
||||
String tenantIdStr = String.valueOf(tenantId);
|
||||
// Keys look like: beanName.tenantId_X or beanName.tenantId.prop_X
|
||||
String segmentBeforeTypeCode = '.' + tenantIdStr + '_';
|
||||
String segmentBeforeProperty = '.' + tenantIdStr + '.';
|
||||
allCaches.entrySet().removeIf(e -> {
|
||||
String k = e.getKey();
|
||||
return k.contains(segmentBeforeTypeCode) || k.contains(segmentBeforeProperty);
|
||||
});
|
||||
// collectIdCaches keys look like: beanName.tenantId
|
||||
String collectKeySuffix = '.' + tenantIdStr;
|
||||
lock.lock();
|
||||
try {
|
||||
collectIdCaches.entrySet().removeIf(e -> e.getKey().endsWith(collectKeySuffix));
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
boolean isTenantPartitionedCache() {
|
||||
return tenantPartitionedCache;
|
||||
}
|
||||
|
||||
private void clearIfExists(String fullKey) {
|
||||
ServerCache cache = allCaches.get(fullKey);
|
||||
if (cache != null) {
|
||||
|
||||
+10
@@ -154,4 +154,14 @@ public final class DefaultServerCacheManager implements SpiCacheManager {
|
||||
return cacheHolder.getCache(beanType, ServerCacheType.BEAN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTenantPartitionedCache() {
|
||||
return cacheHolder.isTenantPartitionedCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearTenant(Object tenantId) {
|
||||
cacheHolder.clearTenant(tenantId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -88,4 +88,17 @@ public interface SpiCacheManager {
|
||||
*/
|
||||
void clearLocal(Class<?> beanType);
|
||||
|
||||
/**
|
||||
* Returns true if this cache manager runs in tenant-partitioned mode.
|
||||
* In this mode caches are namespaced per tenant to improve cache-hit ratio.
|
||||
*/
|
||||
boolean isTenantPartitionedCache();
|
||||
|
||||
/**
|
||||
* Remove all cache entries belonging to the given tenant.
|
||||
* Call this when a tenant is deactivated to prevent unbounded memory growth
|
||||
* in tenant-partitioned mode.
|
||||
*/
|
||||
void clearTenant(Object tenantId);
|
||||
|
||||
}
|
||||
|
||||
@@ -1197,15 +1197,14 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
@Override
|
||||
public <T> boolean exists(SpiQuery<T> ormQuery) {
|
||||
SpiQuery<T> ormQueryCopy = ormQuery.copy();
|
||||
ormQueryCopy.setMaxRows(1);
|
||||
SpiOrmQueryRequest<?> request = createQueryRequest(Type.EXISTS, ormQueryCopy);
|
||||
List<Object> ids = request.getFromQueryCache();
|
||||
if (ids != null) {
|
||||
return !ids.isEmpty();
|
||||
Object cached = request.getFromQueryCache();
|
||||
if (cached != null) {
|
||||
return (Boolean) cached;
|
||||
}
|
||||
try {
|
||||
request.initTransIfRequired();
|
||||
return !request.findIds().isEmpty();
|
||||
return request.findExists();
|
||||
} finally {
|
||||
request.endTransIfRequired();
|
||||
}
|
||||
@@ -1234,6 +1233,15 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
|
||||
@Override
|
||||
public <T> int delete(SpiQuery<T> query) {
|
||||
return delete(query, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> int deletePermanent(SpiQuery<T> query) {
|
||||
return delete(query, true);
|
||||
}
|
||||
|
||||
private <T> int delete(SpiQuery<T> query, boolean permanent) {
|
||||
SpiOrmQueryRequest<T> request = createQueryRequest(Type.DELETE, query);
|
||||
try {
|
||||
request.initTransIfRequired();
|
||||
@@ -1247,7 +1255,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
if (ids.isEmpty()) {
|
||||
return 0;
|
||||
} else {
|
||||
return persister.deleteByIds(request.descriptor(), ids, request.transaction(), false);
|
||||
return persister.deleteByIds(request.descriptor(), ids, request.transaction(), permanent);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -2188,12 +2196,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Property> checkUniqueness(Object bean) {
|
||||
return checkUniqueness(bean, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Property> checkUniqueness(Object bean, @Nullable Transaction transaction) {
|
||||
public Set<Property> checkUniqueness(Object bean, @Nullable Transaction transaction, boolean useQueryCache, boolean skipClean) {
|
||||
EntityBean entityBean = checkEntityBean(bean);
|
||||
BeanDescriptor<?> beanDesc = descriptor(entityBean.getClass());
|
||||
BeanProperty idProperty = beanDesc.idProperty();
|
||||
@@ -2205,14 +2208,15 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
if (entityBean._ebean_getIntercept().isNew() && id != null) {
|
||||
// Primary Key is changeable only on new models - so skip check if we are not new
|
||||
SpiQuery<?> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
|
||||
query.setUseQueryCache(useQueryCache);
|
||||
query.usingTransaction(transaction);
|
||||
query.setId(id);
|
||||
if (findCount(query) > 0) {
|
||||
if (exists(query)) {
|
||||
return Collections.singleton(idProperty);
|
||||
}
|
||||
}
|
||||
for (BeanProperty[] props : beanDesc.uniqueProps()) {
|
||||
Set<Property> ret = checkUniqueness(entityBean, beanDesc, props, transaction);
|
||||
Set<Property> ret = checkUniqueness(entityBean, beanDesc, props, transaction, useQueryCache, skipClean);
|
||||
if (ret != null) {
|
||||
return ret;
|
||||
}
|
||||
@@ -2220,13 +2224,34 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, if any property is dirty.
|
||||
*/
|
||||
private boolean isAnyPropertyDirty(EntityBean entityBean, BeanProperty[] props) {
|
||||
if (entityBean._ebean_getIntercept().isNew()) {
|
||||
return true;
|
||||
}
|
||||
for (BeanProperty prop : props) {
|
||||
if (entityBean._ebean_getIntercept().isDirtyProperty(prop.propertyIndex())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of properties if saving the bean will violate the unique constraints (defined by given properties).
|
||||
*/
|
||||
@Nullable
|
||||
private Set<Property> checkUniqueness(EntityBean entityBean, BeanDescriptor<?> beanDesc, BeanProperty[] props, @Nullable Transaction transaction) {
|
||||
private Set<Property> checkUniqueness(EntityBean entityBean, BeanDescriptor<?> beanDesc, BeanProperty[] props, @Nullable Transaction transaction,
|
||||
boolean useQueryCache, boolean skipClean) {
|
||||
if (skipClean && !isAnyPropertyDirty(entityBean, props)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BeanProperty idProperty = beanDesc.idProperty();
|
||||
SpiQuery<?> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
|
||||
query.setUseQueryCache(useQueryCache);
|
||||
query.usingTransaction(transaction);
|
||||
ExpressionList<?> exprList = query.where();
|
||||
if (!entityBean._ebean_getIntercept().isNew()) {
|
||||
@@ -2240,7 +2265,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
}
|
||||
exprList.eq(prop.name(), value);
|
||||
}
|
||||
if (findCount(query) > 0) {
|
||||
if (exists(query)) {
|
||||
Set<Property> ret = new LinkedHashSet<>();
|
||||
Collections.addAll(ret, props);
|
||||
return ret;
|
||||
|
||||
@@ -589,7 +589,8 @@ public final class InternalConfiguration {
|
||||
return QueryPlanManager.NOOP;
|
||||
}
|
||||
long threshold = config.getQueryPlanThresholdMicros();
|
||||
return new CQueryPlanManager(transactionManager, threshold, queryPlanLogger(databasePlatform.platform(), config), extraMetrics);
|
||||
return new CQueryPlanManager(transactionManager, config.getCurrentTenantProvider(),
|
||||
threshold, queryPlanLogger(databasePlatform.platform(), config), extraMetrics);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,6 +49,11 @@ public interface OrmQueryEngine {
|
||||
*/
|
||||
<T> int findCount(OrmQueryRequest<T> request);
|
||||
|
||||
/**
|
||||
* Execute the exists query using SELECT EXISTS(...).
|
||||
*/
|
||||
<T> boolean findExists(OrmQueryRequest<T> request);
|
||||
|
||||
/**
|
||||
* Execute the find id's query.
|
||||
*/
|
||||
|
||||
@@ -127,6 +127,13 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the secondary queries (fetchQuery and lazy joins) extracted from the query detail.
|
||||
*/
|
||||
public SpiQuerySecondary secondaryQueries() {
|
||||
return secondaryQueries;
|
||||
}
|
||||
|
||||
/**
|
||||
* For use with QueryIterator and secondary queries this returns the minimum
|
||||
* batch size that should be loaded before executing the secondary queries.
|
||||
@@ -355,6 +362,11 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
|
||||
return queryEngine.findCount(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean findExists() {
|
||||
return queryEngine.findExists(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A> List<A> findIds() {
|
||||
return queryEngine.findIds(this);
|
||||
|
||||
@@ -56,6 +56,11 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
|
||||
private Object idValue;
|
||||
private boolean statelessUpdate;
|
||||
private boolean notifyCache;
|
||||
/**
|
||||
* Snapshot of origValues taken before intercept.setLoaded() clears them.
|
||||
* Used so that cache update code can access old FK values after setLoaded().
|
||||
*/
|
||||
private Object[] capturedOrigValues;
|
||||
/**
|
||||
* Flag used to detect when only many properties where updated via a cascade. Used to ensure
|
||||
* appropriate caches are updated in that case.
|
||||
@@ -122,6 +127,18 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
|
||||
*/
|
||||
private List<SaveMany> saveMany;
|
||||
private InsertOptions insertOptions;
|
||||
/**
|
||||
* Set true when an ON CONFLICT NOTHING insert is skipped (0 rows affected).
|
||||
* Cascade to children must be suppressed to avoid FK violations.
|
||||
*/
|
||||
private boolean insertConflictSkipped;
|
||||
/**
|
||||
* Set true once controller.preDelete() has been invoked so that it is
|
||||
* only ever fired once (as it is fired early, prior to cascading the
|
||||
* delete to children/many's rather than as part of executing the delete).
|
||||
*/
|
||||
private boolean preDeleteCalled;
|
||||
private boolean preDeleteResult = true;
|
||||
|
||||
public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager<T> mgr, SpiTransaction t,
|
||||
PersistExecute persistExecute, PersistRequest.Type type, int flags) {
|
||||
@@ -710,7 +727,11 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
|
||||
* Return the original / old value for the given property.
|
||||
*/
|
||||
public Object origValue(BeanProperty prop) {
|
||||
return intercept.origValue(prop.propertyIndex());
|
||||
int idx = prop.propertyIndex();
|
||||
if (capturedOrigValues != null && idx < capturedOrigValues.length) {
|
||||
return capturedOrigValues[idx];
|
||||
}
|
||||
return intercept.origValue(idx);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -801,6 +822,9 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
|
||||
throw new OptimisticLockException("Data has changed. updated row count " + rowCount, null, bean);
|
||||
} else if (rowCount == 0 && type == Type.UPDATE) {
|
||||
throw new EntityNotFoundException("No rows updated");
|
||||
} else if (rowCount == 0 && type == Type.INSERT && isConflictNothingInsert()) {
|
||||
insertConflictSkipped = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
switch (type) {
|
||||
@@ -815,6 +839,14 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConflictNothingInsert() {
|
||||
return insertOptions != null && insertOptions.key().charAt(0) == 'N';
|
||||
}
|
||||
|
||||
public boolean isInsertConflictSkipped() {
|
||||
return insertConflictSkipped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the bean from the PersistenceContext (L1 cache) for stateless updates.
|
||||
*/
|
||||
@@ -867,6 +899,15 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
|
||||
if (type == Type.UPDATE) {
|
||||
// get the dirty properties for notify cache & orphanRemoval of vanilla collection detection
|
||||
dirtyProperties = intercept.dirtyProperties();
|
||||
// snapshot orig values before setLoaded() clears them - needed by cache FK-eviction logic
|
||||
if (dirtyProperties != null && notifyCache) {
|
||||
capturedOrigValues = new Object[dirtyProperties.length];
|
||||
for (int i = 0; i < dirtyProperties.length; i++) {
|
||||
if (dirtyProperties[i]) {
|
||||
capturedOrigValues[i] = intercept.origValue(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isChangeLog) {
|
||||
changeLog();
|
||||
@@ -903,6 +944,20 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the preUpdate event fires (for case where only a ManyToMany collection has changed).
|
||||
*/
|
||||
public void preManyToManyUpdate() {
|
||||
if (controller != null && !dirty) {
|
||||
// fire preUpdate notification when only ManyToMany intersection updated
|
||||
controller.preUpdate(this);
|
||||
pendingPostUpdateNotify = true;
|
||||
}
|
||||
if (!dirty) {
|
||||
setNotifyCache();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isNotifyCache() {
|
||||
return notifyCache;
|
||||
}
|
||||
@@ -1228,14 +1283,29 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
|
||||
}
|
||||
|
||||
private int executeDelete() {
|
||||
setTenantId();
|
||||
if (controller == null || controller.preDelete(this)) {
|
||||
if (controllerPreDelete()) {
|
||||
return beanManager.getBeanPersister().delete(this);
|
||||
}
|
||||
// delete handled by the BeanController so return 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke controller.preDelete() if not already invoked.
|
||||
* <p>
|
||||
* This is called prior to cascading the delete to children (assoc many's /
|
||||
* many-to-many intersection rows) so that the persist controller can still
|
||||
* see those collections/relationships as they were before the cascade delete.
|
||||
*/
|
||||
public boolean controllerPreDelete() {
|
||||
if (!preDeleteCalled) {
|
||||
preDeleteCalled = true;
|
||||
setTenantId();
|
||||
preDeleteResult = controller == null || controller.preDelete(this);
|
||||
}
|
||||
return preDeleteResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist to the document store now (via buffer, not post commit).
|
||||
*/
|
||||
@@ -1415,7 +1485,12 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
|
||||
}
|
||||
|
||||
public void setInsertOptions(InsertOptions insertOptions) {
|
||||
this.insertOptions = insertOptions;
|
||||
this.insertOptions = insertOptions;
|
||||
if (insertOptions != null && insertOptions.key().charAt(0) == 'N' && beanDescriptor.hasCascadeChildren()) {
|
||||
// force immediate (non-batch) execution of this insert so we know whether it
|
||||
// was actually inserted before attempting cascade saves on children.
|
||||
skipBatchForTopLevel = true;
|
||||
}
|
||||
}
|
||||
|
||||
public InsertOptions insertOptions() {
|
||||
|
||||
@@ -75,6 +75,11 @@ public interface SpiOrmQueryRequest<T> extends BeanQueryRequest<T>, DocQueryRequ
|
||||
*/
|
||||
int findCount();
|
||||
|
||||
/**
|
||||
* Execute the find exists query using select exists(...).
|
||||
*/
|
||||
boolean findExists();
|
||||
|
||||
/**
|
||||
* Execute the find ids query.
|
||||
*/
|
||||
|
||||
@@ -10,8 +10,11 @@ import java.sql.SQLException;
|
||||
*/
|
||||
final class AssocOneHelpEmbedded extends AssocOneHelp {
|
||||
|
||||
AssocOneHelpEmbedded(BeanPropertyAssocOne<?> property) {
|
||||
private final boolean allowEmpty;
|
||||
|
||||
AssocOneHelpEmbedded(BeanPropertyAssocOne<?> property, boolean allowEmpty) {
|
||||
super(property);
|
||||
this.allowEmpty = allowEmpty;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -40,7 +43,7 @@ final class AssocOneHelpEmbedded extends AssocOneHelp {
|
||||
notNull = true;
|
||||
}
|
||||
}
|
||||
if (notNull) {
|
||||
if (notNull || allowEmpty) {
|
||||
return embeddedBean;
|
||||
} else {
|
||||
return null;
|
||||
@@ -69,7 +72,7 @@ final class AssocOneHelpEmbedded extends AssocOneHelp {
|
||||
notNull = true;
|
||||
}
|
||||
}
|
||||
return notNull ? embeddedBean : null;
|
||||
return (notNull || allowEmpty) ? embeddedBean : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -43,6 +43,8 @@ import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyLists;
|
||||
import io.ebeaninternal.server.el.*;
|
||||
import io.ebeaninternal.server.persist.DeleteMode;
|
||||
import io.ebeaninternal.server.persist.MultiValueWrapper;
|
||||
import io.ebeaninternal.server.type.ScalarTypeArray;
|
||||
import io.ebeaninternal.server.query.*;
|
||||
import io.ebeaninternal.server.querydefn.DefaultOrmQuery;
|
||||
import io.ebeaninternal.server.querydefn.OrmQueryDetail;
|
||||
@@ -320,7 +322,7 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
|
||||
this.idOnlyReference = isIdOnlyReference(propertiesBaseScalar);
|
||||
boolean noRelationships = propertiesOne.length + propertiesMany.length == 0;
|
||||
this.cacheSharableBeans = noRelationships && deploy.getCacheOptions().isReadOnly();
|
||||
this.cacheHelp = new BeanDescriptorCacheHelp<>(this, owner.cacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported);
|
||||
this.cacheHelp = BeanDescriptorCacheHelp.create(this, owner.cacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported);
|
||||
this.jsonHelp = initJsonHelp();
|
||||
this.draftHelp = new BeanDescriptorDraftHelp<>(this);
|
||||
this.docStoreAdapter = owner.createDocStoreBeanAdapter(this, deploy);
|
||||
@@ -753,7 +755,16 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
|
||||
public void bindElementValue(SqlUpdate insert, Object value) {
|
||||
EntityBean bean = (EntityBean) value;
|
||||
for (BeanProperty property : propertiesBaseScalar) {
|
||||
insert.setParameter(property.getValue(bean));
|
||||
Object propertyValue = property.getValue(bean);
|
||||
if (property.isArrayType() && propertyValue instanceof Collection) {
|
||||
// Bind with the declared element type rather than relying on MultiValueWrapper's default
|
||||
// constructor which infers the element type from the first value - this fails with a
|
||||
// NoSuchElementException for an empty collection. See #2477.
|
||||
Class<?> elementType = ((ScalarTypeArray) property.scalarType()).elementType();
|
||||
insert.setParameter(new MultiValueWrapper((Collection<?>) propertyValue, elementType));
|
||||
} else {
|
||||
insert.setParameter(propertyValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2402,6 +2413,12 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
|
||||
if (assocProp == null) {
|
||||
return null;
|
||||
}
|
||||
// this method is an entry-point, although it introduces recursive calls via
|
||||
// buildElPropertyValue -> createElPropertyValue -> buildElGetValue (back to here)
|
||||
// it seems we can initialize ElPropertyChainBuilder at this point and skip further checks.
|
||||
if (chain == null) {
|
||||
chain = new ElPropertyChainBuilder(propName);
|
||||
}
|
||||
String remainder = propName.substring(basePos + 1);
|
||||
return assocProp.buildElPropertyValue(propName, remainder, chain, propertyDeploy);
|
||||
}
|
||||
@@ -2413,9 +2430,7 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
|
||||
if (property == null) {
|
||||
throw new PersistenceException("No property found for [" + propName + "] in expression " + chain.expression());
|
||||
}
|
||||
if (property.containsMany()) {
|
||||
chain.setContainsMany();
|
||||
}
|
||||
|
||||
return chain.add(property).build();
|
||||
}
|
||||
|
||||
@@ -3337,6 +3352,15 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
|
||||
return propertiesManySave;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this bean has cascade-save children (OneToMany or exported OneToOne)
|
||||
* that hold a FK back to this bean. Used to decide whether an ON CONFLICT NOTHING
|
||||
* insert must be executed immediately so the row-count is known before cascading.
|
||||
*/
|
||||
public boolean hasCascadeChildren() {
|
||||
return propertiesManySave.length > 0 || propertiesOneExportedSave.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assoc Many's with delete cascade.
|
||||
*/
|
||||
|
||||
+135
-80
@@ -27,7 +27,7 @@ import static java.lang.System.Logger.Level.*;
|
||||
*
|
||||
* @param <T> The entity bean type
|
||||
*/
|
||||
final class BeanDescriptorCacheHelp<T> {
|
||||
abstract class BeanDescriptorCacheHelp<T> {
|
||||
|
||||
private static final System.Logger log = CoreLog.internal;
|
||||
|
||||
@@ -36,7 +36,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
private static final System.Logger manyLog = AppLog.getLogger("io.ebean.cache.COLL");
|
||||
private static final System.Logger natLog = AppLog.getLogger("io.ebean.cache.NATKEY");
|
||||
|
||||
private final BeanDescriptor<T> desc;
|
||||
final BeanDescriptor<T> desc;
|
||||
private final SpiCacheManager cacheManager;
|
||||
private final CacheOptions cacheOptions;
|
||||
/**
|
||||
@@ -44,13 +44,10 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
*/
|
||||
private final boolean cacheSharableBeans;
|
||||
private final boolean invalidateQueryCache;
|
||||
private final Class<?> beanType;
|
||||
final Class<?> beanType;
|
||||
private final String cacheName;
|
||||
private final BeanPropertyAssocOne<?>[] propertiesOneImported;
|
||||
private final String[] naturalKey;
|
||||
private final ServerCache beanCache;
|
||||
private final ServerCache naturalKeyCache;
|
||||
private final ServerCache queryCache;
|
||||
private final boolean noCaching;
|
||||
private final SpiCacheControl cacheControl;
|
||||
private final SpiCacheRegion cacheRegion;
|
||||
@@ -63,6 +60,21 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Set to true if delete changes need to notify cache.
|
||||
*/
|
||||
private boolean cacheNotifyOnDelete;
|
||||
/**
|
||||
* Set to true if this bean type has owning OneToOne properties pointing to cached targets.
|
||||
* When true, all persist events must evict the target bean caches regardless of whether
|
||||
* this bean type itself has a bean cache (bypasses the cacheRegion.isEnabled() gate).
|
||||
*/
|
||||
private boolean cacheNotifyOneToOneOwner;
|
||||
|
||||
static <T> BeanDescriptorCacheHelp<T> create(BeanDescriptor<T> desc, SpiCacheManager cacheManager, CacheOptions cacheOptions,
|
||||
boolean cacheSharableBeans, BeanPropertyAssocOne<?>[] propertiesOneImported) {
|
||||
if ((cacheOptions.isEnableQueryCache() || cacheOptions.isEnableBeanCache()) && cacheManager.isTenantPartitionedCache()) {
|
||||
return new BeanDescriptorCacheHelpPartitioned<>(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported);
|
||||
} else {
|
||||
return new BeanDescriptorCacheHelpFixed<>(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported);
|
||||
}
|
||||
}
|
||||
|
||||
BeanDescriptorCacheHelp(BeanDescriptor<T> desc, SpiCacheManager cacheManager, CacheOptions cacheOptions,
|
||||
boolean cacheSharableBeans, BeanPropertyAssocOne<?>[] propertiesOneImported) {
|
||||
@@ -75,48 +87,54 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
this.cacheSharableBeans = cacheSharableBeans;
|
||||
this.propertiesOneImported = propertiesOneImported;
|
||||
this.naturalKey = cacheOptions.getNaturalKey();
|
||||
if (!cacheOptions.isEnableQueryCache()) {
|
||||
this.queryCache = null;
|
||||
} else {
|
||||
this.queryCache = cacheManager.getQueryCache(beanType);
|
||||
}
|
||||
|
||||
if (cacheOptions.isEnableBeanCache()) {
|
||||
this.beanCache = cacheManager.getBeanCache(beanType);
|
||||
if (cacheOptions.getNaturalKey() != null) {
|
||||
this.naturalKeyCache = cacheManager.getNaturalKeyCache(beanType);
|
||||
} else {
|
||||
this.naturalKeyCache = null;
|
||||
}
|
||||
} else {
|
||||
this.beanCache = null;
|
||||
this.naturalKeyCache = null;
|
||||
}
|
||||
this.noCaching = (beanCache == null && queryCache == null);
|
||||
this.noCaching = !cacheOptions.isEnableQueryCache() && !cacheOptions.isEnableBeanCache();
|
||||
if (noCaching) {
|
||||
this.cacheControl = DCacheControlNone.INSTANCE;
|
||||
this.cacheRegion = (invalidateQueryCache) ? cacheManager.getRegion(cacheOptions.getRegion()) : DCacheRegionNone.INSTANCE;
|
||||
} else {
|
||||
this.cacheRegion = cacheManager.getRegion(cacheOptions.getRegion());
|
||||
this.cacheControl = new DCacheControl(cacheRegion, (beanCache != null), (naturalKeyCache != null), (queryCache != null));
|
||||
this.cacheControl = new DCacheControl(cacheRegion,
|
||||
cacheOptions.isEnableBeanCache(),
|
||||
cacheOptions.isEnableBeanCache() && cacheOptions.getNaturalKey() != null,
|
||||
cacheOptions.isEnableQueryCache());
|
||||
}
|
||||
}
|
||||
|
||||
abstract boolean hasBeanCache();
|
||||
|
||||
abstract boolean hasQueryCache();
|
||||
|
||||
abstract ServerCache queryCache();
|
||||
|
||||
abstract ServerCache naturalKeyCache();
|
||||
|
||||
abstract ServerCache beanCache();
|
||||
|
||||
/**
|
||||
* Derive the cache notify flags.
|
||||
*/
|
||||
void deriveNotifyFlags() {
|
||||
cacheNotifyOnAll = (invalidateQueryCache || beanCache != null || queryCache != null);
|
||||
cacheNotifyOnAll = (invalidateQueryCache || hasBeanCache() || hasQueryCache());
|
||||
cacheNotifyOnDelete = !cacheNotifyOnAll && isNotifyOnDeletes();
|
||||
cacheNotifyOneToOneOwner = hasOwningOneToOneWithCachedTarget();
|
||||
if (log.isLoggable(DEBUG)) {
|
||||
if (cacheNotifyOnAll || cacheNotifyOnDelete) {
|
||||
String notifyMode = cacheNotifyOnAll ? "All" : "Delete";
|
||||
if (cacheNotifyOnAll || cacheNotifyOnDelete || cacheNotifyOneToOneOwner) {
|
||||
String notifyMode = cacheNotifyOnAll ? "All" : (cacheNotifyOnDelete ? "Delete" : "OneToOneOwner");
|
||||
log.log(DEBUG, "l2 caching on {0} - beanCaching:{1} queryCaching:{2} notifyMode:{3} ",
|
||||
desc.fullName(), isBeanCaching(), isQueryCaching(), notifyMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasOwningOneToOneWithCachedTarget() {
|
||||
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
|
||||
if (imported.isCacheNotifyOwningOneToOne()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if there is an imported bi-directional relationship to a bea
|
||||
* that does have bean caching enabled.
|
||||
@@ -137,6 +155,9 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
if (hasImmutableCaches()) {
|
||||
return true;
|
||||
}
|
||||
if (cacheNotifyOneToOneOwner) {
|
||||
return true;
|
||||
}
|
||||
return cacheRegion.isEnabled()
|
||||
&& (cacheNotifyOnAll || cacheNotifyOnDelete && (type == PersistRequest.Type.DELETE || type == PersistRequest.Type.DELETE_PERMANENT));
|
||||
}
|
||||
@@ -184,11 +205,11 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Clear the query cache.
|
||||
*/
|
||||
void queryCacheClear() {
|
||||
if (queryCache != null) {
|
||||
if (hasQueryCache()) {
|
||||
if (queryLog.isLoggable(DEBUG)) {
|
||||
queryLog.log(DEBUG, " CLEAR {0}", cacheName);
|
||||
}
|
||||
queryCache.clear();
|
||||
queryCache().clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +217,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Add query cache clear to the changeSet.
|
||||
*/
|
||||
private void queryCacheClear(CacheChangeSet changeSet) {
|
||||
if (queryCache != null) {
|
||||
if (hasQueryCache()) {
|
||||
changeSet.addClearQuery(desc);
|
||||
}
|
||||
}
|
||||
@@ -205,10 +226,10 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Get a query result from the query cache.
|
||||
*/
|
||||
Object queryCacheGet(Object id) {
|
||||
if (queryCache == null) {
|
||||
if (!hasQueryCache()) {
|
||||
throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)");
|
||||
}
|
||||
Object queryResult = queryCache.get(id);
|
||||
Object queryResult = queryCache().get(id);
|
||||
if (queryLog.isLoggable(DEBUG)) {
|
||||
if (queryResult == null) {
|
||||
queryLog.log(DEBUG, " GET {0}({1}) - cache miss", cacheName, id);
|
||||
@@ -223,13 +244,13 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Put a query result into the query cache.
|
||||
*/
|
||||
void queryCachePut(Object id, QueryCacheEntry entry) {
|
||||
if (queryCache == null) {
|
||||
if (!hasQueryCache()) {
|
||||
throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)");
|
||||
}
|
||||
if (queryLog.isLoggable(DEBUG)) {
|
||||
queryLog.log(DEBUG, " PUT {0}({1})", cacheName, id);
|
||||
}
|
||||
queryCache.put(id, entry);
|
||||
queryCache().put(id, entry);
|
||||
}
|
||||
|
||||
void manyPropRemove(String propertyName, String parentKey) {
|
||||
@@ -300,7 +321,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
*/
|
||||
void manyPropPut(BeanPropertyAssocMany<?> many, Object details, String parentKey) {
|
||||
if (many.isElementCollection()) {
|
||||
CachedBeanData data = (CachedBeanData) beanCache.get(parentKey);
|
||||
CachedBeanData data = (CachedBeanData) beanCache().get(parentKey);
|
||||
if (data != null) {
|
||||
try {
|
||||
// add as JSON to bean cache
|
||||
@@ -312,7 +333,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
if (beanLog.isLoggable(DEBUG)) {
|
||||
beanLog.log(DEBUG, " UPDATE {0}({1}) changes:{2}", cacheName, parentKey, changes);
|
||||
}
|
||||
beanCache.put(parentKey, newData);
|
||||
beanCache().put(parentKey, newData);
|
||||
} catch (IOException e) {
|
||||
log.log(ERROR, "Error updating L2 cache", e);
|
||||
}
|
||||
@@ -351,14 +372,15 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Hit the bean cache with the given ids returning the hits.
|
||||
*/
|
||||
BeanCacheResult<T> cacheIdLookup(PersistenceContext context, boolean unmodifiable, Collection<?> ids) {
|
||||
Set<Object> keys = new HashSet<>(ids.size());
|
||||
for (Object id : ids) {
|
||||
keys.add(desc.cacheKey(id));
|
||||
}
|
||||
if (ids.isEmpty()) {
|
||||
return new BeanCacheResult<>();
|
||||
}
|
||||
Map<Object, Object> beanDataMap = beanCache.getAll(keys);
|
||||
// map cacheKey -> original id to support type coercion
|
||||
Map<Object, Object> keyToOriginalId = new HashMap<>(ids.size());
|
||||
for (Object id : ids) {
|
||||
keyToOriginalId.put(desc.cacheKey(id), id);
|
||||
}
|
||||
Map<Object, Object> beanDataMap = beanCache().getAll(keyToOriginalId.keySet());
|
||||
if (beanLog.isLoggable(TRACE)) {
|
||||
beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet());
|
||||
}
|
||||
@@ -366,7 +388,8 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
for (Map.Entry<Object, Object> entry : beanDataMap.entrySet()) {
|
||||
CachedBeanData cachedBeanData = (CachedBeanData) entry.getValue();
|
||||
T bean = convertToBean(entry.getKey(), unmodifiable, context, cachedBeanData);
|
||||
result.add(bean, desc.id(bean));
|
||||
Object originalId = keyToOriginalId.get(entry.getKey());
|
||||
result.add(bean, originalId != null ? originalId : desc.id(bean));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -380,7 +403,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
}
|
||||
|
||||
// naturalKey -> Id map
|
||||
Map<Object, Object> naturalKeyMap = naturalKeyCache.getAll(keys);
|
||||
Map<Object, Object> naturalKeyMap = naturalKeyCache().getAll(keys);
|
||||
if (natLog.isLoggable(TRACE)) {
|
||||
natLog.log(TRACE, " MLOOKUP {0}({1}) - hits:{2}", cacheName, keys, naturalKeyMap);
|
||||
}
|
||||
@@ -397,7 +420,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
}
|
||||
|
||||
Set<Object> ids = new HashSet<>(naturalKeyMap.values());
|
||||
Map<Object, Object> beanDataMap = beanCache.getAll(ids);
|
||||
Map<Object, Object> beanDataMap = beanCache().getAll(ids);
|
||||
if (beanLog.isLoggable(TRACE)) {
|
||||
beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet());
|
||||
}
|
||||
@@ -428,25 +451,15 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
desc.contextPut(context, id, bean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the beanCache creating it if necessary.
|
||||
*/
|
||||
private ServerCache getBeanCache() {
|
||||
if (beanCache == null) {
|
||||
throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation.");
|
||||
}
|
||||
return beanCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the bean cache.
|
||||
*/
|
||||
void beanCacheClear() {
|
||||
if (beanCache != null) {
|
||||
if (hasBeanCache()) {
|
||||
if (beanLog.isLoggable(DEBUG)) {
|
||||
beanLog.log(DEBUG, " CLEAR {0}", cacheName);
|
||||
}
|
||||
beanCache.clear();
|
||||
beanCache().clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,13 +529,13 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
if (beanLog.isLoggable(DEBUG)) {
|
||||
beanLog.log(DEBUG, " MPUT {0}({1})", cacheName, map.keySet());
|
||||
}
|
||||
getBeanCache().putAll(map);
|
||||
beanCache().putAll(map);
|
||||
|
||||
if (natKeys != null && !natKeys.isEmpty()) {
|
||||
if (natLog.isLoggable(DEBUG)) {
|
||||
natLog.log(DEBUG, " MPUT {0}({1}, {2})", cacheName, Arrays.toString(naturalKey), natKeys.keySet());
|
||||
}
|
||||
naturalKeyCache.putAll(natKeys);
|
||||
naturalKeyCache().putAll(natKeys);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,14 +548,14 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
if (beanLog.isLoggable(DEBUG)) {
|
||||
beanLog.log(DEBUG, " PUT {0}({1}) data:{2}", cacheName, key, beanData);
|
||||
}
|
||||
getBeanCache().put(key, beanData);
|
||||
beanCache().put(key, beanData);
|
||||
if (naturalKey != null) {
|
||||
String naturalKey = calculateNaturalKey(beanData);
|
||||
if (naturalKey != null) {
|
||||
if (natLog.isLoggable(DEBUG)) {
|
||||
natLog.log(DEBUG, " PUT {0}({1}, {2})", cacheName, naturalKey, key);
|
||||
}
|
||||
naturalKeyCache.put(naturalKey, key);
|
||||
naturalKeyCache().put(naturalKey, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -564,7 +577,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
}
|
||||
|
||||
CachedBeanData beanCacheGetData(String key) {
|
||||
return (CachedBeanData) getBeanCache().get(key);
|
||||
return (CachedBeanData) beanCache().get(key);
|
||||
}
|
||||
|
||||
T beanCacheGet(String key, boolean unmodifiable, PersistenceContext context) {
|
||||
@@ -579,7 +592,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Return a bean from the bean cache.
|
||||
*/
|
||||
private T beanCacheGetInternal(String key, boolean unmodifiable, PersistenceContext context) {
|
||||
CachedBeanData data = (CachedBeanData) getBeanCache().get(key);
|
||||
CachedBeanData data = (CachedBeanData) beanCache().get(key);
|
||||
if (data == null) {
|
||||
if (beanLog.isLoggable(TRACE)) {
|
||||
beanLog.log(TRACE, " GET {0}({1}) - cache miss", cacheName, key);
|
||||
@@ -684,11 +697,11 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Remove a bean from the cache given its Id.
|
||||
*/
|
||||
void beanCacheApplyInvalidate(Collection<String> keys) {
|
||||
if (beanCache != null) {
|
||||
if (hasBeanCache()) {
|
||||
if (beanLog.isLoggable(DEBUG)) {
|
||||
beanLog.log(DEBUG, " MREMOVE {0}({1})", cacheName, keys);
|
||||
}
|
||||
beanCache.removeAll(new HashSet<>(keys));
|
||||
beanCache().removeAll(new HashSet<>(keys));
|
||||
}
|
||||
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
|
||||
imported.cacheClear();
|
||||
@@ -704,7 +717,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
ebis.put(desc.cacheKeyForBean(ebi.owner()), ebi);
|
||||
}
|
||||
|
||||
Map<Object, Object> hits = getBeanCache().getAll(ebis.keySet());
|
||||
Map<Object, Object> hits = beanCache().getAll(ebis.keySet());
|
||||
if (beanLog.isLoggable(TRACE)) {
|
||||
beanLog.log(TRACE, " MLOAD {0}({1}) - got hits ({2})", cacheName, ebis.keySet(), hits.size());
|
||||
}
|
||||
@@ -737,7 +750,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Returns true if it managed to populate/load the single bean from the cache.
|
||||
*/
|
||||
boolean beanCacheLoad(EntityBean bean, EntityBeanIntercept ebi, String key, PersistenceContext context) {
|
||||
CachedBeanData cacheData = (CachedBeanData) getBeanCache().get(key);
|
||||
CachedBeanData cacheData = (CachedBeanData) beanCache().get(key);
|
||||
if (cacheData == null) {
|
||||
if (beanLog.isLoggable(TRACE)) {
|
||||
beanLog.log(TRACE, " LOAD {0}({1}) - cache miss", cacheName, key);
|
||||
@@ -759,7 +772,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
}
|
||||
|
||||
void cacheUpdateQuery(boolean update, SpiTransaction transaction) {
|
||||
if (invalidateQueryCache || cacheNotifyOnAll || (!update && cacheNotifyOnDelete)) {
|
||||
if (invalidateQueryCache || cacheNotifyOnAll || cacheNotifyOneToOneOwner || (!update && cacheNotifyOnDelete)) {
|
||||
transaction.event().add(desc.baseTable(), false, update, !update);
|
||||
}
|
||||
}
|
||||
@@ -775,10 +788,11 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
changeSet.addInvalidate(desc);
|
||||
} else {
|
||||
queryCacheClear(changeSet);
|
||||
if (beanCache != null) {
|
||||
if (hasBeanCache()) {
|
||||
changeSet.addBeanRemoveMany(desc, ids);
|
||||
}
|
||||
cacheDeleteImported(true, null, changeSet);
|
||||
cacheClearImportedOneToOne(changeSet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -793,10 +807,11 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
changeSet.addInvalidate(desc);
|
||||
} else {
|
||||
queryCacheClear(changeSet);
|
||||
if (beanCache != null) {
|
||||
if (hasBeanCache()) {
|
||||
changeSet.addBeanRemove(desc, id);
|
||||
}
|
||||
cacheDeleteImported(true, deleteRequest.entityBean(), changeSet);
|
||||
cacheDeleteImportedOneToOne(deleteRequest.entityBean(), changeSet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -809,6 +824,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
} else {
|
||||
queryCacheClear(changeSet);
|
||||
cacheDeleteImported(false, insertRequest.entityBean(), changeSet);
|
||||
cacheDeleteImportedOneToOne(insertRequest.entityBean(), changeSet);
|
||||
changeSet.addBeanInsert(desc.baseTable());
|
||||
}
|
||||
}
|
||||
@@ -819,6 +835,42 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict the target bean cache for each owning OneToOne property on insert or delete.
|
||||
*/
|
||||
private void cacheDeleteImportedOneToOne(EntityBean entityBean, CacheChangeSet changeSet) {
|
||||
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
|
||||
imported.cacheDeleteOneToOneOwned(entityBean, changeSet);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the target bean cache for each owning OneToOne property on bulk delete-by-ids.
|
||||
*/
|
||||
private void cacheClearImportedOneToOne(CacheChangeSet changeSet) {
|
||||
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
|
||||
imported.cacheClearOneToOneOwned(changeSet);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict inverse caches for any imported FK properties whose value changed in this update.
|
||||
* Handles collection-ids caches (ManyToOne FK reassignment) and bean caches (owning OneToOne FK change).
|
||||
*/
|
||||
private void cacheUpdateImportedFKs(PersistRequestBean<T> updateRequest, CacheChangeSet changeSet) {
|
||||
boolean[] dirty = updateRequest.dirtyProperties();
|
||||
if (dirty == null) {
|
||||
return;
|
||||
}
|
||||
EntityBean entityBean = updateRequest.entityBean();
|
||||
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
|
||||
int propIdx = imported.propertyIndex();
|
||||
if (propIdx < dirty.length && dirty[propIdx]) {
|
||||
imported.cacheUpdateFKchange(entityBean, updateRequest.origValue(imported), changeSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add appropriate changes to support update.
|
||||
*/
|
||||
@@ -831,7 +883,8 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
|
||||
} else {
|
||||
queryCacheClear(changeSet);
|
||||
if (beanCache == null) {
|
||||
cacheUpdateImportedFKs(updateRequest, changeSet);
|
||||
if (!hasBeanCache()) {
|
||||
// query caching only
|
||||
return;
|
||||
}
|
||||
@@ -861,15 +914,17 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
changeSet.addInvalidate(desc);
|
||||
return;
|
||||
}
|
||||
if (noCaching) {
|
||||
if (noCaching && !cacheNotifyOneToOneOwner) {
|
||||
return;
|
||||
}
|
||||
changeSet.addClearQuery(desc);
|
||||
// inserts don't invalidate the bean cache
|
||||
if (tableIUD.isUpdateOrDelete()) {
|
||||
changeSet.addClearBean(desc);
|
||||
if (!noCaching) {
|
||||
changeSet.addClearQuery(desc);
|
||||
// inserts don't invalidate the bean cache
|
||||
if (tableIUD.isUpdateOrDelete()) {
|
||||
changeSet.addClearBean(desc);
|
||||
}
|
||||
}
|
||||
// any change invalidates the collection IDs cache
|
||||
// any change invalidates the collection IDs cache and owning OneToOne target bean caches
|
||||
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
|
||||
imported.cacheClear(changeSet);
|
||||
}
|
||||
@@ -877,7 +932,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
|
||||
void cacheNaturalKeyPut(String key, String newKey) {
|
||||
if (newKey != null) {
|
||||
naturalKeyCache.put(newKey, key);
|
||||
naturalKeyCache().put(newKey, key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,7 +940,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Apply changes to the bean cache entry.
|
||||
*/
|
||||
void cacheBeanUpdate(String key, Map<String, Object> changes, boolean updateNaturalKey, long version) {
|
||||
ServerCache cache = getBeanCache();
|
||||
ServerCache cache = beanCache();
|
||||
CachedBeanData existingData = (CachedBeanData) cache.get(key);
|
||||
if (existingData != null) {
|
||||
long currentVersion = existingData.getVersion();
|
||||
@@ -910,7 +965,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
if (natLog.isLoggable(DEBUG)) {
|
||||
natLog.log(DEBUG, ".. update {0} REMOVE({1}) - old key for ({2})", cacheName, oldKey, key);
|
||||
}
|
||||
naturalKeyCache.remove(oldKey);
|
||||
naturalKeyCache().remove(oldKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package io.ebeaninternal.server.deploy;
|
||||
|
||||
import io.ebean.cache.ServerCache;
|
||||
import io.ebeaninternal.server.cache.SpiCacheManager;
|
||||
import io.ebeaninternal.server.core.CacheOptions;
|
||||
|
||||
/**
|
||||
* BeanDescriptorCacheHelp implementation for non-tenant-partitioned (fixed) caches.
|
||||
* Cache instances are obtained once at construction time and reused for all requests.
|
||||
*
|
||||
* @param <T> The entity bean type
|
||||
*/
|
||||
final class BeanDescriptorCacheHelpFixed<T> extends BeanDescriptorCacheHelp<T> {
|
||||
|
||||
private final ServerCache beanCache;
|
||||
private final ServerCache naturalKeyCache;
|
||||
private final ServerCache queryCache;
|
||||
|
||||
BeanDescriptorCacheHelpFixed(BeanDescriptor<T> desc, SpiCacheManager cacheManager, CacheOptions cacheOptions,
|
||||
boolean cacheSharableBeans, BeanPropertyAssocOne<?>[] propertiesOneImported) {
|
||||
super(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported);
|
||||
if (!cacheOptions.isEnableQueryCache()) {
|
||||
this.queryCache = null;
|
||||
} else {
|
||||
this.queryCache = cacheManager.getQueryCache(beanType);
|
||||
}
|
||||
if (cacheOptions.isEnableBeanCache()) {
|
||||
this.beanCache = cacheManager.getBeanCache(beanType);
|
||||
this.naturalKeyCache = (cacheOptions.getNaturalKey() != null) ? cacheManager.getNaturalKeyCache(beanType) : null;
|
||||
} else {
|
||||
this.beanCache = null;
|
||||
this.naturalKeyCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean hasBeanCache() {
|
||||
return beanCache != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean hasQueryCache() {
|
||||
return queryCache != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
ServerCache queryCache() {
|
||||
return queryCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
ServerCache naturalKeyCache() {
|
||||
return naturalKeyCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
ServerCache beanCache() {
|
||||
if (beanCache == null) {
|
||||
throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation.");
|
||||
}
|
||||
return beanCache;
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package io.ebeaninternal.server.deploy;
|
||||
|
||||
import io.ebean.cache.ServerCache;
|
||||
import io.ebeaninternal.server.cache.SpiCacheManager;
|
||||
import io.ebeaninternal.server.core.CacheOptions;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* BeanDescriptorCacheHelp implementation for tenant-partitioned caches.
|
||||
* Cache instances are looked up on every access via the cacheManager (which resolves the
|
||||
* correct tenant-namespaced cache using the current tenant id from the tenant provider).
|
||||
*
|
||||
* @param <T> The entity bean type
|
||||
*/
|
||||
final class BeanDescriptorCacheHelpPartitioned<T> extends BeanDescriptorCacheHelp<T> {
|
||||
|
||||
private final Supplier<ServerCache> beanCacheSupplier;
|
||||
private final Supplier<ServerCache> naturalKeyCacheSupplier;
|
||||
private final Supplier<ServerCache> queryCacheSupplier;
|
||||
|
||||
BeanDescriptorCacheHelpPartitioned(BeanDescriptor<T> desc, SpiCacheManager cacheManager, CacheOptions cacheOptions,
|
||||
boolean cacheSharableBeans, BeanPropertyAssocOne<?>[] propertiesOneImported) {
|
||||
super(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported);
|
||||
this.queryCacheSupplier = cacheOptions.isEnableQueryCache() ? () -> cacheManager.getQueryCache(beanType) : null;
|
||||
if (cacheOptions.isEnableBeanCache()) {
|
||||
this.beanCacheSupplier = () -> cacheManager.getBeanCache(beanType);
|
||||
this.naturalKeyCacheSupplier = (cacheOptions.getNaturalKey() != null) ? () -> cacheManager.getNaturalKeyCache(beanType) : null;
|
||||
} else {
|
||||
this.beanCacheSupplier = null;
|
||||
this.naturalKeyCacheSupplier = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean hasBeanCache() {
|
||||
return beanCacheSupplier != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean hasQueryCache() {
|
||||
return queryCacheSupplier != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
ServerCache queryCache() {
|
||||
return queryCacheSupplier.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
ServerCache naturalKeyCache() {
|
||||
return naturalKeyCacheSupplier.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
ServerCache beanCache() {
|
||||
if (beanCacheSupplier == null) {
|
||||
throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation.");
|
||||
}
|
||||
return beanCacheSupplier.get();
|
||||
}
|
||||
}
|
||||
@@ -907,8 +907,18 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
|
||||
}
|
||||
|
||||
private void makeOrderColumn(DeployBeanPropertyAssocMany<?> oneToMany) {
|
||||
DeployBeanDescriptor<?> targetDesc = targetDescriptor(oneToMany);
|
||||
DeployOrderColumn orderColumn = oneToMany.getOrderColumn();
|
||||
makeOrderColumn(oneToMany, targetDescriptor(oneToMany));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and assign the synthetic order column property onto the given target descriptor.
|
||||
* <p>
|
||||
* Used for both {@code @OneToMany} (targetDesc looked up via the target entity type) and
|
||||
* {@code @ElementCollection} (targetDesc is the synthetic element descriptor which is not
|
||||
* registered in {@code deployInfoMap} and so must be passed in directly).
|
||||
*/
|
||||
public void makeOrderColumn(DeployBeanPropertyAssocMany<?> many, DeployBeanDescriptor<?> targetDesc) {
|
||||
DeployOrderColumn orderColumn = many.getOrderColumn();
|
||||
final ScalarType<?> scalarType = typeManager.type(Integer.class);
|
||||
DeployBeanProperty orderProperty = new DeployBeanProperty(targetDesc, Integer.class, scalarType, null);
|
||||
orderProperty.setName(DeployOrderColumn.LOGICAL_NAME);
|
||||
|
||||
@@ -671,6 +671,35 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
|
||||
setValue(entityBean, tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, getIntercept and setIntercept will check if the passed bean is an instance of the descriptor type.
|
||||
* <p>
|
||||
* If the property is not part of the type hierarchy (i.e. is not this property from this descriptor) an
|
||||
* IllegalArgumentException is thrown.
|
||||
* <p>
|
||||
* If inheritance is involved, this method returns false instead of throwing an exception, if the property might
|
||||
* exist on one of the sibling child beans. This is necessary for getIntercept, as it returns <code>null</code>
|
||||
* in this case.
|
||||
*
|
||||
* @return true if the property can be accessed on the given bean, false if it should be treated as unloaded.
|
||||
*/
|
||||
private boolean checkPropertyAccess(EntityBean bean) {
|
||||
if (bean == null || descriptor.type().isInstance(bean)) { // null = fall through - NPE is caught later.
|
||||
return true;
|
||||
}
|
||||
InheritInfo inheritInfo = descriptor.inheritInfo();
|
||||
if (inheritInfo == null || inheritInfo.isRoot() || !inheritInfo.getRoot().getType().isInstance(bean)) {
|
||||
throw new IllegalArgumentException(propertyIncompatibleMsg(bean));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String propertyIncompatibleMsg(EntityBean bean) {
|
||||
String beanType = bean == null ? "null" : bean.getClass().getName();
|
||||
return "Property " + name + " on [" + descriptor + "] is incompatible with type[" + beanType + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of the property without interception or
|
||||
* PropertyChangeSupport.
|
||||
@@ -687,6 +716,9 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
|
||||
* Set the value of the property.
|
||||
*/
|
||||
public void setValueIntercept(EntityBean bean, Object value) {
|
||||
if (!checkPropertyAccess(bean)) {
|
||||
throw new IllegalArgumentException(propertyIncompatibleMsg(bean));
|
||||
}
|
||||
try {
|
||||
setter.setIntercept(bean, value);
|
||||
} catch (Exception ex) {
|
||||
@@ -798,6 +830,9 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
|
||||
}
|
||||
|
||||
public Object getValueIntercept(EntityBean bean) {
|
||||
if (!checkPropertyAccess(bean)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return getter.getIntercept(bean);
|
||||
} catch (Exception ex) {
|
||||
|
||||
@@ -161,13 +161,7 @@ public abstract class BeanPropertyAssoc<T> extends BeanProperty implements STree
|
||||
ElPropertyValue createElPropertyValue(String propName, String remainder, ElPropertyChainBuilder chain, boolean propertyDeploy) {
|
||||
// associated or embedded bean
|
||||
BeanDescriptor<?> embDesc = targetDescriptor();
|
||||
if (chain == null) {
|
||||
chain = new ElPropertyChainBuilder(isEmbedded(), propName);
|
||||
}
|
||||
chain.add(this);
|
||||
if (containsMany()) {
|
||||
chain.setContainsMany();
|
||||
}
|
||||
return embDesc.buildElGetValue(remainder, chain, propertyDeploy);
|
||||
}
|
||||
|
||||
@@ -217,6 +211,10 @@ public abstract class BeanPropertyAssoc<T> extends BeanProperty implements STree
|
||||
* Return the BeanDescriptor of the target.
|
||||
*/
|
||||
public BeanDescriptor<T> targetDescriptor() {
|
||||
if (targetDescriptor == null) {
|
||||
// lazily resolve for association properties inside an @Embeddable used as override copy
|
||||
targetDescriptor = descriptor.descriptor(targetType);
|
||||
}
|
||||
return targetDescriptor;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,12 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
* Flag to indicate that the target has a order column to auto populate.
|
||||
*/
|
||||
private final boolean hasOrderColumn;
|
||||
/**
|
||||
* For ManyToMany, the db column name of the order column stored on the intersection
|
||||
* table (null for OneToMany/ElementCollection which use a target descriptor property instead).
|
||||
*/
|
||||
private final String intersectionOrderColumn;
|
||||
private final boolean intersectionOrderColumnNullable;
|
||||
/**
|
||||
* Flag to indicate manyToMany relationship.
|
||||
*/
|
||||
@@ -95,6 +101,8 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
this.o2mJoinTable = deploy.isO2mJoinTable();
|
||||
this.hasOrderColumn = deploy.hasOrderColumn();
|
||||
this.manyToMany = deploy.isManyToMany();
|
||||
this.intersectionOrderColumn = (manyToMany && hasOrderColumn) ? deploy.getOrderColumn().getName() : null;
|
||||
this.intersectionOrderColumnNullable = (manyToMany && hasOrderColumn) && deploy.getOrderColumn().isNullable();
|
||||
this.elementCollection = deploy.isElementCollection();
|
||||
this.elementDescriptor = deploy.getElementDescriptor();
|
||||
this.manyType = deploy.getManyType();
|
||||
@@ -154,6 +162,9 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
embeddedExportedProperties = exportedProperties[0].isEmbedded();
|
||||
if (fetchOrderBy != null) {
|
||||
lazyFetchOrderBy = sqlHelp.lazyFetchOrderBy(fetchOrderBy);
|
||||
} else if (intersectionOrderColumn != null) {
|
||||
// ManyToMany @OrderColumn - the intersection table is always aliased "int_"
|
||||
lazyFetchOrderBy = sqlHelp.lazyFetchOrderBy("int_." + intersectionOrderColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,6 +522,29 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
return hasOrderColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this is a ManyToMany with an order column stored on the intersection table.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasIntersectionOrderColumn() {
|
||||
return intersectionOrderColumn != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the db column name of the ManyToMany intersection table order column (or null).
|
||||
*/
|
||||
@Override
|
||||
public String intersectionOrderColumn() {
|
||||
return intersectionOrderColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the intersection table order column is nullable.
|
||||
*/
|
||||
public boolean isIntersectionOrderColumnNullable() {
|
||||
return intersectionOrderColumnNullable;
|
||||
}
|
||||
|
||||
public boolean isOrphanRemoval() {
|
||||
return orphanRemoval;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
|
||||
private final boolean orphanRemoval;
|
||||
private final boolean primaryKeyExport;
|
||||
private final boolean primaryKeyJoin;
|
||||
private final boolean embeddedAllowEmpty;
|
||||
|
||||
private AssocOneHelp localHelp;
|
||||
final BeanProperty[] embeddedProps;
|
||||
@@ -54,6 +55,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
|
||||
private String deleteByParentIdInSql;
|
||||
private BeanPropertyAssocMany<?> relationshipProperty;
|
||||
private boolean cacheNotifyRelationship;
|
||||
private boolean cacheNotifyOwningOneToOne;
|
||||
|
||||
/**
|
||||
* Create based on deploy information of an EmbeddedId.
|
||||
@@ -73,6 +75,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
|
||||
oneToOne = deploy.isOneToOne();
|
||||
oneToOneExported = deploy.isOneToOneExported();
|
||||
orphanRemoval = deploy.isOrphanRemoval();
|
||||
embeddedAllowEmpty = deploy.isEmbeddedAllowEmpty();
|
||||
if (embedded) {
|
||||
// Overriding of the columns and use table alias of owning BeanDescriptor
|
||||
BeanEmbeddedMeta overrideMeta = BeanEmbeddedMetaFactory.create(owner, deploy);
|
||||
@@ -99,6 +102,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
|
||||
oneToOne = source.oneToOne;
|
||||
oneToOneExported = source.oneToOneExported;
|
||||
orphanRemoval = source.orphanRemoval;
|
||||
embeddedAllowEmpty = source.embeddedAllowEmpty;
|
||||
embeddedProps = null;
|
||||
embeddedPropsMap = null;
|
||||
}
|
||||
@@ -143,6 +147,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
|
||||
*/
|
||||
void initialisePostTarget() {
|
||||
this.cacheNotifyRelationship = isCacheNotifyRelationship();
|
||||
this.cacheNotifyOwningOneToOne = oneToOne && !oneToOneExported && targetDescriptor.isBeanCaching();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,18 +170,31 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the L2 relationship cache for this property.
|
||||
* Return true if this owning OneToOne property requires target bean cache eviction on change.
|
||||
*/
|
||||
boolean isCacheNotifyOwningOneToOne() {
|
||||
return cacheNotifyOwningOneToOne;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the L2 relationship cache for this property (used from beanCacheApplyInvalidate).
|
||||
*/
|
||||
void cacheClear() {
|
||||
if (cacheNotifyRelationship) {
|
||||
targetDescriptor.cacheManyPropClear(relationshipProperty.name());
|
||||
}
|
||||
if (cacheNotifyOwningOneToOne) {
|
||||
targetDescriptor.clearBeanCache();
|
||||
}
|
||||
}
|
||||
|
||||
void cacheClear(CacheChangeSet changeSet) {
|
||||
if (cacheNotifyRelationship) {
|
||||
changeSet.addManyClear(targetDescriptor, relationshipProperty.name());
|
||||
}
|
||||
if (cacheNotifyOwningOneToOne) {
|
||||
changeSet.addClearBean(targetDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,6 +217,66 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict the target's bean cache entry when this owning OneToOne bean is inserted or deleted.
|
||||
*/
|
||||
void cacheDeleteOneToOneOwned(EntityBean bean, CacheChangeSet changeSet) {
|
||||
if (cacheNotifyOwningOneToOne) {
|
||||
Object assocBean = getValue(bean);
|
||||
if (assocBean != null) {
|
||||
Object targetId = targetDescriptor.id(assocBean);
|
||||
if (targetId != null) {
|
||||
changeSet.addBeanRemove(targetDescriptor, targetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the target's entire bean cache when owning OneToOne beans are deleted by IDs.
|
||||
*/
|
||||
void cacheClearOneToOneOwned(CacheChangeSet changeSet) {
|
||||
if (cacheNotifyOwningOneToOne) {
|
||||
changeSet.addClearBean(targetDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict the inverse caches for BOTH old and new targets when the FK changes on update.
|
||||
* Handles collection-ids caches (ManyToOne) and bean caches (owning OneToOne).
|
||||
*/
|
||||
void cacheUpdateFKchange(EntityBean bean, Object origAssocBean, CacheChangeSet changeSet) {
|
||||
Object newAssocBean = getValue(bean);
|
||||
if (cacheNotifyRelationship) {
|
||||
if (origAssocBean != null) {
|
||||
Object oldId = targetDescriptor.id(origAssocBean);
|
||||
if (oldId != null) {
|
||||
changeSet.addManyRemove(targetDescriptor, relationshipProperty.name(), targetDescriptor.cacheKey(oldId));
|
||||
}
|
||||
}
|
||||
if (newAssocBean != null) {
|
||||
Object newId = targetDescriptor.id(newAssocBean);
|
||||
if (newId != null) {
|
||||
changeSet.addManyRemove(targetDescriptor, relationshipProperty.name(), targetDescriptor.cacheKey(newId));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cacheNotifyOwningOneToOne) {
|
||||
if (origAssocBean != null) {
|
||||
Object oldId = targetDescriptor.id(origAssocBean);
|
||||
if (oldId != null) {
|
||||
changeSet.addBeanRemove(targetDescriptor, oldId);
|
||||
}
|
||||
}
|
||||
if (newAssocBean != null) {
|
||||
Object newId = targetDescriptor.id(newAssocBean);
|
||||
if (newId != null) {
|
||||
changeSet.addBeanRemove(targetDescriptor, newId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
Object naturalKeyVal(Map<String, Object> values) {
|
||||
EntityBean bean = (EntityBean) values.get(name);
|
||||
@@ -211,15 +289,31 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
|
||||
@Override
|
||||
public ElPropertyValue buildElPropertyValue(String propName, String remainder, ElPropertyChainBuilder chain, boolean propertyDeploy) {
|
||||
if (embedded) {
|
||||
BeanProperty embProp = embeddedPropsMap.get(remainder);
|
||||
String embName = remainder;
|
||||
String embRemainder = null;
|
||||
int basePos = remainder.indexOf('.');
|
||||
if (basePos > -1) {
|
||||
embName = remainder.substring(0, basePos);
|
||||
embRemainder = remainder.substring(basePos + 1);
|
||||
}
|
||||
BeanProperty embProp = embeddedPropsMap.get(embName);
|
||||
|
||||
if (embProp == null) {
|
||||
String msg = "Embedded Property " + remainder + " not found in " + fullName();
|
||||
String msg = "Embedded Property " + embName + " not found in " + fullName();
|
||||
throw new PersistenceException(msg);
|
||||
}
|
||||
if (chain == null) {
|
||||
chain = new ElPropertyChainBuilder(true, propName);
|
||||
}
|
||||
chain.add(this);
|
||||
if (embRemainder != null) {
|
||||
if (embProp instanceof BeanPropertyAssocOne) {
|
||||
// @ManyToOne using the overridden property, e.g. "ma_country_code" instead of "country_code")
|
||||
return embProp.buildElPropertyValue(propName, embRemainder, chain, propertyDeploy);
|
||||
}
|
||||
// scalar (or non-navigable) leaf with a further path segment - invalid path
|
||||
String msg = "Embedded Property " + embName + "." + embRemainder + " not found in " + fullName();
|
||||
throw new PersistenceException(msg);
|
||||
}
|
||||
// direct scalar/assoc access within embedded — mark as embedded so the prefix
|
||||
// strips the embedded segment (keeps parent table alias, not the embedded segment)
|
||||
chain.setEmbedded(true);
|
||||
return chain.add(embProp).build();
|
||||
}
|
||||
@@ -749,7 +843,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
|
||||
|
||||
private AssocOneHelp createHelp(boolean embedded, boolean oneToOneExported, String embeddedPrefix) {
|
||||
if (embedded) {
|
||||
return new AssocOneHelpEmbedded(this);
|
||||
return new AssocOneHelpEmbedded(this, embeddedAllowEmpty);
|
||||
} else if (oneToOneExported) {
|
||||
return new AssocOneHelpRefExported(this);
|
||||
} else {
|
||||
|
||||
@@ -234,6 +234,10 @@ public final class IdBinderSimple implements IdBinder {
|
||||
@Override
|
||||
public Object convertId(Object idValue) {
|
||||
if (!idValue.getClass().equals(expectedType)) {
|
||||
if (idValue instanceof String) {
|
||||
// for cacheKey() formatted values
|
||||
return scalarType.parse((String) idValue);
|
||||
}
|
||||
return scalarType.toBeanType(idValue);
|
||||
}
|
||||
return idValue;
|
||||
|
||||
+9
@@ -13,6 +13,7 @@ public final class DeployBeanPropertyAssocOne<T> extends DeployBeanPropertyAssoc
|
||||
private boolean oneToOneExported;
|
||||
private boolean primaryKeyJoin;
|
||||
private boolean primaryKeyExport;
|
||||
private boolean embeddedAllowEmpty;
|
||||
private DeployBeanEmbedded deployEmbedded;
|
||||
private String columnPrefix;
|
||||
|
||||
@@ -103,6 +104,14 @@ public final class DeployBeanPropertyAssocOne<T> extends DeployBeanPropertyAssoc
|
||||
}
|
||||
}
|
||||
|
||||
public void setEmbeddedAllowEmpty(boolean allowEmpty) {
|
||||
this.embeddedAllowEmpty = allowEmpty;
|
||||
}
|
||||
|
||||
public boolean isEmbeddedAllowEmpty() {
|
||||
return embeddedAllowEmpty;
|
||||
}
|
||||
|
||||
public void setColumnPrefix(String columnPrefix) {
|
||||
this.columnPrefix = columnPrefix;
|
||||
}
|
||||
|
||||
+15
@@ -89,6 +89,11 @@ final class AnnotationAssocManys extends AnnotationAssoc {
|
||||
ManyToMany manyToMany = get(prop, ManyToMany.class);
|
||||
if (manyToMany != null) {
|
||||
readToMany(manyToMany, prop);
|
||||
OrderColumn orderColumn = get(prop, OrderColumn.class);
|
||||
if (orderColumn != null) {
|
||||
// ManyToMany order value is stored on the intersection table (not the target bean)
|
||||
prop.setOrderColumn(new DeployOrderColumn(orderColumn));
|
||||
}
|
||||
}
|
||||
ElementCollection elementCollection = get(prop, ElementCollection.class);
|
||||
if (elementCollection != null) {
|
||||
@@ -177,6 +182,11 @@ final class AnnotationAssocManys extends AnnotationAssoc {
|
||||
if (!elementCollection.targetClass().equals(void.class)) {
|
||||
prop.setTargetType(elementCollection.targetClass());
|
||||
}
|
||||
OrderColumn orderColumn = get(prop, OrderColumn.class);
|
||||
if (orderColumn != null) {
|
||||
prop.setOrderColumn(new DeployOrderColumn(orderColumn));
|
||||
prop.setFetchOrderBy(DeployOrderColumn.LOGICAL_NAME);
|
||||
}
|
||||
Column column = prop.getMetaAnnotation(Column.class);
|
||||
if (column != null) {
|
||||
prop.setDbColumn(column.name());
|
||||
@@ -268,6 +278,11 @@ final class AnnotationAssocManys extends AnnotationAssoc {
|
||||
|
||||
elementDescriptor.setName(prop.toString());
|
||||
factory.createUnidirectional(elementDescriptor, prop.getOwningType(), beanTable, prop.getTableJoin());
|
||||
if (prop.hasOrderColumn()) {
|
||||
// create the synthetic order property on the element descriptor - the element descriptor
|
||||
// is not registered in deployInfoMap so this can't go through the usual OneToMany path
|
||||
factory.makeOrderColumn(prop, elementDescriptor);
|
||||
}
|
||||
prop.setElementDescriptor(factory.createElementDescriptor(elementDescriptor, prop.getManyType(), scalar));
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,13 @@ final class AnnotationAssocOnes extends AnnotationAssoc {
|
||||
} catch (NoSuchMethodError e) {
|
||||
// using standard JPA API without prefix option, maybe in EE container
|
||||
}
|
||||
try {
|
||||
if (!embedded.nullable()) {
|
||||
prop.setEmbeddedAllowEmpty(true);
|
||||
}
|
||||
} catch (NoSuchMethodError e) {
|
||||
// older persistence-api without nullable() extension
|
||||
}
|
||||
readEmbeddedAttributeOverrides(prop);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,17 +42,19 @@ public final class ElPropertyChain implements ElPropertyValue {
|
||||
private final ScalarType<?> scalarType;
|
||||
private final ElPropertyValue lastElPropertyValue;
|
||||
|
||||
public ElPropertyChain(boolean containsMany, boolean embedded, String expression, ElPropertyValue[] chain) {
|
||||
this.containsMany = containsMany;
|
||||
public ElPropertyChain(String expression, boolean containsMany, boolean embedded, ElPropertyValue[] chain) {
|
||||
this.chain = chain;
|
||||
this.expression = expression;
|
||||
this.containsMany = containsMany;
|
||||
|
||||
int dotPos = expression.lastIndexOf('.');
|
||||
if (dotPos > -1) {
|
||||
this.name = expression.substring(dotPos + 1);
|
||||
if (embedded) {
|
||||
// embedded segments are transparent (share parent table) — strip the embedded
|
||||
// segment from the prefix so the alias points to the parent join, not the embedded
|
||||
int embPos = expression.lastIndexOf('.', dotPos - 1);
|
||||
this.prefix = embPos == -1 ? null : expression.substring(0, embPos);
|
||||
|
||||
} else {
|
||||
this.prefix = expression.substring(0, dotPos);
|
||||
}
|
||||
@@ -61,19 +63,18 @@ public final class ElPropertyChain implements ElPropertyValue {
|
||||
this.name = expression;
|
||||
}
|
||||
|
||||
this.assocId = chain[chain.length - 1].isAssocId();
|
||||
|
||||
this.last = chain.length - 1;
|
||||
this.lastBeanProperty = chain[chain.length - 1].beanProperty();
|
||||
this.last = this.chain.length - 1;
|
||||
this.lastElPropertyValue = this.chain[this.last];
|
||||
this.assocId = this.lastElPropertyValue.isAssocId();
|
||||
this.lastBeanProperty = lastElPropertyValue.beanProperty();
|
||||
if (lastBeanProperty != null) {
|
||||
this.scalarType = lastBeanProperty.scalarType();
|
||||
} else {
|
||||
// case for nested compound type (non-scalar)
|
||||
this.scalarType = null;
|
||||
}
|
||||
this.lastElPropertyValue = chain[chain.length - 1];
|
||||
this.placeHolder = placeHolder(prefix, lastElPropertyValue, false);
|
||||
this.placeHolderEncrypted = placeHolder(prefix, lastElPropertyValue, true);
|
||||
this.placeHolder = placeHolder(this.prefix, lastElPropertyValue, false);
|
||||
this.placeHolderEncrypted = placeHolder(this.prefix, lastElPropertyValue, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,14 +17,13 @@ public final class ElPropertyChainBuilder {
|
||||
|
||||
private final String expression;
|
||||
private final List<ElPropertyValue> chain = new ArrayList<>();
|
||||
private boolean embedded;
|
||||
private boolean containsMany;
|
||||
private boolean containsMany = false;
|
||||
private boolean embedded = false;
|
||||
|
||||
/**
|
||||
* Create with the original expression.
|
||||
*/
|
||||
public ElPropertyChainBuilder(boolean embedded, String expression) {
|
||||
this.embedded = embedded;
|
||||
public ElPropertyChainBuilder(String expression) {
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
@@ -32,14 +31,18 @@ public final class ElPropertyChainBuilder {
|
||||
return containsMany;
|
||||
}
|
||||
|
||||
public void setContainsMany() {
|
||||
this.containsMany = true;
|
||||
}
|
||||
|
||||
public String expression() {
|
||||
return expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the chain as going through an embedded property.
|
||||
* This affects how the prefix (table alias) is computed for SQL generation.
|
||||
*/
|
||||
public void setEmbedded(boolean embedded) {
|
||||
this.embedded = embedded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a ElGetValue element to the chain.
|
||||
*/
|
||||
@@ -48,6 +51,9 @@ public final class ElPropertyChainBuilder {
|
||||
throw new NullPointerException("element null in expression " + expression);
|
||||
}
|
||||
chain.add(element);
|
||||
if (element.containsMany()) {
|
||||
containsMany = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -55,13 +61,6 @@ public final class ElPropertyChainBuilder {
|
||||
* Build the immutable ElGetChain from the build information.
|
||||
*/
|
||||
public ElPropertyChain build() {
|
||||
return new ElPropertyChain(containsMany, embedded, expression, chain.toArray(new ElPropertyValue[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Permits to set whole chain as embedded when the leaf is embedded
|
||||
*/
|
||||
public void setEmbedded(boolean embedded) {
|
||||
this.embedded = embedded;
|
||||
return new ElPropertyChain(expression, containsMany, embedded, chain.toArray(new ElPropertyValue[0]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,4 +91,16 @@ public interface ElPropertyDeploy extends SpiQueryManyJoin {
|
||||
default String fetchOrderBy() {
|
||||
return beanProperty().fetchOrderBy();
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean hasIntersectionOrderColumn() {
|
||||
BeanProperty prop = beanProperty();
|
||||
return prop instanceof io.ebeaninternal.server.deploy.BeanPropertyAssocMany
|
||||
&& prop.hasIntersectionOrderColumn();
|
||||
}
|
||||
|
||||
@Override
|
||||
default String intersectionOrderColumn() {
|
||||
return beanProperty().intersectionOrderColumn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +328,11 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
|
||||
return query.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deletePermanent() {
|
||||
return query.deletePermanent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update() {
|
||||
return query.update();
|
||||
|
||||
@@ -342,6 +342,11 @@ final class JunctionExpression<T> implements SpiJunction<T>, SpiExpression, Expr
|
||||
return exprList.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deletePermanent() {
|
||||
return exprList.deletePermanent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update() {
|
||||
return exprList.update();
|
||||
|
||||
@@ -481,7 +481,7 @@ public final class DefaultPersister implements Persister {
|
||||
saveAssocOne(request);
|
||||
}
|
||||
request.executeOrQueue();
|
||||
if (request.isPersistCascade()) {
|
||||
if (request.isPersistCascade() && !request.isInsertConflictSkipped()) {
|
||||
// save any associated List held beans
|
||||
saveAssocMany(request);
|
||||
}
|
||||
@@ -907,6 +907,11 @@ public final class DefaultPersister implements Persister {
|
||||
* </p>
|
||||
*/
|
||||
private int delete(PersistRequestBean<?> request) {
|
||||
// fire preDelete now, before cascading to children/many's so that the
|
||||
// BeanPersistController/Adapter still sees the bean's collections and
|
||||
// relationships as they are prior to the cascade delete
|
||||
request.controllerPreDelete();
|
||||
|
||||
DeleteUnloadedForeignKeys unloadedForeignKeys = null;
|
||||
if (request.isPersistCascade()) {
|
||||
// delete children first ... register the
|
||||
|
||||
@@ -256,6 +256,12 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
}
|
||||
|
||||
private void saveAssocManyIntersection(boolean queue) {
|
||||
if (many.hasIntersectionOrderColumn()) {
|
||||
// With @OrderColumn the position of every row can change on any add/remove/reorder so
|
||||
// we always delete all intersection rows and reinsert them in the current list order.
|
||||
saveAssocManyIntersectionOrdered(queue);
|
||||
return;
|
||||
}
|
||||
final boolean vanillaCollection = !(value instanceof BeanCollection<?>);
|
||||
if (vanillaCollection || forcedUpdate) {
|
||||
// delete all intersection rows and then treat all
|
||||
@@ -292,6 +298,17 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
manyValue.modifyReset();
|
||||
}
|
||||
|
||||
if (!insertedParent) {
|
||||
// only fire controller when the intersection actually changes:
|
||||
// vanillaCollection/forcedUpdate always do a full replace,
|
||||
// tracked BeanCollection fires only when additions or removals exist
|
||||
boolean intersectionChanged = vanillaCollection || forcedUpdate
|
||||
|| (additions != null && !additions.isEmpty())
|
||||
|| (deletions != null && !deletions.isEmpty());
|
||||
if (intersectionChanged) {
|
||||
request.preManyToManyUpdate();
|
||||
}
|
||||
}
|
||||
transaction.depth(+1);
|
||||
if (deletions != null && !deletions.isEmpty()) {
|
||||
for (Object other : deletions) {
|
||||
@@ -329,6 +346,53 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
transaction.depth(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the ManyToMany intersection rows for a property with an {@code @OrderColumn}.
|
||||
* <p>
|
||||
* Unlike the standard diff based save (additions/removals), this always deletes all existing
|
||||
* intersection rows for the parent and reinserts every current entry in list order, binding
|
||||
* the sequential order index. This is required because a pure reorder (no add/remove) would
|
||||
* not otherwise be detected/persisted, and there is no per-row place (unlike OneToMany/
|
||||
* ElementCollection) to compare an existing 'loaded' order against - the order value lives on
|
||||
* the intersection row, not on the target bean.
|
||||
*/
|
||||
private void saveAssocManyIntersectionOrdered(boolean queue) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
Collection<?> current;
|
||||
if (value instanceof Map<?, ?>) {
|
||||
current = ((Map<?, ?>) value).values();
|
||||
} else if (value instanceof Collection<?>) {
|
||||
current = (Collection<?>) value;
|
||||
} else {
|
||||
throw new PersistenceException("Unhandled ManyToMany type " + value.getClass().getName() + " for " + many.fullName());
|
||||
}
|
||||
if (value instanceof BeanCollection<?>) {
|
||||
BeanCollection<?> manyValue = (BeanCollection<?>) value;
|
||||
setListenMode(manyValue, many);
|
||||
manyValue.modifyReset();
|
||||
}
|
||||
if (!insertedParent) {
|
||||
request.preManyToManyUpdate();
|
||||
persister.deleteManyIntersection(parentBean, many, transaction, publish, queue);
|
||||
}
|
||||
String orderColumn = many.intersectionOrderColumn();
|
||||
transaction.depth(+1);
|
||||
int position = 0;
|
||||
for (Object other : current) {
|
||||
EntityBean otherBean = (EntityBean) other;
|
||||
if (!many.hasImportedId(otherBean)) {
|
||||
throw new PersistenceException("ManyToMany bean does not have an Id value? " + otherBean);
|
||||
}
|
||||
IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherBean, publish);
|
||||
intRow.put(orderColumn, position++);
|
||||
SpiSqlUpdate sqlInsert = intRow.createInsert(server);
|
||||
persister.executeOrQueue(sqlInsert, transaction, queue, BatchControl.INSERT_QUEUE);
|
||||
}
|
||||
transaction.depth(-1);
|
||||
}
|
||||
|
||||
private boolean isChangedProperty() {
|
||||
return request.isChangedProperty(many.propertyIndex());
|
||||
}
|
||||
|
||||
+5
@@ -44,10 +44,15 @@ final class SaveManyElementCollection extends SaveManyBase {
|
||||
private void saveCollection() {
|
||||
SpiSqlUpdate proto = many.insertElementCollection();
|
||||
Object parentId = request.beanId();
|
||||
boolean hasOrderColumn = many.hasOrderColumn();
|
||||
int position = 0;
|
||||
for (Object value : collection) {
|
||||
final SpiSqlUpdate sqlInsert = proto.copy();
|
||||
sqlInsert.setParameter(parentId);
|
||||
many.bindElementValue(sqlInsert, value);
|
||||
if (hasOrderColumn) {
|
||||
sqlInsert.setParameter(position++);
|
||||
}
|
||||
persister.addToFlushQueue(sqlInsert, transaction, BatchControl.INSERT_QUEUE);
|
||||
}
|
||||
resetModifyState();
|
||||
|
||||
@@ -97,7 +97,9 @@ public abstract class DmlHandler implements PersistHandler, BindableRequest {
|
||||
void checkRowCount(int rowCount) throws OptimisticLockException {
|
||||
try {
|
||||
persistRequest.checkRowCount(rowCount);
|
||||
persistRequest.postExecute();
|
||||
if (!persistRequest.isInsertConflictSkipped()) {
|
||||
persistRequest.postExecute();
|
||||
}
|
||||
} catch (OptimisticLockException e) {
|
||||
// add the SQL and bind values to error message
|
||||
final String m = e.getMessage() + " sql[" + sql + "] bind[" + bindLog + "]";
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package io.ebeaninternal.server.persist.dml;
|
||||
|
||||
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.
|
||||
* <p>
|
||||
* SQLite supports the same upsert syntax as Postgres but does not support
|
||||
* ON CONFLICT ON CONSTRAINT - only ON CONFLICT (column-list) is supported.
|
||||
*/
|
||||
final class InsertMetaOptionsSqlite implements InsertMetaOptions {
|
||||
|
||||
private final InsertMeta meta;
|
||||
private final BeanDescriptor<?> desc;
|
||||
private final String baseTable;
|
||||
private final Map<String, String> sqlCache = new ConcurrentHashMap<>();
|
||||
|
||||
InsertMetaOptionsSqlite(InsertMeta meta, BeanDescriptor<?> desc) {
|
||||
this.meta = meta;
|
||||
this.desc = desc;
|
||||
this.baseTable = desc.baseTable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sql(boolean withId, InsertOptions options) {
|
||||
String key = withId + options.key();
|
||||
return sqlCache.computeIfAbsent(key, k -> generate(withId, options));
|
||||
}
|
||||
|
||||
private String generate(boolean withId, InsertOptions options) {
|
||||
char type = options.key().charAt(0);
|
||||
switch (type) {
|
||||
case 'U':
|
||||
return generate(withId, false, options);
|
||||
case 'N':
|
||||
return generate(withId, true, options);
|
||||
default:
|
||||
return meta.sqlFor(withId);
|
||||
}
|
||||
}
|
||||
|
||||
private String generate(boolean withId, boolean doNothing, InsertOptions options) {
|
||||
if (options.constraint() != null) {
|
||||
throw new UnsupportedOperationException("SQLite does not support ON CONFLICT ON CONSTRAINT - use uniqueColumns() instead");
|
||||
}
|
||||
GenerateDmlRequest request = new GenerateDmlRequest();
|
||||
meta.sql(request, !withId, baseTable, false);
|
||||
request.append(" on conflict (");
|
||||
|
||||
List<String> uniqueColumns = desc.uniqueProps().stream()
|
||||
.flatMap(Arrays::stream)
|
||||
.map(BeanProperty::dbColumn)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
String cols = options.uniqueColumns();
|
||||
if (cols != null) {
|
||||
request.append(cols);
|
||||
} else {
|
||||
appendUniqueColumns(uniqueColumns, request);
|
||||
}
|
||||
request.append(")");
|
||||
|
||||
if (doNothing) {
|
||||
request.append(" do nothing");
|
||||
return request.toString();
|
||||
}
|
||||
request.append(" do update set ");
|
||||
String updateSet = options.updateSet();
|
||||
if (updateSet != null) {
|
||||
request.append(updateSet);
|
||||
} else {
|
||||
setColumns(withId, request, uniqueColumns);
|
||||
}
|
||||
return request.toString();
|
||||
}
|
||||
|
||||
private void setColumns(boolean withId, GenerateDmlRequest request, List<String> uniqueColumns) {
|
||||
List<String> columns = request.columns();
|
||||
columns.removeAll(uniqueColumns);
|
||||
if (withId) {
|
||||
BeanProperty idProperty = desc.idProperty();
|
||||
if (idProperty != null && !idProperty.isEmbedded()) {
|
||||
columns.remove(idProperty.dbColumn());
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < columns.size(); i++) {
|
||||
if (i > 0) {
|
||||
request.append(", ");
|
||||
}
|
||||
String col = columns.get(i);
|
||||
request.append(col).append("=excluded.").append(col);
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendUniqueColumns(List<String> uniqueColumns, GenerateDmlRequest request) {
|
||||
if (uniqueColumns.isEmpty()) {
|
||||
throw new IllegalStateException("Unable to identify unique columns for INSERT ON CONFLICT - Add mapping like @Column(unique=true) or @Index(unique=true)");
|
||||
}
|
||||
for (int i = 0; i < uniqueColumns.size(); i++) {
|
||||
if (i > 0) {
|
||||
request.append(", ");
|
||||
}
|
||||
request.append(uniqueColumns.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ final class InsertMetaPlatform {
|
||||
case YUGABYTE:
|
||||
case COCKROACH:
|
||||
return new InsertMetaOptionsPostgres(meta, desc);
|
||||
case SQLITE:
|
||||
return new InsertMetaOptionsSqlite(meta, desc);
|
||||
default:
|
||||
return NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
+14
-2
@@ -60,6 +60,10 @@ final class BindableIdEmbedded implements BindableId {
|
||||
@Override
|
||||
public void dmlBind(BindableRequest request, EntityBean bean) throws SQLException {
|
||||
EntityBean idValue = (EntityBean) embId.getValue(bean);
|
||||
if (idValue == null && matches != null) {
|
||||
// The id (e.g. IdClass) hasn't been derived/cached on this bean yet
|
||||
idValue = deriveId(bean);
|
||||
}
|
||||
for (BeanProperty prop : props) {
|
||||
Object value = prop.getValue(idValue);
|
||||
request.bind(value, prop);
|
||||
@@ -83,13 +87,21 @@ final class BindableIdEmbedded implements BindableId {
|
||||
|
||||
@Override
|
||||
public boolean deriveConcatenatedId(PersistRequestBean<?> persist) {
|
||||
deriveId(persist.entityBean());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive/build the concatenated id (e.g. IdClass) from the entity's own matching id
|
||||
* properties, caching it on the bean (via setValueIntercept) for subsequent use.
|
||||
*/
|
||||
private EntityBean deriveId(EntityBean bean) {
|
||||
if (matches == null) {
|
||||
String m = "No matches for " + embId.fullName() + " the concatenated key columns where not found?"
|
||||
+ " I expect that the concatenated key was null, and this bean does"
|
||||
+ " not have ManyToOne assoc beans matching the primary key columns?";
|
||||
throw new PersistenceException(m);
|
||||
}
|
||||
EntityBean bean = persist.entityBean();
|
||||
// create the new id
|
||||
EntityBean newId = (EntityBean) embId.createEmbeddedId();
|
||||
// populate it from the assoc one id values...
|
||||
@@ -97,7 +109,7 @@ final class BindableIdEmbedded implements BindableId {
|
||||
match.populate(bean, newId);
|
||||
}
|
||||
embId.setValueIntercept(bean, newId);
|
||||
return true;
|
||||
return newId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package io.ebeaninternal.server.query;
|
||||
|
||||
import io.ebeaninternal.api.SpiDbQueryPlan;
|
||||
import io.ebeaninternal.api.SpiQueryBindCapture;
|
||||
import io.ebeaninternal.api.SpiQueryPlan;
|
||||
import io.ebean.config.CurrentTenantProvider;
|
||||
import io.ebeaninternal.api.*;
|
||||
import io.ebeaninternal.server.bind.capture.BindCapture;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static java.lang.System.Logger.Level.ERROR;
|
||||
|
||||
final class CQueryBindCapture implements SpiQueryBindCapture {
|
||||
|
||||
private static final double multiplier = 1.5d;
|
||||
@@ -16,23 +19,22 @@ final class CQueryBindCapture implements SpiQueryBindCapture {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final CQueryPlanManager manager;
|
||||
private final SpiQueryPlan queryPlan;
|
||||
private final CurrentTenantProvider tenantProvider;
|
||||
|
||||
private BindCapture bindCapture;
|
||||
private long queryTimeMicros;
|
||||
private long thresholdMicros;
|
||||
private long captureCount;
|
||||
|
||||
private Object tenantId;
|
||||
private long lastBindCapture;
|
||||
|
||||
CQueryBindCapture(CQueryPlanManager manager, SpiQueryPlan queryPlan, long thresholdMicros) {
|
||||
CQueryBindCapture(CQueryPlanManager manager, SpiQueryPlan queryPlan, long thresholdMicros, CurrentTenantProvider tenantProvider) {
|
||||
this.manager = manager;
|
||||
this.queryPlan = queryPlan;
|
||||
this.thresholdMicros = thresholdMicros;
|
||||
this.tenantProvider = tenantProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if we should capture the bind values for this query.
|
||||
*/
|
||||
@Override
|
||||
public boolean collectFor(long timeMicros) {
|
||||
return timeMicros > thresholdMicros && captureCount < 10;
|
||||
@@ -45,6 +47,7 @@ final class CQueryBindCapture implements SpiQueryBindCapture {
|
||||
this.thresholdMicros = Math.round(queryTimeMicros * multiplier);
|
||||
this.captureCount++;
|
||||
this.bindCapture = bindCapture;
|
||||
this.tenantId = tenantProvider == null ? null : tenantProvider.currentId();
|
||||
this.queryTimeMicros = queryTimeMicros;
|
||||
lastBindCapture = System.currentTimeMillis();
|
||||
manager.notifyBindCapture(this, startNanos);
|
||||
@@ -68,7 +71,7 @@ final class CQueryBindCapture implements SpiQueryBindCapture {
|
||||
/**
|
||||
* Collect the query plan using already captured bind values.
|
||||
*/
|
||||
public boolean collectQueryPlan(CQueryPlanRequest request) {
|
||||
boolean collectQueryPlan(CQueryPlanRequest request, SpiTransactionManager transactionManager) {
|
||||
if (bindCapture == null || request.since() < lastBindCapture) {
|
||||
// no bind capture since the last capture
|
||||
return false;
|
||||
@@ -76,11 +79,25 @@ final class CQueryBindCapture implements SpiQueryBindCapture {
|
||||
|
||||
final Instant whenCaptured = Instant.ofEpochMilli(this.lastBindCapture);
|
||||
final BindCapture last = this.bindCapture;
|
||||
final Object tenantId = this.tenantId;
|
||||
final long startNanos = System.nanoTime();
|
||||
SpiDbQueryPlan queryPlan = manager.collectPlan(request.connection(), this.queryPlan, last);
|
||||
|
||||
SpiDbQueryPlan queryPlan;
|
||||
try (Connection connection = transactionManager.queryPlanConnection(tenantId)) {
|
||||
try {
|
||||
queryPlan = manager.collectPlan(connection, this.queryPlan, last);
|
||||
} finally {
|
||||
if (!connection.getAutoCommit()) {
|
||||
connection.rollback();
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
CoreLog.log.log(ERROR, "Error during query plan collection", e);
|
||||
return false;
|
||||
}
|
||||
if (queryPlan != null) {
|
||||
final long captureMicros = TimeUnit.MICROSECONDS.convert(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
|
||||
request.add(queryPlan.with(queryTimeMicros, captureCount, captureMicros, whenCaptured));
|
||||
request.add(queryPlan.with(queryTimeMicros, captureCount, captureMicros, whenCaptured, tenantId));
|
||||
// effectively turn off bind capture for this plan
|
||||
thresholdMicros = Long.MAX_VALUE;
|
||||
return true;
|
||||
|
||||
@@ -336,6 +336,46 @@ final class CQueryBuilder {
|
||||
return sql;
|
||||
}
|
||||
|
||||
static String wrapSelectExists(String sql, boolean existsWithCaseWhen, String existsFromClause) {
|
||||
if (existsWithCaseWhen) {
|
||||
return "select case when exists(" + sql + ") then 1 else 0 end" + existsFromClause;
|
||||
}
|
||||
return "select exists(" + sql + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the exists query using select exists(...) for efficient boolean result.
|
||||
*/
|
||||
<T> CQueryExists buildExistsQuery(OrmQueryRequest<T> request) {
|
||||
SpiQuery<T> query = request.query();
|
||||
query.setOrderBy(null);
|
||||
query.setFirstRow(0);
|
||||
query.setMaxRows(0);
|
||||
if (request.descriptor().hasId()) {
|
||||
query.setSelectId();
|
||||
}
|
||||
|
||||
CQueryPredicates predicates = new CQueryPredicates(binder, request);
|
||||
CQueryPlan queryPlan = request.queryPlan();
|
||||
if (queryPlan != null) {
|
||||
predicates.prepare(false);
|
||||
return new CQueryExists(queryPlan, request, predicates);
|
||||
}
|
||||
|
||||
predicates.prepare(true);
|
||||
SqlTree sqlTree = createSqlTree(request, predicates, false);
|
||||
if (SpiQuery.TemporalMode.CURRENT == query.temporalMode()) {
|
||||
sqlTree.addSoftDeletePredicate(query);
|
||||
}
|
||||
|
||||
SqlLimitResponse s = buildSql("select 1", request, predicates, sqlTree);
|
||||
String sql = wrapSelectExists(s.getSql(), dbPlatform.existsWithCaseWhen(), dbPlatform.existsFromClause());
|
||||
|
||||
queryPlan = new CQueryPlan(request, sql, sqlTree.plan(), predicates.logWhereSql());
|
||||
request.putQueryPlan(queryPlan);
|
||||
return new CQueryExists(queryPlan, request, predicates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SQL Select statement as a String. Converts logical property
|
||||
* names to physical deployment column names.
|
||||
|
||||
@@ -48,9 +48,12 @@ final class CQueryBuilderRawSql {
|
||||
// wrap with a limit offset or ROW_NUMBER() etc
|
||||
return sqlLimiter.limit(new OrmQueryLimitRequest(sql, orderBy, query, dbPlatform, rsql.isDistinct() || query.isDistinct()));
|
||||
} else {
|
||||
// add back select keyword (it was removed to support sqlQueryLimiter)
|
||||
String prefix = "select " + (rsql.isDistinct() ? "distinct " : "");
|
||||
sql = prefix + sql;
|
||||
if (hasValue(rsql.getPreFrom())) {
|
||||
// add back select keyword (it was removed to support sqlQueryLimiter)
|
||||
String prefix = "select " + (rsql.isDistinct() ? "distinct " : "");
|
||||
sql = prefix + sql;
|
||||
}
|
||||
// else: template mode — SQL is already complete (no keyword stripping was done)
|
||||
return new SqlLimitResponse(sql);
|
||||
}
|
||||
}
|
||||
@@ -67,10 +70,12 @@ final class CQueryBuilderRawSql {
|
||||
sb.append(selectProperty);
|
||||
first = false;
|
||||
}
|
||||
} else {
|
||||
sb.append(sql.getPreFrom());
|
||||
sb.append(' ');
|
||||
} else if (hasValue(sql.getPreFrom())) {
|
||||
// standard parsed mode: column list with "select" prefix added in buildSql()
|
||||
sb.append(sql.getPreFrom()).append(' ');
|
||||
}
|
||||
sb.append(' ');
|
||||
// else: template mode (preFrom empty) — the full SQL is in preWhere/preHaving, no prefix needed
|
||||
|
||||
String s = sql.getPreWhere();
|
||||
BindParams bindParams = request.query().bindParams();
|
||||
@@ -126,9 +131,18 @@ final class CQueryBuilderRawSql {
|
||||
}
|
||||
sb.append(dbHaving).append(' ');
|
||||
}
|
||||
|
||||
String preOrderBy = sql.getPreOrderBy();
|
||||
if (hasValue(preOrderBy)) {
|
||||
sb.append(preOrderBy).append(' ');
|
||||
}
|
||||
if (hasValue(orderBy)) {
|
||||
sb.append(' ').append(sql.getOrderByPrefix()).append(' ').append(orderBy);
|
||||
}
|
||||
String postOrderBy = sql.getPostOrderBy();
|
||||
if (hasValue(postOrderBy)) {
|
||||
sb.append(' ').append(postOrderBy);
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
@@ -137,6 +151,12 @@ final class CQueryBuilderRawSql {
|
||||
}
|
||||
|
||||
private String orderBy(CQueryPredicates predicates, SpiRawSql.Sql sql) {
|
||||
if (!hasValue(sql.getPreFrom()) && !sql.isOrderByPlaceholder()) {
|
||||
// template mode (withPlaceholders()) without an explicit ${orderBy}/${andOrderBy} placeholder -
|
||||
// there is no defined injection point for a dynamic order by, so ignore any caller-supplied
|
||||
// order by rather than risk emitting it at an undefined (and likely invalid) position.
|
||||
return sql.getOrderBy();
|
||||
}
|
||||
String orderBy = predicates.dbOrderBy();
|
||||
if (orderBy != null) {
|
||||
return orderBy;
|
||||
|
||||
@@ -170,6 +170,29 @@ public final class CQueryEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and execute the exists query using select exists(...).
|
||||
*/
|
||||
public <T> boolean findExists(OrmQueryRequest<T> request) {
|
||||
CQueryExists rcQuery = queryBuilder.buildExistsQuery(request);
|
||||
request.setCancelableQuery(rcQuery);
|
||||
try {
|
||||
boolean exists = rcQuery.findExists();
|
||||
if (request.logSql()) {
|
||||
logGeneratedSql(request, rcQuery.generatedSql(), rcQuery.bindLog(), rcQuery.micros());
|
||||
}
|
||||
if (request.logSummary()) {
|
||||
request.transaction().logSummary(rcQuery.summary());
|
||||
}
|
||||
if (request.isQueryCachePut()) {
|
||||
request.putToQueryCache(exists);
|
||||
}
|
||||
return exists;
|
||||
} catch (SQLException e) {
|
||||
throw translate(request, rcQuery.bindLog(), rcQuery.generatedSql(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read many beans using an iterator (except you need to close() the iterator
|
||||
* when you have finished).
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package io.ebeaninternal.server.query;
|
||||
|
||||
import io.ebean.CancelableQuery;
|
||||
import io.ebean.util.JdbcClose;
|
||||
import io.ebeaninternal.api.SpiProfileTransactionEvent;
|
||||
import io.ebeaninternal.api.SpiQuery;
|
||||
import io.ebeaninternal.api.SpiTransaction;
|
||||
import io.ebeaninternal.server.core.OrmQueryRequest;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Executes a select exists(...) query returning a boolean result.
|
||||
*/
|
||||
final class CQueryExists implements SpiProfileTransactionEvent, CancelableQuery {
|
||||
|
||||
private final CQueryPlan queryPlan;
|
||||
private final OrmQueryRequest<?> request;
|
||||
private final BeanDescriptor<?> desc;
|
||||
private final SpiQuery<?> query;
|
||||
private final CQueryPredicates predicates;
|
||||
private final String sql;
|
||||
private ResultSet rset;
|
||||
private PreparedStatement pstmt;
|
||||
private String bindLog;
|
||||
private long executionTimeMicros;
|
||||
private boolean result;
|
||||
private long profileOffset;
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
CQueryExists(CQueryPlan queryPlan, OrmQueryRequest<?> request, CQueryPredicates predicates) {
|
||||
this.queryPlan = queryPlan;
|
||||
this.request = request;
|
||||
this.query = request.query();
|
||||
this.sql = queryPlan.sql();
|
||||
this.desc = request.descriptor();
|
||||
this.predicates = predicates;
|
||||
query.setGeneratedSql(sql);
|
||||
}
|
||||
|
||||
public String summary() {
|
||||
return "FindExists exeMicros[" + executionTimeMicros
|
||||
+ "] result[" + result
|
||||
+ "] type[" + desc.fullName()
|
||||
+ "] predicates[" + predicates.logWhereSql()
|
||||
+ "] bind[" + bindLog + ']';
|
||||
}
|
||||
|
||||
public String bindLog() {
|
||||
return bindLog;
|
||||
}
|
||||
|
||||
public String generatedSql() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
long micros() {
|
||||
return executionTimeMicros;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query returning the exists boolean result.
|
||||
*/
|
||||
public boolean findExists() throws SQLException {
|
||||
long startNano = System.nanoTime();
|
||||
try {
|
||||
SpiTransaction t = transaction();
|
||||
profileOffset = t.profileOffset();
|
||||
Connection conn = t.internalConnection();
|
||||
lock.lock();
|
||||
try {
|
||||
query.checkCancelled();
|
||||
pstmt = conn.prepareStatement(sql);
|
||||
if (query.timeout() > 0) {
|
||||
pstmt.setQueryTimeout(query.timeout());
|
||||
}
|
||||
bindLog = predicates.bind(pstmt, conn);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
rset = pstmt.executeQuery();
|
||||
query.checkCancelled();
|
||||
if (!rset.next()) {
|
||||
throw new jakarta.persistence.PersistenceException("Expecting 1 row from exists query but got none?");
|
||||
}
|
||||
result = rset.getBoolean(1);
|
||||
executionTimeMicros = (System.nanoTime() - startNano) / 1000L;
|
||||
request.slowQueryCheck(executionTimeMicros, result ? 1 : 0);
|
||||
if (queryPlan.executionTime(executionTimeMicros)) {
|
||||
queryPlan.captureBindForQueryPlan(predicates, executionTimeMicros);
|
||||
}
|
||||
t.profileEvent(this);
|
||||
return result;
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
private SpiTransaction transaction() {
|
||||
return request.transaction();
|
||||
}
|
||||
|
||||
private void close() {
|
||||
JdbcClose.close(rset);
|
||||
JdbcClose.close(pstmt);
|
||||
rset = null;
|
||||
pstmt = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void profile() {
|
||||
transaction()
|
||||
.profileStream()
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), result ? 1 : 0, query.profileId(), queryPlan.hash(), query.getGeneratedSql());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
lock.lock();
|
||||
try {
|
||||
JdbcClose.cancel(pstmt);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,17 +10,22 @@ import io.ebean.metric.TimedMetric;
|
||||
import io.ebeaninternal.api.*;
|
||||
import io.ebeaninternal.server.core.OrmQueryRequest;
|
||||
import io.ebeaninternal.server.core.timezone.DataTimeZone;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.query.CQueryPlanStats.Snapshot;
|
||||
import io.ebeaninternal.server.bind.DataBind;
|
||||
import io.ebeaninternal.server.bind.DataBindCapture;
|
||||
import io.ebeaninternal.server.querydefn.OrmQueryProperties;
|
||||
import io.ebeaninternal.server.type.RsetDataReader;
|
||||
import io.ebeaninternal.server.util.Md5;
|
||||
import io.ebeaninternal.server.util.Str;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.lang.System.Logger.Level.ERROR;
|
||||
@@ -96,7 +101,7 @@ public class CQueryPlan implements SpiQueryPlan {
|
||||
this.logWhereSql = logWhereSql;
|
||||
this.encryptedProps = sqlTree.encryptedProps();
|
||||
this.stats = new CQueryPlanStats(this);
|
||||
this.dependentTables = sqlTree.dependentTables();
|
||||
this.dependentTables = buildDependentTables(request.descriptor(), request.secondaryQueries(), sqlTree.dependentTables());
|
||||
this.bindCapture = initBindCapture(query);
|
||||
this.hash = Md5.hash(sql, name, location);
|
||||
}
|
||||
@@ -121,7 +126,7 @@ public class CQueryPlan implements SpiQueryPlan {
|
||||
this.logWhereSql = logWhereSql;
|
||||
this.encryptedProps = sqlTree.encryptedProps();
|
||||
this.stats = new CQueryPlanStats(this);
|
||||
this.dependentTables = sqlTree.dependentTables();
|
||||
this.dependentTables = buildDependentTables(request.descriptor(), request.secondaryQueries(), sqlTree.dependentTables());
|
||||
this.bindCapture = initBindCaptureRaw(sql, query);
|
||||
this.hash = Md5.hash(sql, name, location);
|
||||
}
|
||||
@@ -130,6 +135,34 @@ public class CQueryPlan implements SpiQueryPlan {
|
||||
return deriveName(label, query.loadMode() != null, query.label() != null, query.type().label(), simpleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the SQL-tree dependent tables with any additional tables from fetchQuery paths.
|
||||
* fetchQuery paths fire secondary SQL queries whose results are included in the query
|
||||
* cache entry, so the cache must be invalidated when those tables are modified.
|
||||
*/
|
||||
private static Set<String> buildDependentTables(BeanDescriptor<?> desc, SpiQuerySecondary secondary, Set<String> sqlTables) {
|
||||
if (secondary == null) {
|
||||
return sqlTables;
|
||||
}
|
||||
List<OrmQueryProperties> queryJoins = secondary.queryJoins();
|
||||
if (queryJoins == null || queryJoins.isEmpty()) {
|
||||
return sqlTables;
|
||||
}
|
||||
Set<String> merged = null;
|
||||
for (OrmQueryProperties join : queryJoins) {
|
||||
try {
|
||||
String table = desc.descriptor(join.getPath()).baseTable();
|
||||
if (merged == null) {
|
||||
merged = new LinkedHashSet<>(sqlTables);
|
||||
}
|
||||
merged.add(table);
|
||||
} catch (PersistenceException ignore) {
|
||||
// invalid path — ignore
|
||||
}
|
||||
}
|
||||
return merged != null ? merged : sqlTables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the query plan / metric name.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.ebeaninternal.server.query;
|
||||
|
||||
import io.ebean.config.CurrentTenantProvider;
|
||||
import io.ebean.meta.MetaQueryPlan;
|
||||
import io.ebean.meta.QueryPlanRequest;
|
||||
import io.ebean.metric.TimedMetric;
|
||||
@@ -8,11 +9,9 @@ import io.ebeaninternal.server.transaction.TransactionManager;
|
||||
import io.ebeaninternal.server.bind.capture.BindCapture;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static java.lang.System.Logger.Level.ERROR;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class CQueryPlanManager implements QueryPlanManager {
|
||||
@@ -21,13 +20,17 @@ public final class CQueryPlanManager implements QueryPlanManager {
|
||||
|
||||
private final ConcurrentHashMap<CQueryBindCapture, Object> plans = new ConcurrentHashMap<>();
|
||||
private final TransactionManager transactionManager;
|
||||
private final CurrentTenantProvider tenantProvider;
|
||||
private final QueryPlanLogger planLogger;
|
||||
private final TimedMetric timeCollection;
|
||||
private final TimedMetric timeBindCapture;
|
||||
private long defaultThreshold;
|
||||
|
||||
public CQueryPlanManager(TransactionManager transactionManager, long defaultThreshold, QueryPlanLogger planLogger, ExtraMetrics extraMetrics) {
|
||||
public CQueryPlanManager(TransactionManager transactionManager,
|
||||
CurrentTenantProvider tenantProvider,
|
||||
long defaultThreshold, QueryPlanLogger planLogger, ExtraMetrics extraMetrics) {
|
||||
this.transactionManager = transactionManager;
|
||||
this.tenantProvider = tenantProvider;
|
||||
this.defaultThreshold = defaultThreshold;
|
||||
this.planLogger = planLogger;
|
||||
this.timeCollection = extraMetrics.planCollect();
|
||||
@@ -41,7 +44,7 @@ public final class CQueryPlanManager implements QueryPlanManager {
|
||||
|
||||
@Override
|
||||
public SpiQueryBindCapture createBindCapture(SpiQueryPlan queryPlan) {
|
||||
return new CQueryBindCapture(this, queryPlan, defaultThreshold);
|
||||
return new CQueryBindCapture(this, queryPlan, defaultThreshold, tenantProvider);
|
||||
}
|
||||
|
||||
public void notifyBindCapture(CQueryBindCapture planBind, long startNanos) {
|
||||
@@ -58,23 +61,11 @@ public final class CQueryPlanManager implements QueryPlanManager {
|
||||
}
|
||||
|
||||
private List<MetaQueryPlan> collectPlans(QueryPlanRequest request) {
|
||||
try (Connection connection = transactionManager.queryPlanConnection()) {
|
||||
CQueryPlanRequest req = new CQueryPlanRequest(connection, request, plans.keySet().iterator());
|
||||
while (req.hasNext()) {
|
||||
req.nextCapture();
|
||||
}
|
||||
if (!connection.getAutoCommit()) {
|
||||
// CHECKME: commit or rollback here?
|
||||
// arguments for rollback: the collecting should never modify data.
|
||||
// if there are collectors that may copy the plan into tables, it's up to the collector to
|
||||
// commit the transaction.
|
||||
connection.rollback();
|
||||
}
|
||||
return req.plans();
|
||||
} catch (SQLException e) {
|
||||
CoreLog.log.log(ERROR, "Error during query plan collection", e);
|
||||
return emptyList();
|
||||
CQueryPlanRequest req = new CQueryPlanRequest(transactionManager, request, plans.keySet().iterator());
|
||||
while (req.hasNext()) {
|
||||
req.nextCapture();
|
||||
}
|
||||
return req.plans();
|
||||
}
|
||||
|
||||
public SpiDbQueryPlan collectPlan(Connection connection, SpiQueryPlan queryPlan, BindCapture last) {
|
||||
|
||||
@@ -2,8 +2,8 @@ package io.ebeaninternal.server.query;
|
||||
|
||||
import io.ebean.meta.MetaQueryPlan;
|
||||
import io.ebean.meta.QueryPlanRequest;
|
||||
import io.ebeaninternal.api.SpiTransactionManager;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -14,15 +14,14 @@ import java.util.List;
|
||||
final class CQueryPlanRequest {
|
||||
|
||||
private final List<MetaQueryPlan> plans = new ArrayList<>();
|
||||
|
||||
private final Connection connection;
|
||||
private final SpiTransactionManager transactionManager;
|
||||
private final long since;
|
||||
private final int maxCount;
|
||||
private final long maxTime;
|
||||
private final Iterator<CQueryBindCapture> iterator;
|
||||
|
||||
CQueryPlanRequest(Connection connection, QueryPlanRequest req, Iterator<CQueryBindCapture> iterator) {
|
||||
this.connection = connection;
|
||||
CQueryPlanRequest(SpiTransactionManager transactionManager, QueryPlanRequest req, Iterator<CQueryBindCapture> iterator) {
|
||||
this.transactionManager = transactionManager;
|
||||
this.iterator = iterator;
|
||||
this.maxCount = req.maxCount();
|
||||
long reqSince = req.since();
|
||||
@@ -31,13 +30,6 @@ final class CQueryPlanRequest {
|
||||
this.maxTime = maxTimeMillis > 0 ? System.currentTimeMillis() + maxTimeMillis : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the connection used to collect the db query plan.
|
||||
*/
|
||||
Connection connection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the collected query plan.
|
||||
*/
|
||||
@@ -71,7 +63,7 @@ final class CQueryPlanRequest {
|
||||
*/
|
||||
void nextCapture() {
|
||||
final CQueryBindCapture next = iterator.next();
|
||||
if (next.collectQueryPlan(this)) {
|
||||
if (next.collectQueryPlan(this, transactionManager)) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@ import static java.lang.System.Logger.Level.WARNING;
|
||||
*/
|
||||
public final class CQueryPredicates {
|
||||
|
||||
/**
|
||||
* Default lower bound used for findVersions() (no explicit start/end) on sql2011
|
||||
* standards based platforms that require actual bind values for the root table's
|
||||
* 'for system_time between ? and ?' clause.
|
||||
*/
|
||||
private static final Timestamp EPOCH = Timestamp.valueOf("1970-01-01 00:00:00");
|
||||
|
||||
private final Binder binder;
|
||||
private final OrmQueryRequest<?> request;
|
||||
private final SpiQuery<?> query;
|
||||
@@ -60,6 +67,12 @@ public final class CQueryPredicates {
|
||||
private String dbOrderBy;
|
||||
private String dbDistinctOn;
|
||||
private String dbUpdateClause;
|
||||
/**
|
||||
* Set when the many join is a ManyToMany with an intersection table order column - used to
|
||||
* resolve the literal "${path}dbColumn" marker appended to dbOrderBy in parseTableAlias().
|
||||
*/
|
||||
private String intersectionOrderPath;
|
||||
private String intersectionOrderColumn;
|
||||
/**
|
||||
* Includes from where and order by clauses.
|
||||
*/
|
||||
@@ -84,10 +97,16 @@ public final class CQueryPredicates {
|
||||
// bind the update set clause
|
||||
updateProperties.bind(binder, dataBind);
|
||||
}
|
||||
if (query.isVersionsBetween() && binder.isAsOfStandardsBased()) {
|
||||
if (binder.isAsOfStandardsBased() && query.temporalMode() == SpiQuery.TemporalMode.VERSIONS) {
|
||||
// sql2011 based versions between timestamp syntax
|
||||
Timestamp start = query.versionStart();
|
||||
Timestamp end = query.versionEnd();
|
||||
if (start == null) {
|
||||
start = EPOCH;
|
||||
}
|
||||
if (end == null) {
|
||||
end = new Timestamp(System.currentTimeMillis());
|
||||
}
|
||||
dataBind.append("between ").append(start).append(" and ").append(end);
|
||||
binder.bindObject(dataBind, start);
|
||||
binder.bindObject(dataBind, end);
|
||||
@@ -239,6 +258,14 @@ public final class CQueryPredicates {
|
||||
dbHaving = alias.parseWhere(dbHaving);
|
||||
}
|
||||
if (dbOrderBy != null) {
|
||||
if (intersectionOrderColumn != null) {
|
||||
// resolve the ManyToMany intersection table order column BEFORE the generic
|
||||
// alias substitution runs, as it needs the "z_" suffixed intersection alias
|
||||
// rather than the plain target table alias.
|
||||
String marker = "${" + intersectionOrderPath + "}" + intersectionOrderColumn;
|
||||
String targetAlias = alias.tableAlias(intersectionOrderPath);
|
||||
dbOrderBy = dbOrderBy.replace(marker, targetAlias + "z_." + intersectionOrderColumn);
|
||||
}
|
||||
dbOrderBy = alias.parse(dbOrderBy);
|
||||
}
|
||||
if (dbDistinctOn != null) {
|
||||
@@ -268,9 +295,18 @@ public final class CQueryPredicates {
|
||||
}
|
||||
// check for default ordering on the many property...
|
||||
SpiQueryManyJoin manyProp = request.manyJoin();
|
||||
String manyOrderBy = manyProp.fetchOrderBy();
|
||||
if (manyOrderBy != null) {
|
||||
orderBy = orderBy + ", " + parser.parse(CQueryBuilder.prefixOrderByFields(manyProp.path(), manyOrderBy));
|
||||
if (manyProp.hasIntersectionOrderColumn()) {
|
||||
// ManyToMany with @OrderColumn on the intersection table - the column lives on the
|
||||
// intersection table (alias "<targetAlias>z_") rather than a normal property path,
|
||||
// so we append a literal marker that parseTableAlias() resolves directly.
|
||||
intersectionOrderPath = manyProp.path();
|
||||
intersectionOrderColumn = manyProp.intersectionOrderColumn();
|
||||
orderBy = orderBy + ", ${" + intersectionOrderPath + "}" + intersectionOrderColumn;
|
||||
} else {
|
||||
String manyOrderBy = manyProp.fetchOrderBy();
|
||||
if (manyOrderBy != null) {
|
||||
orderBy = orderBy + ", " + parser.parse(CQueryBuilder.prefixOrderByFields(manyProp.path(), manyOrderBy));
|
||||
}
|
||||
}
|
||||
if (request.isFindById()) {
|
||||
// only one master bean so should be fine...
|
||||
|
||||
@@ -23,6 +23,7 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
|
||||
private long captureCount;
|
||||
private long captureMicros;
|
||||
private Instant whenCaptured;
|
||||
private Object tenantId;
|
||||
|
||||
public DQueryPlanOutput(Class<?> beanType, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
|
||||
this.beanType = beanType;
|
||||
@@ -39,17 +40,11 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the associated bean.
|
||||
*/
|
||||
@Override
|
||||
public Class<?> beanType() {
|
||||
return beanType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query label if set.
|
||||
*/
|
||||
@Override
|
||||
public String label() {
|
||||
return label;
|
||||
@@ -60,42 +55,31 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
|
||||
return profileLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sql of query.
|
||||
*/
|
||||
@Override
|
||||
public String sql() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a description of the bind values used.
|
||||
*/
|
||||
@Override
|
||||
public String bind() {
|
||||
return bind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query plan.
|
||||
*/
|
||||
@Override
|
||||
public String plan() {
|
||||
return plan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query execution time associated with the capture of bind values used
|
||||
* to build the query plan.
|
||||
*/
|
||||
@Override
|
||||
public Object tenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long queryTimeMicros() {
|
||||
return queryTimeMicros;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the total count of times bind capture has occurred.
|
||||
*/
|
||||
@Override
|
||||
public long captureCount() {
|
||||
return captureCount;
|
||||
@@ -113,18 +97,24 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return " BeanType:" + ((beanType == null) ? "" : beanType.getSimpleName()) + " planHash:" + hash + " label:" + label + " queryTimeMicros:" + queryTimeMicros + " captureCount:" + captureCount + "\n SQL:" + sql + "\nBIND:" + bind + "\nPLAN:" + plan;
|
||||
return " BeanType:" + ((beanType == null) ? "" : beanType.getSimpleName())
|
||||
+ " planHash:" + hash
|
||||
+ " label:" + label
|
||||
+ " queryTimeMicros:" + queryTimeMicros
|
||||
+ " captureCount:" + captureCount
|
||||
+ (tenantId == null ? "" : (" tenant:" + tenantId))
|
||||
+ "\n SQL:" + sql
|
||||
+ "\nBIND:" + bind
|
||||
+ "\nPLAN:" + plan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additionally set the query execution time and the number of bind captures.
|
||||
*/
|
||||
@Override
|
||||
public DQueryPlanOutput with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured) {
|
||||
public DQueryPlanOutput with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured, Object tenantId) {
|
||||
this.queryTimeMicros = queryTimeMicros;
|
||||
this.captureCount = captureCount;
|
||||
this.captureMicros = captureMicros;
|
||||
this.whenCaptured = whenCaptured;
|
||||
this.tenantId = tenantId;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +350,11 @@ final class DefaultFetchGroupQuery<T> implements SpiFetchGroupQuery<T>, SpiQuery
|
||||
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deletePermanent() {
|
||||
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update() {
|
||||
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
|
||||
|
||||
@@ -88,6 +88,12 @@ public final class DefaultOrmQueryEngine implements OrmQueryEngine {
|
||||
return queryEngine.findCount(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> boolean findExists(OrmQueryRequest<T> request) {
|
||||
flushJdbcBatchOnQuery(request);
|
||||
return queryEngine.findExists(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A> List<A> findIds(OrmQueryRequest<?> request) {
|
||||
flushJdbcBatchOnQuery(request);
|
||||
|
||||
@@ -43,6 +43,16 @@ public interface STreePropertyAssocMany extends STreePropertyAssoc {
|
||||
*/
|
||||
boolean hasJoinTable();
|
||||
|
||||
/**
|
||||
* Return true if this is a ManyToMany with an order column stored on the intersection table.
|
||||
*/
|
||||
boolean hasIntersectionOrderColumn();
|
||||
|
||||
/**
|
||||
* Return the db column name of the ManyToMany intersection table order column (or null).
|
||||
*/
|
||||
String intersectionOrderColumn();
|
||||
|
||||
/**
|
||||
* Return the intersection table join.
|
||||
*/
|
||||
|
||||
@@ -34,41 +34,44 @@ final class SqlTreeAlias {
|
||||
/**
|
||||
* Add joins to support where predicates
|
||||
*/
|
||||
void addManyWhereJoins(Set<String> manyWhereJoins) {
|
||||
void addManyWhereJoins(Set<String> manyWhereJoins, STreeType desc) {
|
||||
if (manyWhereJoins != null) {
|
||||
for (String include : manyWhereJoins) {
|
||||
addPropertyJoin(include, manyWhereJoinProps);
|
||||
addPropertyJoin(include, manyWhereJoinProps, desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addEmbeddedPropertyJoin(String embProp) {
|
||||
private boolean addEmbeddedPropertyJoin(String embProp) {
|
||||
if (embeddedPropertyJoins == null) {
|
||||
embeddedPropertyJoins = new HashSet<>();
|
||||
}
|
||||
embeddedPropertyJoins.add(embProp);
|
||||
return embeddedPropertyJoins.add(embProp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add joins.
|
||||
*/
|
||||
public void addJoin(Set<String> propJoins, STreeType desc) {
|
||||
if (propJoins != null) {
|
||||
for (String propJoin : propJoins) {
|
||||
if (desc.isEmbeddedPath(propJoin)) {
|
||||
addEmbeddedPropertyJoin(propJoin);
|
||||
} else {
|
||||
addPropertyJoin(propJoin, joinProps);
|
||||
}
|
||||
}
|
||||
if (propJoins == null) {
|
||||
return;
|
||||
}
|
||||
for (String propJoin : propJoins) {
|
||||
addPropertyJoin(propJoin, joinProps, desc);
|
||||
}
|
||||
}
|
||||
|
||||
private void addPropertyJoin(String include, TreeSet<String> set) {
|
||||
if (set.add(include)) {
|
||||
private void addPropertyJoin(String include, TreeSet<String> set, STreeType desc) {
|
||||
boolean added = false;
|
||||
if (desc.isEmbeddedPath(include)) {
|
||||
added = addEmbeddedPropertyJoin(include);
|
||||
} else {
|
||||
added = set.add(include);
|
||||
}
|
||||
if (added) {
|
||||
String[] split = SplitName.split(include);
|
||||
if (split[0] != null) {
|
||||
addPropertyJoin(split[0], set);
|
||||
addPropertyJoin(split[0], set, desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +94,7 @@ final class SqlTreeAlias {
|
||||
for (String propJoin : embeddedPropertyJoins) {
|
||||
String[] split = SplitName.split(propJoin);
|
||||
// the table alias of the parent path
|
||||
String alias = tableAlias(split[0]);
|
||||
String alias = tableAliasManyWhere(split[0]);
|
||||
aliasMap.put(propJoin, alias);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ public final class SqlTreeBuilder {
|
||||
alias.addJoin(queryDetail.getFetchPaths(), desc);
|
||||
alias.addJoin(predicates.predicateIncludes(), desc);
|
||||
alias.addJoin(formula2JoinIncludes, desc);
|
||||
alias.addManyWhereJoins(manyWhereJoins.propertyNames());
|
||||
alias.addManyWhereJoins(manyWhereJoins.propertyNames(), desc);
|
||||
// build set of table alias
|
||||
alias.buildAlias();
|
||||
predicates.parseTableAlias(alias);
|
||||
@@ -398,7 +398,7 @@ public final class SqlTreeBuilder {
|
||||
// the 'select' part of the query. We may need to add other joins to
|
||||
// support the predicates or order by clauses.
|
||||
|
||||
// remove ManyWhereJoins from the predicateIncludes
|
||||
// remove ManyWhereJoins from the predicateIncludes (they are handled as manyWhere correlated joins)
|
||||
predicateIncludes.removeAll(manyWhereJoins.propertyNames());
|
||||
predicateIncludes.addAll(predicates.orderByIncludes());
|
||||
|
||||
@@ -778,11 +778,15 @@ public final class SqlTreeBuilder {
|
||||
* Create a SqlTreeNodeExtraJoin, register and return it.
|
||||
*/
|
||||
private SqlTreeNodeExtraJoin createJoinLeaf(String propertyName) {
|
||||
SqlTreeNodeExtraJoin extraJoin = joinRegister.get(propertyName);
|
||||
if (extraJoin != null) {
|
||||
return extraJoin;
|
||||
}
|
||||
ExtraJoin extra = desc.extraJoin(propertyName);
|
||||
if (extra == null) {
|
||||
return null;
|
||||
} else {
|
||||
SqlTreeNodeExtraJoin extraJoin = new SqlTreeNodeExtraJoin(propertyName, extra.property(), extra.isContainsMany(), temporalMode);
|
||||
extraJoin = new SqlTreeNodeExtraJoin(propertyName, extra.property(), extra.isContainsMany(), temporalMode);
|
||||
joinRegister.put(propertyName, extraJoin);
|
||||
return extraJoin;
|
||||
}
|
||||
@@ -803,30 +807,30 @@ public final class SqlTreeBuilder {
|
||||
// no parent possible(parent is root)
|
||||
return childJoin;
|
||||
|
||||
} else {
|
||||
// look in register ...
|
||||
String parentPropertyName = includeProp.substring(0, dotPos);
|
||||
if (selectIncludes.contains(parentPropertyName)) {
|
||||
// parent already handled by select
|
||||
return childJoin;
|
||||
}
|
||||
|
||||
SqlTreeNodeExtraJoin parentJoin = joinRegister.get(parentPropertyName);
|
||||
if (parentJoin == null) {
|
||||
// we need to create this the parent implicitly...
|
||||
parentJoin = createJoinLeaf(parentPropertyName);
|
||||
}
|
||||
|
||||
// formula2 dependency joins must appear before formula2 property joins that reference
|
||||
// their table aliases — use addChildFirst to push dependencies ahead
|
||||
if (isFormula2Dependency(childJoin.prefix())) {
|
||||
parentJoin.addChildFirst(childJoin);
|
||||
} else {
|
||||
parentJoin.addChild(childJoin);
|
||||
}
|
||||
childJoin = parentJoin;
|
||||
includeProp = parentPropertyName;
|
||||
}
|
||||
String parentPropertyName = includeProp.substring(0, dotPos);
|
||||
if (desc.isEmbeddedPath(parentPropertyName)) {
|
||||
// digging in embedded property, skip to parent
|
||||
includeProp = parentPropertyName;
|
||||
continue;
|
||||
}
|
||||
// look in register ...
|
||||
if (selectIncludes.contains(parentPropertyName)) {
|
||||
// parent already handled by select
|
||||
return childJoin;
|
||||
}
|
||||
|
||||
SqlTreeNodeExtraJoin parentJoin = createJoinLeaf(parentPropertyName);
|
||||
|
||||
// formula2 dependency joins must appear before formula2 property joins that reference
|
||||
// their table aliases — use addChildFirst to push dependencies ahead
|
||||
if (isFormula2Dependency(childJoin.prefix())) {
|
||||
parentJoin.addChildFirst(childJoin);
|
||||
} else {
|
||||
parentJoin.addChild(childJoin);
|
||||
}
|
||||
childJoin = parentJoin;
|
||||
includeProp = parentPropertyName;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,9 @@ final class SqlTreeNodeManyWhereJoin implements SqlTreeNode {
|
||||
* intersection table if this is a ManyToMany node.
|
||||
*/
|
||||
private void appendFromBaseTable(DbSqlContext ctx, SqlJoinType joinType) {
|
||||
if (nodeBeanProp.isEmbedded()) {
|
||||
return;
|
||||
}
|
||||
String alias = ctx.tableAliasManyWhere(prefix);
|
||||
String parentAlias = ctx.tableAliasManyWhere(parentPrefix);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user