mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 11:17:36 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63a1e9907d | ||
|
|
67db089646 | ||
|
|
26478924a7 | ||
|
|
3cb22fb0cc | ||
|
|
4dde7064ba | ||
|
|
681221c0ff | ||
|
|
d6ce093ac5 | ||
|
|
515b8e5958 | ||
|
|
86b4af1321 | ||
|
|
eb73318f53 | ||
|
|
96a3ab9d79 | ||
|
|
9954ad1ea7 | ||
|
|
2928247ba7 | ||
|
|
3066bb4f74 | ||
|
|
a96144cdaa | ||
|
|
50e20c3aa0 | ||
|
|
f07ca68eb3 | ||
|
|
0ec7ddb82d | ||
|
|
8074a24d03 | ||
|
|
7e4a2db9aa | ||
|
|
292dcf4b0e | ||
|
|
f44bf59cb6 | ||
|
|
7c3741e258 | ||
|
|
afadaa7208 | ||
|
|
46b7510e6c | ||
|
|
bcb0b049ff | ||
|
|
42bfaaf2bf | ||
|
|
2ca39f1419 | ||
|
|
de2914e550 | ||
|
|
6eda7044a0 | ||
|
|
e4adf25022 | ||
|
|
ad5483100e | ||
|
|
6d9ed670ef | ||
|
|
9e2d821475 | ||
|
|
80ce7a41a2 | ||
|
|
6458bb0f1c | ||
|
|
7ba550cc0d | ||
|
|
fff8dfb8b1 | ||
|
|
d0af7b1788 | ||
|
|
ebb0305cf3 | ||
|
|
d03d36bba4 | ||
|
|
88408edd24 | ||
|
|
0aad35b840 | ||
|
|
e7979b285d | ||
|
|
91920b00dc | ||
|
|
bdfe016d2d | ||
|
|
b2670f9cc0 | ||
|
|
ebc90e0e82 | ||
|
|
c4230c780f | ||
|
|
794f933310 | ||
|
|
c8a7a263a9 | ||
|
|
aeef6d0ea2 | ||
|
|
a99aef8b00 | ||
|
|
6f17cc6327 | ||
|
|
903947b3cb | ||
|
|
797f75f7b7 | ||
|
|
af443ef2f2 | ||
|
|
ad4f027835 | ||
|
|
367eba8685 | ||
|
|
be542635a5 | ||
|
|
6e9d0a91e0 | ||
|
|
a99ef3ebf2 | ||
|
|
19afa845d1 | ||
|
|
239900cd3b | ||
|
|
73ac39971c | ||
|
|
3a876252ec | ||
|
|
231b52ba88 | ||
|
|
808019cf3d | ||
|
|
364520455f | ||
|
|
e92621a489 | ||
|
|
ccd1b7b8ec | ||
|
|
d144273307 | ||
|
|
41c5ebdcd7 | ||
|
|
9e711efecb | ||
|
|
ef7fd76f14 | ||
|
|
b7e3ddbedd | ||
|
|
69c932816a | ||
|
|
a1facf527b | ||
|
|
ec42ebf66d | ||
|
|
5dede70801 | ||
|
|
b2f02ecc1f | ||
|
|
15fa7cd1c2 | ||
|
|
8dced38cd5 | ||
|
|
7addba7c93 | ||
|
|
00b45c1642 | ||
|
|
a3d9711fe6 | ||
|
|
1239c50723 | ||
|
|
d85d8449e2 | ||
|
|
baf71cbab3 | ||
|
|
ff0ca35e54 | ||
|
|
0fc9a1ac2f | ||
|
|
f7b4edb193 | ||
|
|
284c8d4d21 | ||
|
|
59a5c5f9bd | ||
|
|
5b04d6eca3 | ||
|
|
d5547808a2 | ||
|
|
5606c95de5 | ||
|
|
4c2bd408b0 | ||
|
|
db76d5c27c | ||
|
|
f92c2f8c14 | ||
|
|
51c3065419 | ||
|
|
e46dd66292 | ||
|
|
3e1451bee7 | ||
|
|
4ac3c4ddb1 | ||
|
|
ee8e48902b | ||
|
|
c37e752ace | ||
|
|
c9a6f822ac | ||
|
|
2231ef9fd1 | ||
|
|
3ff1905318 | ||
|
|
9b4fecff96 | ||
|
|
6f356fd55e | ||
|
|
591f260f5b | ||
|
|
b361b10571 | ||
|
|
c0e4e548ee | ||
|
|
92f3245919 | ||
|
|
59c2290916 | ||
|
|
1135eb3f99 | ||
|
|
1dccdff7bf | ||
|
|
5f74a53e3e | ||
|
|
1f9e652ea9 | ||
|
|
4e592c97ff | ||
|
|
760d19a10d | ||
|
|
d2693c3ff2 | ||
|
|
3324b2bda4 | ||
|
|
1b5e066562 | ||
|
|
104e4ec756 | ||
|
|
74a2967a6e | ||
|
|
e693b12a97 | ||
|
|
48fb9b34bb | ||
|
|
f64400400a | ||
|
|
d7cc081942 | ||
|
|
3351548b60 | ||
|
|
8d9858f57a | ||
|
|
9b520bc176 | ||
|
|
44fd8e3a56 | ||
|
|
6910016c4d | ||
|
|
5f8a3c0591 | ||
|
|
0ae6956c1c | ||
|
|
2e9c5d8548 | ||
|
|
93edc840b3 | ||
|
|
34311785fa | ||
|
|
63929f8ff3 | ||
|
|
d918c5117f | ||
|
|
dd845a7b0c | ||
|
|
df0e4d57f0 | ||
|
|
9c7fa25d49 | ||
|
|
e82c1a4140 | ||
|
|
389bf48e3e | ||
|
|
3b3b44543e | ||
|
|
297f334da5 | ||
|
|
78c7439445 | ||
|
|
2043ba6411 | ||
|
|
437c58001b | ||
|
|
0e812efa7e | ||
|
|
80d4d21986 | ||
|
|
fe66b8c242 | ||
|
|
206b2b2d3b | ||
|
|
c0bc3faea0 | ||
|
|
3d07e7d804 | ||
|
|
fc124e8318 | ||
|
|
f4fd4f4b09 | ||
|
|
16f5311885 | ||
|
|
75a0675977 | ||
|
|
80ebf4a1ee | ||
|
|
4185beb5aa | ||
|
|
79908c0164 | ||
|
|
b5a16f42ca | ||
|
|
a320a132b3 | ||
|
|
9e84e93511 | ||
|
|
6fb40c71ef | ||
|
|
202b9ae94a | ||
|
|
d79b452378 | ||
|
|
78a408dd10 | ||
|
|
6af974e432 | ||
|
|
04a8475291 | ||
|
|
c56345f92e | ||
|
|
ed0a46955e | ||
|
|
3c4b04a893 | ||
|
|
ee403bdb83 | ||
|
|
b18abe2221 | ||
|
|
d68f372d99 | ||
|
|
c8f769aa2a | ||
|
|
bde626fd45 | ||
|
|
b828b73b2c | ||
|
|
2795a8578f | ||
|
|
bd0a3c2303 | ||
|
|
21e50e9d00 | ||
|
|
88a47bf0a9 | ||
|
|
d9a45245f4 | ||
|
|
7be72df788 | ||
|
|
42685335e3 | ||
|
|
478a2be405 | ||
|
|
ebcac9a503 | ||
|
|
996239c29f | ||
|
|
caf2772fb3 | ||
|
|
1482c4c84f | ||
|
|
792d52b794 | ||
|
|
aa4ea4904f | ||
|
|
bab839fb38 | ||
|
|
dde2567ccd | ||
|
|
7e4d070b75 | ||
|
|
3151e3f82b | ||
|
|
c630aaff0e | ||
|
|
67ec848f15 | ||
|
|
57f0734381 | ||
|
|
cbd5bf23c9 | ||
|
|
5dc664a63f | ||
|
|
d9b53677f2 | ||
|
|
f55c2cf3b6 | ||
|
|
c98def7ab7 | ||
|
|
705e158a70 | ||
|
|
569e782e99 | ||
|
|
265b92fb12 | ||
|
|
74311fad0a | ||
|
|
1eded5682a | ||
|
|
ff21bfe1de | ||
|
|
86db7fadfd | ||
|
|
e48b327967 | ||
|
|
964b3ccf8e | ||
|
|
eb6906ad2b | ||
|
|
86e5ea3627 | ||
|
|
3a5e8290e0 | ||
|
|
929604e33d | ||
|
|
2eed68ad61 | ||
|
|
f705560b8b | ||
|
|
6d791c7b50 | ||
|
|
1afe42f4c6 | ||
|
|
4e7f3c13a0 | ||
|
|
a35db6bdee | ||
|
|
d7366dec0f | ||
|
|
ceee1d7489 | ||
|
|
1b23319fa4 | ||
|
|
6f726973ea | ||
|
|
82284b19d5 | ||
|
|
798d01abd1 | ||
|
|
5136a5a708 | ||
|
|
fa25d7cc8a | ||
|
|
c13e09257c | ||
|
|
ed65433640 | ||
|
|
08890e9d5a | ||
|
|
4273c4af6a | ||
|
|
51424d9eea | ||
|
|
cf2ab33903 | ||
|
|
90d8b4fb04 | ||
|
|
ff4eeff264 | ||
|
|
64ba343e35 | ||
|
|
af3c59c0fd | ||
|
|
ae55d28026 | ||
|
|
ddffc8eee2 | ||
|
|
7f1a3a46cf | ||
|
|
a59f75a468 | ||
|
|
6f1c66ecd6 | ||
|
|
25f6fb6d6f | ||
|
|
852def8536 | ||
|
|
dfb29fd75f | ||
|
|
80c3501197 | ||
|
|
b4ff5748fc | ||
|
|
64f01f0543 | ||
|
|
fee6e0f480 | ||
|
|
50b44c4529 | ||
|
|
f0c2ba0e28 | ||
|
|
7643dc9b22 | ||
|
|
c49a1b3d71 | ||
|
|
11e783938c | ||
|
|
3845861e24 | ||
|
|
d04181865c | ||
|
|
00d7db31d6 | ||
|
|
6880b5673a | ||
|
|
e18b59a9dd | ||
|
|
cc48dced09 | ||
|
|
b13bdc95be | ||
|
|
0a5c3ced21 | ||
|
|
8ff5f7f72a | ||
|
|
71944bb8fa | ||
|
|
24888fc01e | ||
|
|
645d5bb613 | ||
|
|
4a6f1a39e0 | ||
|
|
40637bd68c | ||
|
|
8dd438be61 | ||
|
|
5e1ad29c5d | ||
|
|
9eeb137907 | ||
|
|
578a041c9e | ||
|
|
b0b2a0d320 | ||
|
|
de81dfc067 | ||
|
|
2bd53cb52b | ||
|
|
a6d792de64 | ||
|
|
c73d83ef33 | ||
|
|
9f1b4ba758 | ||
|
|
1f7117da25 | ||
|
|
f0ceab43bf | ||
|
|
1107e4ea4c | ||
|
|
4dd8a5dfea | ||
|
|
195edf0355 | ||
|
|
e9c438adab | ||
|
|
e45cf5fab9 | ||
|
|
1bf9d1dd4f | ||
|
|
657920c4a2 | ||
|
|
6c47107436 | ||
|
|
88cf4e001a | ||
|
|
7f1fcbcf92 | ||
|
|
95d5d5e467 | ||
|
|
2dde23d577 | ||
|
|
1ec26d13f9 | ||
|
|
bb1b55e897 | ||
|
|
fb07447df4 | ||
|
|
ad18b6573c | ||
|
|
11632c3a6e | ||
|
|
b066f9c072 | ||
|
|
1e38ed9efc | ||
|
|
2d48dd2fef | ||
|
|
60aa899a34 | ||
|
|
c38c75fdec | ||
|
|
cf275f4edd | ||
|
|
121f4afed8 | ||
|
|
c8621c3a29 | ||
|
|
f83da15d5e | ||
|
|
52729d9802 | ||
|
|
69c25309da | ||
|
|
723a12fd66 | ||
|
|
5cf74f1ee9 | ||
|
|
2c18b4fe97 | ||
|
|
9c9fb70999 | ||
|
|
18e6374a60 | ||
|
|
3fea5ce93d | ||
|
|
43675468ae | ||
|
|
1c038f4629 | ||
|
|
49b6f3dec9 | ||
|
|
346e30217a | ||
|
|
5dc165debf | ||
|
|
d5cb7c3ff1 | ||
|
|
4d08dce033 | ||
|
|
beef4407e3 | ||
|
|
e99f922de6 | ||
|
|
1fef4f7c11 | ||
|
|
103e8323f2 | ||
|
|
59ed0bb015 | ||
|
|
c0eb896e1f | ||
|
|
3ab3f81fa3 | ||
|
|
ddb4b9d3e5 | ||
|
|
7035b4c74c | ||
|
|
85a575e353 | ||
|
|
eb05be286c | ||
|
|
9f5a77fdaa | ||
|
|
42ee75f212 | ||
|
|
4610dbb26e | ||
|
|
b26382f99c | ||
|
|
2fba89f846 | ||
|
|
e4f13da868 | ||
|
|
32cd081fce | ||
|
|
e6f92eaceb | ||
|
|
f6da332998 | ||
|
|
d8f321d87b | ||
|
|
c868d8a23b | ||
|
|
d77c14d866 | ||
|
|
423d906848 | ||
|
|
100d33cc94 | ||
|
|
20cf6a9c00 | ||
|
|
e2c12268f7 | ||
|
|
937f6296e1 | ||
|
|
24749567d2 | ||
|
|
7045240778 | ||
|
|
6ceea85b94 | ||
|
|
e04ac50800 | ||
|
|
a452fc0e21 | ||
|
|
4438d5db7d | ||
|
|
9ca6fa31e5 | ||
|
|
e0a00bea34 | ||
|
|
b24b549298 | ||
|
|
51d0f4efcc | ||
|
|
e08985d7c5 | ||
|
|
4f984ff2d4 | ||
|
|
b6843c6db0 | ||
|
|
57782c05b5 | ||
|
|
a2ee8b61f8 | ||
|
|
1225a2776f | ||
|
|
a499c8c26a | ||
|
|
8e627c756f | ||
|
|
45cc78bb0a | ||
|
|
38a8e95b36 | ||
|
|
bca381104b | ||
|
|
7e0c6795ff | ||
|
|
8b4804be22 | ||
|
|
a8418a3d75 | ||
|
|
bb0f63e38a | ||
|
|
978ed26b78 | ||
|
|
5cfa7295f8 | ||
|
|
88d8b4fb5e | ||
|
|
93155d3c8f | ||
|
|
6d5f740337 | ||
|
|
bd06b57144 | ||
|
|
77383f4d29 | ||
|
|
29fcbbfe97 | ||
|
|
d9e0a35efb | ||
|
|
0c01ecbac7 | ||
|
|
5095583cc8 | ||
|
|
92f04a2353 | ||
|
|
acbce35182 | ||
|
|
d6165f4b17 | ||
|
|
95656360a9 | ||
|
|
65d7df5488 | ||
|
|
12648cd37d | ||
|
|
edbd13c075 | ||
|
|
0964980a69 | ||
|
|
dc7d349366 | ||
|
|
a2e66bcb89 | ||
|
|
4d79815779 | ||
|
|
cbac00a89f | ||
|
|
33b2c398f6 | ||
|
|
416f50e782 | ||
|
|
2e8a6ceb53 | ||
|
|
f0fd9003ad | ||
|
|
dfeffb7c5e | ||
|
|
4b0fcb1d29 | ||
|
|
a7fe5d2b84 | ||
|
|
30327fe33e | ||
|
|
1e013331d4 | ||
|
|
f8964707d9 | ||
|
|
86c7de2de8 | ||
|
|
53b4d94933 | ||
|
|
b0ec23e53c | ||
|
|
20caae532c | ||
|
|
4369481c1a | ||
|
|
c94426b6b4 | ||
|
|
b2ff32e88c | ||
|
|
3bd8dfa7bf | ||
|
|
0e7abdd137 | ||
|
|
9135f2cd3e | ||
|
|
6785d5703e | ||
|
|
0a95ac7fa0 | ||
|
|
bbd5636689 | ||
|
|
9f0eaef3d8 | ||
|
|
a5d2a574d3 | ||
|
|
6e7adbb8e4 | ||
|
|
8913bead89 | ||
|
|
cdf1cd8487 | ||
|
|
2582c4b653 | ||
|
|
a9e3e2ea43 | ||
|
|
834ce971b3 | ||
|
|
5c27f89677 | ||
|
|
bfbbb7e7a7 | ||
|
|
a8947c7d2f | ||
|
|
d5401efb08 | ||
|
|
925ceaf441 | ||
|
|
c5fea13ca5 | ||
|
|
6fa3759786 | ||
|
|
1c5288b6ec | ||
|
|
b485fb7440 | ||
|
|
b07f909dd3 | ||
|
|
fa92e7a4b7 | ||
|
|
1823ef288a | ||
|
|
7b2128b02f | ||
|
|
3c84a902e8 | ||
|
|
3ce95c34cb | ||
|
|
d817dcb549 | ||
|
|
5f218e1fdb | ||
|
|
59daf5689a | ||
|
|
44efed5cd9 | ||
|
|
be28314e31 | ||
|
|
053d82b54a | ||
|
|
0141e2effa | ||
|
|
a33bb4d4c8 | ||
|
|
d0348210be | ||
|
|
753cf66cf0 | ||
|
|
4a8b4dd5fb | ||
|
|
2faf7008db | ||
|
|
76c25cbe83 | ||
|
|
5c36a725f4 | ||
|
|
b99d4f5a51 | ||
|
|
7d18942357 | ||
|
|
e51d9ba2ab | ||
|
|
1b1dce90ed | ||
|
|
405834fa45 | ||
|
|
7720229af9 | ||
|
|
89a294414e | ||
|
|
7bbfc00b86 | ||
|
|
d6c76de0a2 | ||
|
|
36f3986689 | ||
|
|
f0f4f13ec6 | ||
|
|
08466d42e9 | ||
|
|
1f841982a5 | ||
|
|
b34704a1fa | ||
|
|
0128d64841 | ||
|
|
68b313f778 | ||
|
|
fad32c58d8 | ||
|
|
8f1ebdb89a | ||
|
|
f7703efde2 | ||
|
|
c5ffccb649 | ||
|
|
3e65705bf9 | ||
|
|
7469a38bbc | ||
|
|
7ea4311662 | ||
|
|
f556e04f58 | ||
|
|
d9c75a60a5 | ||
|
|
225bec7b28 | ||
|
|
da44f7e604 | ||
|
|
e0eda4f8be | ||
|
|
e4e780588c | ||
|
|
7c8f235550 | ||
|
|
9a77ec1567 | ||
|
|
ff426319f5 | ||
|
|
7f56aff9dd | ||
|
|
cdbdd98d36 | ||
|
|
7bf030f59c | ||
|
|
16dca28831 | ||
|
|
678b43693d | ||
|
|
7ab93a0cdf | ||
|
|
951f942c73 | ||
|
|
cf2ec4b81d | ||
|
|
7d1745ba53 | ||
|
|
7adbdbf532 | ||
|
|
40d9c88e34 |
@@ -2,6 +2,7 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches: master
|
||||
@@ -20,14 +21,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'zulu'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
@@ -39,5 +40,5 @@ jobs:
|
||||
# - name: Maven single test
|
||||
# run: mvn --batch-mode clean verify -Dtest="io.ebeaninternal.server.core.DefaultServer_getReferenceTest" -DfailIfNoTests=false
|
||||
- name: Build with Maven
|
||||
run: mvn -T 8 clean test
|
||||
run: mvn -T 8 clean test -Pdefault
|
||||
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'zulu'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: oracle-actions/setup-java@v1
|
||||
with:
|
||||
website: jdk.java.net
|
||||
release: ${{ matrix.java_version }}
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
@@ -37,5 +37,5 @@ jobs:
|
||||
- name: Maven version
|
||||
run: mvn --version
|
||||
- name: Build with Maven
|
||||
run: mvn -T 8 test
|
||||
run: mvn test -Pea
|
||||
|
||||
|
||||
@@ -20,19 +20,19 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
path:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: mariadb 10.6
|
||||
- name: mariadb 10.11
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties
|
||||
|
||||
@@ -17,14 +17,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'zulu'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'zulu'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -17,14 +17,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -20,19 +20,19 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
path:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: sqlserver 2017
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver17.properties
|
||||
- name: sqlserver 2022
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver.properties
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Valhalla EA
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '39 2 * * 3'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [valhalla]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: oracle-actions/setup-java@v1
|
||||
with:
|
||||
website: jdk.java.net
|
||||
release: ${{ matrix.java_version }}
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
path:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: Maven version
|
||||
run: mvn --version
|
||||
# - name: Prepare
|
||||
# run: ./jakarta-to-valhalla.sh
|
||||
- name: Build with Maven
|
||||
run: mvn package
|
||||
@@ -4,7 +4,7 @@ name: Yugabyte
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '10 3 * * *'
|
||||
- cron: '10 3 * * 3'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean)
|
||||
[](https://github.com/ebean-orm/ebean/blob/master/LICENSE)
|
||||
[](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml)
|
||||
[](https://www.graalvm.org/)
|
||||
|
||||
##### Build with database platforms
|
||||
[](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml)
|
||||
@@ -56,12 +57,7 @@ Work at the highest level of abstraction and drop down levels as needed.
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.foconis.de/" target="_blank">
|
||||
<img width="222px" src="https://www.foconis.de/templates/yootheme/cache/foconis_logo_322-709da1de.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.payintech.com/" target="_blank">
|
||||
<img width="222px" src="https://ebean.io/images/sponsor_PayinTech-logo-noir.png">
|
||||
<img width="222px" src="https://group.foconis.com/download/ci/logo/png-72dpi/logo-quer/foconis-analytics-quer.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
@@ -85,6 +81,18 @@ or [github discussions](https://github.com/ebean-orm/ebean/discussions)
|
||||
## Documentation
|
||||
Goto [https://ebean.io/docs/](https://ebean.io/docs/)
|
||||
|
||||
## Guides
|
||||
Library reference (capabilities, scope, and AI guidance): [docs/LIBRARY.md](docs/LIBRARY.md)
|
||||
|
||||
Step-by-step guides for common tasks: [docs/guides/](docs/guides/README.md)
|
||||
|
||||
Available guides:
|
||||
- [Maven POM setup](docs/guides/add-ebean-postgres-maven-pom.md)
|
||||
- [Database configuration](docs/guides/add-ebean-postgres-database-config.md)
|
||||
- [Test container setup](docs/guides/add-ebean-postgres-test-container.md)
|
||||
- [DB migration generation](docs/guides/add-ebean-db-migration-generation.md)
|
||||
- [Lombok with Ebean entity beans](docs/guides/lombok-with-ebean-entity-beans.md)
|
||||
|
||||
## Maven central
|
||||
[Maven central - g:io.ebean](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.ebean%22%20)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-net-postgis</name>
|
||||
<description>ebean-net-postgis composite</description>
|
||||
<artifactId>ebean-net-postgis</artifactId>
|
||||
|
||||
<properties>
|
||||
<postgis.jdbc.version>2023.1.0</postgis.jdbc.version>
|
||||
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-datasource</artifactId>
|
||||
<version>${ebean-datasource.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-migration</artifactId>
|
||||
<version>${ebean-migration.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Technically optional but most expected to use query beans -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-net-postgis-types</artifactId>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${postgres.jdbc.version}</version>
|
||||
<exclusions>
|
||||
<!-- exclude unnecessary checker framework -->
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.postgis</groupId>
|
||||
<artifactId>postgis-jdbc</artifactId>
|
||||
<version>${postgis.jdbc.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.ebean.postgis.assembly;
|
||||
|
||||
/**
|
||||
* Nothing interesting here - required placeholder for javadoc.
|
||||
*/
|
||||
public class Assembly {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
module io.ebean.postgis {
|
||||
|
||||
requires transitive io.ebean.api;
|
||||
requires transitive io.ebean.core;
|
||||
requires transitive io.ebean.datasource;
|
||||
requires transitive io.ebean.querybean;
|
||||
requires transitive io.ebean.platform.postgres;
|
||||
// requires transitive io.ebean.postgis.types;
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-pgvector</name>
|
||||
<description>ebean-pgvector composite</description>
|
||||
<artifactId>ebean-pgvector</artifactId>
|
||||
|
||||
<properties>
|
||||
<pgvector.version>0.1.6</pgvector.version>
|
||||
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-datasource</artifactId>
|
||||
<version>${ebean-datasource.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-migration</artifactId>
|
||||
<version>${ebean-migration.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Technically optional but most expected to use query beans -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${postgres.jdbc.version}</version>
|
||||
<exclusions>
|
||||
<!-- exclude unnecessary checker framework -->
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.pgvector</groupId>
|
||||
<artifactId>pgvector</artifactId>
|
||||
<version>${pgvector.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.ebean.pgvector.assembly;
|
||||
|
||||
/**
|
||||
* Nothing interesting here - required placeholder for javadoc.
|
||||
*/
|
||||
public class Assembly {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module io.ebean.pgvector {
|
||||
|
||||
requires transitive io.ebean.api;
|
||||
requires transitive io.ebean.core;
|
||||
requires transitive io.ebean.datasource;
|
||||
requires transitive io.ebean.querybean;
|
||||
requires transitive io.ebean.platform.postgres;
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,7 +41,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -60,13 +60,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+3
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>composites</artifactId>
|
||||
@@ -24,6 +24,8 @@
|
||||
<module>ebean-oracle</module>
|
||||
<module>ebean-postgres</module>
|
||||
<module>ebean-postgis</module>
|
||||
<module>ebean-net-postgis</module>
|
||||
<module>ebean-pgvector</module>
|
||||
<!-- <module>sqlanywhere</module>-->
|
||||
<module>ebean-sqlite</module>
|
||||
<module>ebean-sqlserver</module>
|
||||
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
# Ebean ORM Library Definition
|
||||
|
||||
Ebean is an ORM library for Java and Kotlin focused on relational data access, type-safe query construction, and production-friendly SQL behavior.
|
||||
|
||||
## Identity
|
||||
|
||||
- **Name**: Ebean ORM
|
||||
- **Package**: `io.ebean`
|
||||
- **Primary Maven Group**: `io.ebean`
|
||||
- **Category**: ORM / Data Access
|
||||
- **Repository**: https://github.com/ebean-orm/ebean
|
||||
- **Issues**: https://github.com/ebean-orm/ebean/issues
|
||||
- **Discussions**: https://github.com/ebean-orm/ebean/discussions
|
||||
- **Website**: https://ebean.io/
|
||||
- **Documentation**: https://ebean.io/docs/
|
||||
- **License**: Apache 2.0
|
||||
|
||||
## Version & Requirements
|
||||
|
||||
- **Repository Version (this checkout)**: `16.5.0` (from repository `pom.xml`)
|
||||
- **Minimum Java Version**: 11+
|
||||
- **Languages**: Java, Kotlin
|
||||
- **Build Tooling in this docs set**: Maven-focused examples
|
||||
|
||||
## Core Artifacts
|
||||
|
||||
| Artifact | Purpose |
|
||||
|------|------|
|
||||
| `io.ebean:ebean` | Core ORM runtime and API |
|
||||
| `io.ebean:ebean-postgres` | PostgreSQL platform bundle used in setup guides |
|
||||
| `io.ebean:ebean-test` | Test support, including Docker-backed database testing |
|
||||
| `io.ebean:querybean-generator` | Generates `Q*` type-safe query beans |
|
||||
| `io.ebean:ebean-maven-plugin` | Bytecode enhancement for entities at build time |
|
||||
| `io.ebean:ebean-migration` | Runtime migration runner (often transitive via platform artifact) |
|
||||
|
||||
## Core APIs & Annotations
|
||||
|
||||
### Database and transaction APIs
|
||||
|
||||
| API | Purpose | Example |
|
||||
|------|------|------|
|
||||
| `DB.getDefault()` | Access default `Database` | `Database db = DB.getDefault();` |
|
||||
| `DB.byName("...")` | Access named `Database` | `Database reporting = DB.byName("reporting");` |
|
||||
| `database.find(...)` | Query entities | `Customer c = database.find(Customer.class, id);` |
|
||||
| `database.insert/save/update/delete` | Persist entity changes | `database.save(customer);` |
|
||||
| `database.beginTransaction()` | Manual transaction boundary | `try (Transaction txn = database.beginTransaction()) { ... }` |
|
||||
| `Database.builder()` | Programmatic `Database` setup | `Database.builder().loadFromProperties().build();` |
|
||||
|
||||
### Query APIs
|
||||
|
||||
| API | Purpose | Example |
|
||||
|------|------|------|
|
||||
| `Q*` query beans | Type-safe query construction | `new QCustomer().status.equalTo(ACTIVE).findList();` |
|
||||
| `exists()` | Efficient existence checks | `new QCustomer().email.equalTo(email).exists();` |
|
||||
| `findOne()` | Unique/single-row retrieval | `new QCustomer().id.equalTo(id).findOne();` |
|
||||
| `findList()` | List retrieval | `new QCustomer().findList();` |
|
||||
| `asDto(...).findList()` | DTO projection reads | `new QOrder().asDto(OrderSummary.class).findList();` |
|
||||
|
||||
### Entity mapping and lifecycle annotations
|
||||
|
||||
| Annotation | Purpose |
|
||||
|------|------|
|
||||
| `@Entity` | Marks class as persistent entity |
|
||||
| `@Id` | Primary key mapping |
|
||||
| `@Version` | Optimistic locking |
|
||||
| `@WhenCreated` | Creation timestamp management |
|
||||
| `@WhenModified` | Modification timestamp management |
|
||||
| `@Transactional` | Declarative transaction boundary |
|
||||
|
||||
## Capabilities
|
||||
|
||||
### ✅ Included
|
||||
|
||||
- Relational ORM with automatic dirty checking and lazy loading (via enhancement)
|
||||
- Multiple query abstraction levels (ORM query, DTO query, SQL/JDBC)
|
||||
- Type-safe query beans (`Q*`) with IDE autocomplete
|
||||
- Built-in migration generation and migration running support
|
||||
- Transaction APIs for implicit, declarative, and explicit transaction control
|
||||
- Support for test-time Docker database workflows
|
||||
- Query tuning and caching features for performance-sensitive workloads
|
||||
|
||||
### ❌ Not in scope
|
||||
|
||||
- HTTP routing, REST controllers, or web server runtime
|
||||
- Dependency injection container functionality
|
||||
- JSON serialization framework responsibilities
|
||||
- Front-end/UI rendering concerns
|
||||
|
||||
Ebean is intentionally focused on persistence and data access. Pair it with a web framework and DI library as needed.
|
||||
|
||||
## Use Cases
|
||||
|
||||
### ✅ Strong fit
|
||||
|
||||
- SQL-backed business applications with rich domain models
|
||||
- Services that need both ORM productivity and SQL-level control
|
||||
- Projects requiring type-safe query authoring via generated query beans
|
||||
- Teams that want migration generation integrated with entity model changes
|
||||
- Integration test suites that need real database behavior (not only in-memory mocks)
|
||||
|
||||
### ⚠️ Consider alternatives if
|
||||
|
||||
- You need a full web framework (routing/controllers) rather than a persistence layer
|
||||
- Your project does not use relational databases as a core storage model
|
||||
- You want a single library to cover persistence, DI, and HTTP all at once
|
||||
|
||||
## Quick Start (Maven)
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<ebean.version><!-- use latest stable from Maven Central --></ebean.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgres</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-maven-plugin</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<extensions>true</extensions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
```
|
||||
|
||||
## Minimal Example
|
||||
|
||||
```java
|
||||
import io.ebean.DB;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
@Entity
|
||||
class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
Database database = DB.getDefault(); // or injected
|
||||
|
||||
Customer customer = database.find(Customer.class, 42);
|
||||
customer.setName("Updated");
|
||||
database.save(customer);
|
||||
```
|
||||
|
||||
## Common Tasks & Guides
|
||||
|
||||
| Task | Guide |
|
||||
|------|------|
|
||||
| Add Ebean to an existing Maven project | [add-ebean-postgres-maven-pom.md](guides/add-ebean-postgres-maven-pom.md) |
|
||||
| Configure database and `Database` bean | [add-ebean-postgres-database-config.md](guides/add-ebean-postgres-database-config.md) |
|
||||
| Add PostgreSQL test container support | [add-ebean-postgres-test-container.md](guides/add-ebean-postgres-test-container.md) |
|
||||
| Generate DB migrations | [add-ebean-db-migration-generation.md](guides/add-ebean-db-migration-generation.md) |
|
||||
| Model entity beans correctly | [entity-bean-creation.md](guides/entity-bean-creation.md) |
|
||||
| Use Lombok safely with entities | [lombok-with-ebean-entity-beans.md](guides/lombok-with-ebean-entity-beans.md) |
|
||||
| Write type-safe query bean queries | [writing-ebean-query-beans.md](guides/writing-ebean-query-beans.md) |
|
||||
| Persist changes and manage transactions | [persisting-and-transactions-with-ebean.md](guides/persisting-and-transactions-with-ebean.md) |
|
||||
| Build test entities quickly | [testing-with-testentitybuilder.md](guides/testing-with-testentitybuilder.md) |
|
||||
|
||||
**Guides index**: [guides/README.md](guides/README.md)
|
||||
|
||||
## Related Ecosystem Docs
|
||||
|
||||
- [Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md)
|
||||
- [AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md)
|
||||
- [Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md)
|
||||
|
||||
## AI Agent Instructions
|
||||
|
||||
### For Claude, GPT, and web-based agents
|
||||
|
||||
Use this file as the top-level reference when answering Ebean questions.
|
||||
|
||||
1. Check this file first for scope and capability fit.
|
||||
2. Route implementation tasks to the relevant guide in **Common Tasks & Guides**.
|
||||
3. Treat Ebean as the persistence layer only; avoid implying it provides HTTP/DI features.
|
||||
4. Prefer type-safe query bean examples when showing query code.
|
||||
5. For setup and migration changes, follow the Maven-focused guide steps exactly.
|
||||
|
||||
### For IDE-based agents (Copilot, Cursor, etc.)
|
||||
|
||||
If `docs/LIBRARY.md` is not in context automatically:
|
||||
|
||||
1. Read `README.md` for docs entry points.
|
||||
2. Open `docs/guides/README.md` for task-specific guides.
|
||||
3. Follow linked guide files directly for concrete implementation steps.
|
||||
|
||||
---
|
||||
|
||||
## Notes for Maintainers
|
||||
|
||||
### When to update this file
|
||||
|
||||
- New release that changes requirements or key APIs
|
||||
- New guide added to `docs/guides/`
|
||||
- Capability/scope changes that affect "Included" or "Not in scope"
|
||||
- Significant migration or setup workflow changes
|
||||
|
||||
### Maintenance checklist
|
||||
|
||||
- [ ] Keep requirements and version references accurate
|
||||
- [ ] Keep Common Tasks table aligned with `docs/guides/README.md`
|
||||
- [ ] Keep artifact names/snippets aligned with setup guides
|
||||
- [ ] Keep AI instructions aligned with current docs structure
|
||||
|
||||
### Link from repository README
|
||||
|
||||
In `README.md`, include:
|
||||
|
||||
```markdown
|
||||
## Documentation
|
||||
|
||||
- [Ebean docs](https://ebean.io/docs/)
|
||||
- [Library reference](docs/LIBRARY.md)
|
||||
- [Step-by-step guides](docs/guides/README.md)
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
# AGENTS.md — Ebean ORM
|
||||
|
||||
This project uses [Ebean ORM](https://ebean.io).
|
||||
|
||||
## AI Agent Instructions
|
||||
|
||||
Step-by-step guides for common Ebean tasks (Maven setup, database config, query writing, transactions, test containers, DB migrations, Lombok) are at:
|
||||
|
||||
**https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/**
|
||||
|
||||
Key guides (fetch and follow when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
|
||||
|
||||
Agents: Before performing any Ebean-related task, fetch and follow the relevant guide above.
|
||||
@@ -0,0 +1,182 @@
|
||||
# Guides
|
||||
|
||||
See also: [AGENTS.md](AGENTS.md) — a minimal template for AI agent onboarding and automation in Ebean ORM projects.
|
||||
|
||||
Step-by-step guides written as instructions for AI agents and developers.
|
||||
|
||||
For a high-level capability reference (scope, core APIs, and AI guidance), see
|
||||
[../LIBRARY.md](../LIBRARY.md).
|
||||
|
||||
## Adding Ebean ORM with PostgreSQL to an existing Maven project
|
||||
|
||||
A three-part guide covering everything needed to wire Ebean + PostgreSQL into an
|
||||
existing Maven project. Complete the steps in order.
|
||||
|
||||
| Step | Guide | Description |
|
||||
|------|-------|-------------|
|
||||
| 1 | [Maven POM setup](add-ebean-postgres-maven-pom.md) | Add Ebean dependencies, the enhancement plugin, and the querybean-generator annotation processor to `pom.xml` |
|
||||
| 2 | [Test container setup](add-ebean-postgres-test-container.md) | Start a PostgreSQL (or PostGIS) Docker container for tests using `@TestScope @Factory` with Avaje Inject; verify the test database works with `mvn verify` before adding production configuration |
|
||||
| 3 | [Database configuration](add-ebean-postgres-database-config.md) | Configure the production Ebean `Database` bean using `DataSourceBuilder` and `DatabaseBuilder` with Avaje Inject |
|
||||
|
||||
## Entity beans
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Entity Bean Creation](entity-bean-creation.md) | How to generate clean, idiomatic Ebean entity beans for AI agents; patterns and anti-patterns; field visibility and accessor guidance; minimal boilerplate |
|
||||
| [Lombok with Ebean entity beans](lombok-with-ebean-entity-beans.md) | Which Lombok annotations to use and avoid on entity beans; why `@Data` is incompatible with Ebean; how to use `@Getter` + `@Setter` + `@Accessors(chain = true)` |
|
||||
|
||||
## Querying
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [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 |
|
||||
|
||||
## Persisting & transactions
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Persisting and transactions with Ebean](persisting-and-transactions-with-ebean.md) | Step-by-step guidance for AI agents to choose `insert` / `save` / `update` / `delete`; inspect cascades; select the right transaction boundary; and use batch or bulk update for large write sets |
|
||||
|
||||
## Testing
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Testing with TestEntityBuilder](testing-with-testentitybuilder.md) | Rapidly create test entity instances with auto-populated random values; manage relationships and cascades; customize value generation for domain-specific testing needs |
|
||||
|
||||
## Database migrations
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [DB migration generation](add-ebean-db-migration-generation.md) | Add `GenerateDbMigration.java` to generate schema diff migrations offline; configure the migration runner; understand `.sql` and `.model.xml` output files; workflow for pending drops |
|
||||
|
||||
## Connection Pooling & DataSource Configuration
|
||||
|
||||
The [ebean-datasource](https://github.com/ebean-orm/ebean-datasource) project provides
|
||||
comprehensive guides on connection pool configuration and best practices. These are particularly
|
||||
useful for production deployments, especially in Kubernetes or AWS environments:
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md) | Step-by-step guide for basic, read-only, Kubernetes, and AWS Lambda datasource configurations |
|
||||
| [AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md) | Setting up dual DataSources with Aurora read and write endpoints for Ebean integration |
|
||||
| [Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md) | Understanding connection heartbeat strategies and why `Connection.isValid()` is the recommended approach |
|
||||
|
||||
---
|
||||
|
||||
## Helping AI agents find these guides
|
||||
|
||||
### Agent Skills (recommended)
|
||||
|
||||
The **ebean-orm skill** packages these guides into a structured format that AI agents
|
||||
load on-demand. See the [ebean-orm/skills](https://github.com/ebean-orm/skills) repo
|
||||
for installation instructions. Quick start:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:ebean-orm/skills.git ~/.agents/ebean-skills
|
||||
mkdir -p ~/.agents/skills
|
||||
ln -sf ~/.agents/ebean-skills/ebean-orm ~/.agents/skills/ebean-orm
|
||||
```
|
||||
|
||||
This works with [pi](https://github.com/mariozechner/pi-coding-agent),
|
||||
[Claude Code](https://docs.anthropic.com/en/docs/claude-code), and any harness
|
||||
that supports the [Agent Skills standard](https://agentskills.io/specification).
|
||||
|
||||
### Other approaches
|
||||
|
||||
AI coding agents can only follow these guides if they know they exist. Below are
|
||||
copy-paste snippets for the most common AI tooling configurations. Add whichever
|
||||
applies to your project.
|
||||
|
||||
### llms.txt (fastest bootstrap for any agent)
|
||||
|
||||
`https://ebean.io/llms.txt` provides a concise Ebean capability reference and links
|
||||
to all guides in a format optimised for LLMs. Agents that check `llms.txt` before
|
||||
starting work will find everything they need without requiring project-level
|
||||
configuration. This is worth knowing if you are an AI agent reading this file.
|
||||
|
||||
|
||||
### Project `README.md` (universal — works with all AI tools)
|
||||
|
||||
Add a section near the top of your project README:
|
||||
|
||||
```markdown
|
||||
## AI Agent Instructions
|
||||
|
||||
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
|
||||
Ebean tasks (Maven setup, database configuration, query writing, transactions,
|
||||
test containers, DB migrations, Lombok) are at:
|
||||
|
||||
**https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/**
|
||||
|
||||
AI agents should fetch and follow the relevant guide before performing Ebean-related
|
||||
tasks.
|
||||
```
|
||||
|
||||
### Why README is important
|
||||
|
||||
Many AI agents (Copilot, Claude, Cursor, Aider) read the project `README.md`
|
||||
automatically as part of understanding the codebase. A visible link in README is the
|
||||
most universally effective way to ensure any agent working on your project will
|
||||
discover the guides without requiring tool-specific configuration files.
|
||||
|
||||
------
|
||||
|
||||
### GitHub Copilot — `.github/copilot-instructions.md`
|
||||
|
||||
```markdown
|
||||
## Ebean ORM
|
||||
|
||||
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
|
||||
tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
|
||||
|
||||
Key guides (fetch and follow these when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Immutable bean cache for read-only references: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/immutable-bean-cache.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
|
||||
```
|
||||
|
||||
### Claude Code — `CLAUDE.md`
|
||||
|
||||
Same content as above — Claude Code reads `CLAUDE.md` at the project root.
|
||||
|
||||
### AGENTS.md — OpenAI Codex / GitHub Copilot coding agent
|
||||
|
||||
Place an `AGENTS.md` at your repo root:
|
||||
|
||||
```markdown
|
||||
## Ebean ORM
|
||||
|
||||
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
|
||||
tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
|
||||
|
||||
Key guides (fetch and follow these when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
- Entity bean creation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/entity-bean-creation.md
|
||||
```
|
||||
|
||||
### Cursor — `.cursor/rules/ebean.mdc`
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Ebean ORM task guidance
|
||||
globs: ["**/*.java", "**/pom.xml"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
## Ebean ORM
|
||||
|
||||
This project uses Ebean ORM. Before performing any Ebean-related task, fetch and
|
||||
follow the relevant step-by-step guide from:
|
||||
https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
|
||||
```
|
||||
@@ -0,0 +1,366 @@
|
||||
# Guide: Add Ebean Database Migration Generation to an Existing Maven Project
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides step-by-step instructions for adding Ebean DB migration generation
|
||||
to an existing Maven project that already uses Ebean ORM. Ebean generates migrations by
|
||||
performing a diff of the current entity model against the previously recorded model state,
|
||||
producing platform-specific DDL SQL scripts.
|
||||
|
||||
These instructions are designed for AI agents and developers to follow precisely.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An existing Maven project with Ebean ORM configured (entity beans present)
|
||||
- `ebean-test` is already a test-scoped dependency (from POM setup guide)
|
||||
- The project targets PostgreSQL (adjust `Platform.POSTGRES` for other databases)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Verify migration dependencies
|
||||
|
||||
### Generation tooling (`ebean-ddl-generator`)
|
||||
|
||||
`ebean-test` (already present as a test dependency) transitively includes
|
||||
`ebean-ddl-generator`, which provides the `DbMigration` class. No additional dependency
|
||||
is required for generation.
|
||||
|
||||
### Runtime migration runner (`ebean-migration`)
|
||||
|
||||
`ebean-migration` is the library that runs migrations on application startup.
|
||||
It is typically included **transitively** via `io.ebean:ebean-postgres` (or the
|
||||
equivalent platform dependency). Verify it is on the classpath by running:
|
||||
|
||||
```bash
|
||||
mvn dependency:tree | grep ebean-migration
|
||||
```
|
||||
|
||||
If it is **not** present transitively, add it explicitly as a compile-scope dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-migration</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Create `GenerateDbMigration.java`
|
||||
|
||||
Create the following class in `src/test/java/main/`. This `main` method is run manually
|
||||
by a developer (or AI agent) whenever entity beans change and a new migration is needed.
|
||||
|
||||
```java
|
||||
package main;
|
||||
|
||||
import io.ebean.annotation.Platform;
|
||||
import io.ebean.dbmigration.DbMigration;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Generate the next database migration based on a diff of the entity model.
|
||||
* Run this main method after making entity bean changes to produce the migration SQL.
|
||||
*/
|
||||
public class GenerateDbMigration {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
|
||||
DbMigration migration = DbMigration.create();
|
||||
migration.setPlatform(Platform.POSTGRES);
|
||||
|
||||
migration.setVersion("1.1"); // set to the next migration version
|
||||
migration.setName("add-customer"); // short description of the change
|
||||
|
||||
migration.generateMigration();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Version naming convention
|
||||
|
||||
Ebean supports two common version formats — choose one and apply it consistently:
|
||||
|
||||
| Format | Example | Notes |
|
||||
|--------|---------|-------|
|
||||
| **Date-based** | `20240820` | `YYYYMMDD`; used when changes are tied to dates; easily sortable |
|
||||
| **Semantic** | `1.1`, `1.2`, `2.0` | Traditional versioning; useful for release-based workflows |
|
||||
|
||||
The version controls execution order — Ebean runs migrations in ascending version order.
|
||||
|
||||
### Name convention
|
||||
|
||||
The `name` should be a short, lowercase, hyphenated description of the change:
|
||||
- `add-customer-email`
|
||||
- `rename-machine-type`
|
||||
- `drop-unused-columns`
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Configure the output path (if needed)
|
||||
|
||||
By default, migration files are written to `src/main/resources/dbmigration/` relative
|
||||
to the **current working directory** when `generateMigration()` is called. This is
|
||||
usually the module root, which is correct for single-module projects.
|
||||
|
||||
For **multi-module projects** where `GenerateDbMigration` is in a submodule but the
|
||||
resources directory is at a different relative path, specify it explicitly:
|
||||
|
||||
```java
|
||||
// Relative path from the working directory (project root) to the module's resources
|
||||
migration.setPathToResources("my-module/src/main/resources");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Run `GenerateDbMigration` to produce the first migration
|
||||
|
||||
Run the `main` method via the IDE or Maven:
|
||||
|
||||
```bash
|
||||
# Run via Maven exec plugin (or use IDE run configuration)
|
||||
mvn test-compile exec:java \
|
||||
-Dexec.mainClass="main.GenerateDbMigration" \
|
||||
-Dexec.classpathScope="test" \
|
||||
-pl <your-module>
|
||||
```
|
||||
|
||||
Ebean migration generation runs in **offline mode** — no database connection is required.
|
||||
|
||||
### Expected output files
|
||||
|
||||
After running, two files are created per migration in `src/main/resources/dbmigration/`:
|
||||
|
||||
```
|
||||
src/main/resources/dbmigration/
|
||||
1.1__add-customer.sql ← DDL SQL to apply (commit this)
|
||||
model/
|
||||
1.1__add-customer.model.xml ← logical model diff XML (commit this)
|
||||
```
|
||||
|
||||
Both files must be committed to source control. The `.model.xml` file records the
|
||||
logical state of the diff and is used by subsequent migration generations to determine
|
||||
what has changed.
|
||||
|
||||
If **no entity beans have changed** since the last migration, the command outputs:
|
||||
```
|
||||
DbMigration - no changes detected - no migration written
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Enable the migration runner
|
||||
|
||||
Configure Ebean to run pending migrations automatically on application startup.
|
||||
|
||||
### Preferred approach — programmatic via `DatabaseBuilder`
|
||||
|
||||
Set `runMigration(true)` directly on the `DatabaseBuilder` when constructing
|
||||
the `Database` bean. This is the preferred approach as it is explicit, co-located with
|
||||
the database configuration, and does not rely on external property files.
|
||||
|
||||
In the `@Factory` class that builds the `Database` bean (see the database configuration
|
||||
guide), add `.runMigration(true)` to the builder chain:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
Database database(ConfigWrapper config) {
|
||||
var dataSource = DataSourceBuilder.create()
|
||||
.url(config.getDatabaseUrl())
|
||||
.username(config.getDatabaseUser())
|
||||
.password(config.getDatabasePassword())
|
||||
// ... other datasource settings ...
|
||||
;
|
||||
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(dataSource)
|
||||
.runMigration(true) // run pending migrations on startup
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
If migrations should only run in certain environments (e.g., not in production, or
|
||||
only when a config flag is set), make it conditional:
|
||||
|
||||
```java
|
||||
.runMigration(config.isRunMigrations()) // driven by config value
|
||||
```
|
||||
|
||||
### Alternative — via application properties
|
||||
|
||||
If programmatic configuration is not available or not preferred, set the property
|
||||
in `src/main/resources/application.properties`:
|
||||
|
||||
```properties
|
||||
ebean.migration.run=true
|
||||
```
|
||||
|
||||
Or in `src/main/resources/application.yaml`:
|
||||
```yaml
|
||||
ebean:
|
||||
migration:
|
||||
run: true
|
||||
```
|
||||
|
||||
For a **named database** (i.e., `Database.builder().name("mydb")`), use the database
|
||||
name in the property key:
|
||||
|
||||
```properties
|
||||
ebean.mydb.migration.run=true
|
||||
```
|
||||
|
||||
### What the runner does at startup
|
||||
|
||||
When migration running is enabled, Ebean will on each application start:
|
||||
1. Look at the migrations in `src/main/resources/dbmigration/`
|
||||
2. Compare against the `db_migration` table (created automatically on first run)
|
||||
3. Apply any migrations that have not yet been executed, in version order
|
||||
4. Record each successfully applied migration in `db_migration`
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Commit the migration files
|
||||
|
||||
Add both generated files to source control:
|
||||
|
||||
```bash
|
||||
git add src/main/resources/dbmigration/1.1__add-customer.sql
|
||||
git add src/main/resources/dbmigration/model/1.1__add-customer.model.xml
|
||||
git commit -m "Add db migration 1.1: add-customer"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ongoing workflow — generating subsequent migrations
|
||||
|
||||
For each future set of entity bean changes:
|
||||
|
||||
1. Make changes to the entity bean classes
|
||||
2. Update `GenerateDbMigration.java` with the **new version** and **new name**:
|
||||
```java
|
||||
migration.setVersion("1.2");
|
||||
migration.setName("add-address-table");
|
||||
```
|
||||
3. Run the `main` method — a new `.sql` and `.model.xml` pair is written
|
||||
4. Review the generated `.sql` to confirm it reflects the intended changes
|
||||
5. Commit both files
|
||||
|
||||
---
|
||||
|
||||
## Understanding the output files
|
||||
|
||||
### Apply SQL (`.sql`)
|
||||
|
||||
The apply SQL file contains the DDL that will be executed against the database:
|
||||
|
||||
```sql
|
||||
-- apply changes
|
||||
alter table customer add column email varchar(255);
|
||||
```
|
||||
|
||||
### Model XML (`.model.xml`)
|
||||
|
||||
The model XML records the logical diff in a database-agnostic format. Ebean uses
|
||||
this file on the next generation run to determine what has already been captured.
|
||||
It is not executed against the database.
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<migration xmlns="http://ebean-orm.github.io/xml/ns/dbmigration">
|
||||
<changeSet type="apply">
|
||||
<addColumn tableName="customer">
|
||||
<column name="email" type="varchar(255)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</migration>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optional configurations
|
||||
|
||||
### Multiple database platforms
|
||||
|
||||
To generate migrations for multiple platforms simultaneously, use `addPlatform()`
|
||||
instead of `setPlatform()`:
|
||||
|
||||
```java
|
||||
migration.addPlatform(Platform.POSTGRES);
|
||||
migration.addPlatform(Platform.SQLSERVER17);
|
||||
migration.addPlatform(Platform.MYSQL);
|
||||
```
|
||||
|
||||
Each platform gets its own subdirectory under `dbmigration/`.
|
||||
|
||||
### Include index
|
||||
|
||||
When enabled the migration generation also generates a file that contains
|
||||
all the migrations and their associated hashes. This is a performance
|
||||
optimisation (that will become the default) and means that the migration
|
||||
runner just needs to read the one resource and has the pre-computed hash
|
||||
values (so does not need to read each migration resource and compute the
|
||||
hash for each of those at runtime).
|
||||
|
||||
```java
|
||||
migration.setIncludeIndex(true);
|
||||
```
|
||||
|
||||
### Strict mode
|
||||
|
||||
Strict mode (on by default) errors if there are any pending drops not yet applied.
|
||||
Set to `false` to allow generation to proceed regardless:
|
||||
|
||||
```java
|
||||
migration.setStrictMode(false);
|
||||
```
|
||||
|
||||
### Applying pending drops
|
||||
|
||||
Destructive changes (drop column, drop table) are **not** included in the apply
|
||||
SQL by default — they are recorded as `pendingDrops` in the model XML. This allows
|
||||
the application to be deployed without immediately dropping columns (important for
|
||||
rolling deployments).
|
||||
|
||||
The migration runner logs a message when pending drops exist:
|
||||
```
|
||||
INFO DbMigration - Pending un-applied drops in versions [1.1]
|
||||
```
|
||||
|
||||
When ready to apply the drops, set `setGeneratePendingDrop` to the version that
|
||||
contains the pending drops:
|
||||
|
||||
```java
|
||||
migration.setVersion("1.3");
|
||||
migration.setName("drop-pending-from-1.1");
|
||||
migration.setGeneratePendingDrop("1.1"); // apply drops recorded in version 1.1
|
||||
migration.generateMigration();
|
||||
```
|
||||
|
||||
### Custom dbSchema
|
||||
|
||||
If the project uses a named Postgres schema (set via `ebean.dbSchema` in
|
||||
`application.properties`), no additional configuration is needed in
|
||||
`GenerateDbMigration` — Ebean picks up the schema from the application config
|
||||
automatically when running in offline mode.
|
||||
|
||||
```properties
|
||||
# application.properties
|
||||
ebean.dbSchema=myschema
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|-------------|-----|
|
||||
| `no changes detected - no migration written` | Entity beans unchanged since last migration | Make entity bean changes first, then re-run |
|
||||
| `DbMigration - Pending un-applied drops` | A previous migration has drops not yet applied | Either suppress with `setStrictMode(false)` or apply drops with `setGeneratePendingDrop(...)` |
|
||||
| Generated SQL is empty or wrong | Wrong working directory path | Set `setPathToResources(...)` to the correct module-relative path |
|
||||
| `ClassNotFoundException` for entity classes | Test classpath not including main classes | Ensure `exec.classpathScope=test` or run via IDE with test classpath |
|
||||
| Migrations not running on startup | Property key wrong or `ebean-migration` missing | Verify `ebean[.name].migration.run=true` and that `ebean-migration` is on the classpath |
|
||||
@@ -0,0 +1,291 @@
|
||||
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 2: Database Configuration
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides step-by-step instructions for configuring an Ebean `Database` bean
|
||||
using **Avaje Inject** (`@Factory` / `@Bean`), backed by a PostgreSQL datasource built
|
||||
with Ebean's `DataSourceBuilder`. Follow every step in order. This is Step 3 of 3.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Step 1 complete**: `pom.xml` already includes `ebean-postgres`, `ebean-maven-plugin`,
|
||||
and `querybean-generator` (see `add-ebean-postgres-maven-pom.md`)
|
||||
- **Step 2 complete**: Test container setup is working and `mvn verify` passes
|
||||
(see `add-ebean-postgres-test-container.md`)
|
||||
- **Avaje Inject** is on the classpath (e.g. `io.avaje:avaje-inject`)
|
||||
- A configuration source is available at runtime (e.g. `avaje-config` reading
|
||||
`application.yml` or environment variables)
|
||||
- The following configuration keys are resolvable at runtime (adapt names to your project):
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `db_url` | JDBC URL for the master/write connection |
|
||||
| `db_user` | Database username |
|
||||
| `db_pass` | Database password |
|
||||
| `db_master_min_connections` | Minimum pool size (default: 1) |
|
||||
| `db_master_initial_connections` | Initial pool size at startup — set high to pre-warm on pod start (see K8s note below) |
|
||||
| `db_master_max_connections` | Maximum pool size (default: 200) |
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Locate or create the `@Factory` class
|
||||
|
||||
Look for an existing Avaje Inject `@Factory`-annotated class in the project
|
||||
(often named `AppConfig`, `DatabaseConfig`, or similar). If one exists, add the new
|
||||
`@Bean` method to it. If none exists, create one:
|
||||
|
||||
```java
|
||||
package com.example.configuration;
|
||||
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
|
||||
@Factory
|
||||
class DatabaseConfig {
|
||||
// beans will be added in the steps below
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Add the `Database` bean method (minimal — master datasource only)
|
||||
|
||||
Add the following `@Bean` method to the `@Factory` class. This creates an Ebean
|
||||
`Database` backed by a single master (read-write) PostgreSQL datasource.
|
||||
|
||||
```java
|
||||
import io.ebean.Database;
|
||||
import io.ebean.datasource.DataSourceBuilder;
|
||||
|
||||
@Bean
|
||||
Database database() {
|
||||
var dataSource = DataSourceBuilder.create()
|
||||
.url(/* resolve from config, e.g.: */ Config.get("db_url"))
|
||||
.username(Config.get("db_user"))
|
||||
.password(Config.get("db_pass"))
|
||||
.driver("org.postgresql.Driver")
|
||||
.schema("myschema") // set to your target schema
|
||||
.applicationName("my-app") // visible in pg_stat_activity
|
||||
.minConnections(Config.getInt("db_master_min_connections", 1))
|
||||
.initialConnections(Config.getInt("db_master_initial_connections", 10))
|
||||
.maxConnections(Config.getInt("db_master_max_connections", 200));
|
||||
|
||||
return Database.builder()
|
||||
.name("db") // logical name for this Database instance
|
||||
.dataSourceBuilder(dataSource)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### Field guidance
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `url` | Full JDBC URL, e.g. `jdbc:postgresql://host:5432/dbname` |
|
||||
| `schema` | The Postgres schema Ebean should use (omit if using `public`) |
|
||||
| `applicationName` | Shown in `pg_stat_activity.application_name`; helps with DB-side diagnostics |
|
||||
| `name("db")` | Logical Ebean database name; relevant if multiple Database instances exist |
|
||||
| `minConnections` | Connections kept open at all times; pool will not shrink below this |
|
||||
| `initialConnections` | Connections opened at startup; see K8s warm-up note below |
|
||||
| `maxConnections` | Hard upper limit on concurrent connections |
|
||||
|
||||
### Connection pool sizing for Kubernetes (and similar orchestrated environments)
|
||||
|
||||
When a pod starts in Kubernetes it will receive live traffic as soon as it passes
|
||||
readiness checks — often before the connection pool has had a chance to grow to handle
|
||||
the load. This can cause latency spikes on the first wave of requests while the pool
|
||||
expands one connection at a time.
|
||||
|
||||
Use `initialConnections` to **pre-warm the pool at startup** so it is already sized
|
||||
for peak load when the pod goes live:
|
||||
|
||||
```
|
||||
minConnections: 2 ← floor; pool will shrink back here when idle
|
||||
initialConnections: 20 ← opened at pod start, before first request arrives
|
||||
maxConnections: 50 ← hard ceiling
|
||||
```
|
||||
|
||||
The lifecycle is:
|
||||
1. **Pod starts** — pool opens `initialConnections` connections immediately.
|
||||
2. **Pod receives traffic** — pool is already at capacity; no growth latency.
|
||||
3. **Traffic drops** — idle connections are closed; pool trims back toward `minConnections`.
|
||||
4. **Next traffic spike** — pool grows again up to `maxConnections` on demand.
|
||||
|
||||
Set `initialConnections` to a value high enough that the pool does not need to grow
|
||||
during the first minute of live traffic. A common starting point is 50–75% of
|
||||
`maxConnections`.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Inject configuration via a constructor or config helper (recommended)
|
||||
|
||||
Rather than calling `Config.get(...)` inline, inject a typed config helper or the
|
||||
Avaje `Configuration` bean if one is available. This makes the factory testable and
|
||||
keeps the wiring explicit. For example:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
Database database(Configuration config) {
|
||||
String url = config.get("db_url");
|
||||
String user = config.get("db_user");
|
||||
String pass = config.get("db_pass");
|
||||
int min = config.getInt("db_master_min_connections", 1);
|
||||
int init = config.getInt("db_master_initial_connections", 10);
|
||||
int max = config.getInt("db_master_max_connections", 200);
|
||||
|
||||
var dataSource = DataSourceBuilder.create()
|
||||
.url(url)
|
||||
.username(user)
|
||||
.password(pass)
|
||||
.driver("org.postgresql.Driver")
|
||||
.schema("myschema")
|
||||
.applicationName("my-app")
|
||||
.minConnections(min)
|
||||
.initialConnections(init)
|
||||
.maxConnections(max);
|
||||
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(dataSource)
|
||||
.skipDataSourceCheck(true)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
If the project has a dedicated config-wrapper class (a `@Component` that reads config
|
||||
keys), accept it as a parameter instead of `Configuration`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 (Optional) — Add a read-only datasource
|
||||
|
||||
For production services that have a separate read-replica, add a second
|
||||
`DataSourceBuilder` for read-only queries and wire it via
|
||||
`readOnlyDataSourceBuilder(...)`. The read-only datasource:
|
||||
|
||||
- Uses `readOnly(true)` and `autoCommit(true)` (Ebean routes read queries there automatically)
|
||||
- Typically has a higher max connection count than the master
|
||||
- Benefits from a prepared-statement cache (`pstmtCacheSize`)
|
||||
|
||||
```java
|
||||
@Bean
|
||||
Database database(Configuration config) {
|
||||
String masterUrl = config.get("db_url");
|
||||
String readOnlyUrl = config.get("db_url_readonly");
|
||||
String user = config.get("db_user");
|
||||
String pass = config.get("db_pass");
|
||||
|
||||
var masterDataSource = buildDataSource(user, pass)
|
||||
.url(masterUrl)
|
||||
.minConnections(config.getInt("db_master_min_connections", 1))
|
||||
.initialConnections(config.getInt("db_master_initial_connections", 10))
|
||||
.maxConnections(config.getInt("db_master_max_connections", 50));
|
||||
|
||||
var readOnlyDataSource = buildDataSource(user, pass)
|
||||
.url(readOnlyUrl)
|
||||
.readOnly(true)
|
||||
.autoCommit(true)
|
||||
.pstmtCacheSize(250) // cache up to 250 prepared statements per connection
|
||||
.maxInactiveTimeSecs(600) // close idle connections after 10 minutes
|
||||
.minConnections(config.getInt("db_readonly_min_connections", 2))
|
||||
.initialConnections(config.getInt("db_readonly_initial_connections", 10))
|
||||
.maxConnections(config.getInt("db_readonly_max_connections", 200));
|
||||
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(masterDataSource)
|
||||
.readOnlyDataSourceBuilder(readOnlyDataSource)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DataSourceBuilder buildDataSource(String user, String pass) {
|
||||
return DataSourceBuilder.create()
|
||||
.username(user)
|
||||
.password(pass)
|
||||
.driver("org.postgresql.Driver")
|
||||
.schema("myschema")
|
||||
.applicationName("my-app")
|
||||
.addProperty("prepareThreshold", "2"); // PostgreSQL: server-side prepared statements
|
||||
}
|
||||
```
|
||||
|
||||
### Additional configuration keys for the read-only datasource
|
||||
|
||||
| Key | Description | Default |
|
||||
|-----|-------------|---------|
|
||||
| `db_url_readonly` | JDBC URL for the read replica | — |
|
||||
| `db_master_initial_connections` | Initial master pool size at startup | 10 |
|
||||
| `db_readonly_min_connections` | Minimum pool size | 2 |
|
||||
| `db_readonly_initial_connections` | Initial pool size at startup | same as min |
|
||||
| `db_readonly_max_connections` | Maximum pool size | 20 |
|
||||
|
||||
---
|
||||
|
||||
## Step 5 (Optional) — Enable the migration runner
|
||||
|
||||
If the project uses Ebean's built-in DB migration runner to apply SQL migrations on
|
||||
startup, enable it on the `DatabaseBuilder`:
|
||||
|
||||
```java
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(dataSource)
|
||||
.runMigration(true) // run pending migrations on startup
|
||||
.build();
|
||||
```
|
||||
|
||||
This is equivalent to setting `ebean.migration.run=true` in `application.properties`
|
||||
but is preferred because it keeps all database configuration in one place. To make it
|
||||
conditional (e.g. only in non-production environments):
|
||||
|
||||
```java
|
||||
.runMigration(config.getBoolean("db.runMigrations", false))
|
||||
```
|
||||
|
||||
See the DB migration generation guide (`add-ebean-db-migration-generation.md`) for
|
||||
full details on generating and managing migration files.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
For advanced connection pool configuration, production deployment patterns, and connection
|
||||
validation best practices, see the [ebean-datasource guides](https://github.com/ebean-orm/ebean-datasource/tree/master/docs/guides/):
|
||||
|
||||
- **[Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md)** — Covers read-only pools (`readOnly(true)` + `autoCommit(true)`), Kubernetes deployment strategies using `initialConnections`, and AWS Lambda optimization
|
||||
- **[AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md)** — Setting up dual DataSources with Aurora reader and writer endpoints, including Ebean secondary datasource routing
|
||||
- **[Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md)** — Why `Connection.isValid()` is the recommended default and when (rarely) explicit `heartbeatSql` is needed
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
1. Start the application (or run `mvn test -pl <your-module>`).
|
||||
2. Look for log output similar to:
|
||||
|
||||
```
|
||||
INFO o.a.datasource.pool.ConnectionPool - DataSourcePool [db] autoCommit[false] min[1] max[5]
|
||||
INFO io.ebean.internal.DefaultContainer - DatabasePlatform name:db platform:postgres
|
||||
```
|
||||
|
||||
3. If you see `DataSourcePool` and `DatabasePlatform` log lines, Ebean is connected and
|
||||
the database bean is wired correctly.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|-------------|-----|
|
||||
| `ClassNotFoundException: org.postgresql.Driver` | PostgreSQL JDBC driver missing | Add `org.postgresql:postgresql` dependency (see Step 1 guide) |
|
||||
| `Cannot connect to database` at startup | DB unreachable but `skipDataSourceCheck` is `false` | Set `.skipDataSourceCheck(true)` |
|
||||
| Ebean enhancement warnings in logs | `ebean-maven-plugin` not configured | Complete Step 1 guide |
|
||||
| `NullPointerException` reading config key | Config key not defined | Add the key to `application.yml` or environment |
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
The test container setup (Step 2) should already be complete and passing
|
||||
before this step. See `add-ebean-postgres-test-container.md`.
|
||||
@@ -0,0 +1,285 @@
|
||||
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 1: POM Setup
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides step-by-step instructions for modifying an existing Maven `pom.xml`
|
||||
to add Ebean ORM with PostgreSQL support. Follow every step in order. This is Step 1 of 3.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An existing Maven project (`pom.xml` already exists)
|
||||
- Java 11 or higher
|
||||
- The project does **not** yet include any Ebean dependencies
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — Gather requirements from the user
|
||||
|
||||
Before modifying any files, ask the user the following questions to determine
|
||||
the correct setup path. Record the answers — they affect dependency choices
|
||||
in this step and the approach used in Steps 2 and 3.
|
||||
|
||||
### Mandatory gate (do not skip)
|
||||
|
||||
- Do **not** continue to Step 1+ until the DI path is explicitly recorded.
|
||||
- Do **not** infer the **None** path by default. Use **None** only when the user explicitly confirms no DI framework.
|
||||
- If the user asks for a partial action (for example, "do only step 3"), keep the previously selected DI path; do not switch paths implicitly.
|
||||
|
||||
### DI path precedence (when user has not answered yet)
|
||||
|
||||
Use this precedence order:
|
||||
|
||||
1. Existing project context (highest priority): if dependencies/config already show Avaje Inject or Spring, select that path.
|
||||
2. Explicit user answer in this guide's questions.
|
||||
3. Recommended default only when context is genuinely unknown: Avaje Inject.
|
||||
|
||||
If context remains ambiguous, ask one multiple-choice clarification question and wait for the answer before editing files.
|
||||
|
||||
### Question 1: Dependency injection framework
|
||||
|
||||
> "Does this project use (or will it use) a DI framework? If so, which one?"
|
||||
|
||||
| Answer | Effect |
|
||||
|--------|--------|
|
||||
| **Avaje Inject** | Add `avaje-inject` + `avaje-inject-test` dependencies; use `@TestScope @Factory` for test container (Step 2); use `@Factory`/`@Bean` for production database (Step 3) |
|
||||
| **Spring** | Use Spring `@TestConfiguration` for test container (Step 2); use Spring `@Configuration`/`@Bean` for production database (Step 3) |
|
||||
| **None** | Use declarative `application-test.yaml` for test container (Step 2); use programmatic `Database.builder()` directly in application code (Step 3) |
|
||||
|
||||
### Question 2: PostGIS
|
||||
|
||||
> "Do you need PostGIS spatial extensions (geometry types, spatial queries)?"
|
||||
|
||||
| Answer | Effect |
|
||||
|--------|--------|
|
||||
| **Yes** | Use `PostgisContainer` in test setup (Step 2); may need `net.postgis:postgis-jdbc` dependency |
|
||||
| **No** | Use `PostgresContainer` in test setup (Step 2) |
|
||||
|
||||
### Question 3: Read replica
|
||||
|
||||
> "Does your production environment use a separate read-replica (read-only) database?"
|
||||
|
||||
| Answer | Effect |
|
||||
|--------|--------|
|
||||
| **Yes** | Configure a read-only `DataSourceBuilder` in production database config (Step 3) |
|
||||
| **No** | Single datasource only (Step 3) |
|
||||
|
||||
### Defaults
|
||||
|
||||
If the user is unsure or setting up a new project, recommend:
|
||||
- **Avaje Inject** (lightweight, fast compile-time DI)
|
||||
- **No PostGIS** (can be added later)
|
||||
- **No read replica** (can be added later)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Define the Ebean version property
|
||||
|
||||
Open the module's `pom.xml` (the one that will use Ebean directly, i.e. the module
|
||||
containing the database configuration and entity classes).
|
||||
|
||||
Inside the `<properties>` block, add the `ebean.version` property if it does not
|
||||
already exist:
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<!-- add this line; use latest stable from https://github.com/ebean-orm/ebean/releases -->
|
||||
<ebean.version>17.2.0</ebean.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
> If the project has a parent POM that already defines `ebean.version`, skip this step.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Add the PostgreSQL JDBC driver dependency
|
||||
|
||||
Inside the `<dependencies>` block, add the PostgreSQL JDBC driver:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.8</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
> Check [Maven Central](https://central.sonatype.com/artifact/org.postgresql/postgresql)
|
||||
> for the latest version. If the parent POM manages the PostgreSQL version, omit the
|
||||
> `<version>` tag.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Add the Ebean PostgreSQL platform dependency
|
||||
|
||||
Inside the `<dependencies>` block, add the Ebean Postgres platform dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgres</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
This single artifact pulls in the Ebean core, the datasource connection pool
|
||||
(`ebean-datasource`), and all Postgres-specific support.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Add the ebean-test dependency (test scope)
|
||||
|
||||
`ebean-test` configures Ebean for tests and enables automatic Docker container management
|
||||
for Postgres test instances:
|
||||
|
||||
```xml
|
||||
<!-- test dependencies -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4b — Add DI framework dependencies (if applicable)
|
||||
|
||||
If the user chose **Avaje Inject** in Step 0, add the following dependencies and
|
||||
annotation processor. Skip this step if the user chose Spring or no DI.
|
||||
|
||||
### Dependencies
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject</artifactId>
|
||||
<version>11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-test</artifactId>
|
||||
<version>11.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
> Check [Maven Central](https://central.sonatype.com/artifact/io.avaje/avaje-inject)
|
||||
> for the latest version.
|
||||
|
||||
### Annotation processor
|
||||
|
||||
The `avaje-inject-generator` must be added to the `annotationProcessorPaths` in
|
||||
`maven-compiler-plugin` (added in Step 6 below). When adding both processors,
|
||||
the final `<annotationProcessorPaths>` block should include both:
|
||||
|
||||
```xml
|
||||
<annotationProcessorPaths>
|
||||
<path> <!-- generate ebean query beans -->
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</path>
|
||||
<path> <!-- generate avaje-inject DI code -->
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-generator</artifactId>
|
||||
<version>11.5</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Add the ebean-maven-plugin (bytecode enhancement)
|
||||
|
||||
Ebean requires bytecode enhancement to provide dirty-checking and lazy-loading.
|
||||
The `ebean-maven-plugin` performs this enhancement at build time.
|
||||
|
||||
Inside the `<build><plugins>` block, add:
|
||||
|
||||
```xml
|
||||
<plugin> <!-- perform ebean enhancement -->
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-maven-plugin</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<extensions>true</extensions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Add the querybean-generator annotation processor
|
||||
|
||||
The `querybean-generator` annotation processor generates type-safe query bean classes
|
||||
at compile time. It must be registered as an `annotationProcessorPath` inside
|
||||
`maven-compiler-plugin`.
|
||||
|
||||
### Case A — No existing `maven-compiler-plugin` configuration
|
||||
|
||||
Add the full plugin entry to `<build><plugins>`:
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.15.0</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path> <!-- generate ebean query beans -->
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
### Case B — `maven-compiler-plugin` already exists with `<annotationProcessorPaths>`
|
||||
|
||||
Locate the existing `<annotationProcessorPaths>` block inside the existing
|
||||
`maven-compiler-plugin` entry and add the new `<path>` inside it. Do **not** add a
|
||||
second `<configuration>` block or a second `<annotationProcessorPaths>` block.
|
||||
|
||||
Example — if the existing block already has a path for, say, `avaje-nima-generator`:
|
||||
|
||||
```xml
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-nima-generator</artifactId>
|
||||
<version>${avaje-nima.version}</version>
|
||||
</path>
|
||||
<!-- ADD the new path here, inside the existing block -->
|
||||
<path>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
Run the following to confirm the POM is valid and both main and test sources compile:
|
||||
|
||||
```bash
|
||||
mvn test-compile
|
||||
```
|
||||
|
||||
Expected result: `BUILD SUCCESS` with no errors from Ebean or the annotation processor.
|
||||
Using `test-compile` rather than `compile` ensures test dependencies and test
|
||||
source files are also verified.
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Proceed to **Step 2: Test container setup**
|
||||
(`add-ebean-postgres-test-container.md`) to wire an injectable test `Database`
|
||||
backed by `ebean-test` containers. Verify with `mvn verify` before continuing
|
||||
to production database configuration.
|
||||
@@ -0,0 +1,435 @@
|
||||
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project - Step 2: Test Container Setup
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides step-by-step instructions for setting up a PostgreSQL Docker
|
||||
container for tests, exposing an `io.ebean.Database` instance for use in test
|
||||
classes. This is Step 2 of 3.
|
||||
|
||||
Complete this step before configuring the production database in Step 3. Getting
|
||||
the test container working first gives you a fast feedback loop - you can verify
|
||||
entity changes compile, enhance, and persist correctly with `mvn verify` before
|
||||
wiring up production datasource configuration.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Step 1 complete**: `pom.xml` includes `ebean-postgres`, `ebean-maven-plugin`,
|
||||
`querybean-generator`, and **`ebean-test`** as a test-scoped dependency
|
||||
(see `add-ebean-postgres-maven-pom.md`)
|
||||
- **Step 0 answers recorded**: DI framework choice and PostGIS requirement
|
||||
- **Docker** is installed and running on the developer machine
|
||||
|
||||
---
|
||||
|
||||
## Overview: Choosing your approach
|
||||
|
||||
The approach depends on the DI framework choice made in Step 0:
|
||||
|
||||
| DI framework | Approach | How |
|
||||
|--------------|----------|-----|
|
||||
| **Avaje Inject** | Programmatic | `@TestScope @Factory` class with injectable `Database` bean |
|
||||
| **Spring** | Programmatic | `@TestConfiguration` class with `@Bean` methods |
|
||||
| **None** | Declarative | `application-test.yaml` + plain JUnit test |
|
||||
|
||||
Follow the path that matches your choice below.
|
||||
|
||||
---
|
||||
|
||||
## Path A — Programmatic with Avaje Inject (recommended)
|
||||
|
||||
This approach uses `@TestScope @Factory` to expose the container and `Database`
|
||||
as injectable beans. It offers more control (image mirrors, custom config) and
|
||||
makes `Database` directly injectable into test classes.
|
||||
|
||||
### A.1 — Verify Avaje Inject test dependencies
|
||||
|
||||
Confirm the following are present in `pom.xml` (in addition to `ebean-test`):
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject</artifactId>
|
||||
<version>${avaje-inject.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-test</artifactId>
|
||||
<version>${avaje-inject.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
And the `avaje-inject-generator` annotation processor in `maven-compiler-plugin`:
|
||||
|
||||
```xml
|
||||
<path>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-generator</artifactId>
|
||||
<version>${avaje-inject.version}</version>
|
||||
</path>
|
||||
```
|
||||
|
||||
### A.2 — Create a `@TestScope @Factory` class
|
||||
|
||||
Create a new class in the test source tree (e.g., `src/test/java/.../testconfig/TestConfiguration.java`):
|
||||
|
||||
```java
|
||||
package com.example.testconfig;
|
||||
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.avaje.inject.test.TestScope;
|
||||
import io.ebean.Database;
|
||||
|
||||
@TestScope
|
||||
@Factory
|
||||
class TestConfiguration {
|
||||
// bean methods added below
|
||||
}
|
||||
```
|
||||
|
||||
### A.3 — Add a container bean and a Database bean
|
||||
|
||||
#### Plain PostgreSQL
|
||||
|
||||
```java
|
||||
import io.ebean.test.containers.PostgresContainer;
|
||||
|
||||
@TestScope
|
||||
@Factory
|
||||
class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("17") // Postgres image version
|
||||
.dbName("my_app") // database to create inside the container
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### PostGIS (PostgreSQL + PostGIS extension)
|
||||
|
||||
Use `PostgisContainer` instead. The default image is
|
||||
`ghcr.io/baosystems/postgis:{version}` and the extensions `hstore`, `pgcrypto`,
|
||||
and `postgis` are installed automatically.
|
||||
|
||||
```java
|
||||
import io.ebean.test.containers.PostgisContainer;
|
||||
|
||||
@TestScope
|
||||
@Factory
|
||||
class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
PostgisContainer postgres() {
|
||||
return PostgisContainer.builder("17")
|
||||
.dbName("my_app")
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Database database(PostgisContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Key differences
|
||||
|
||||
| | PostgresContainer | PostgisContainer |
|
||||
|---|---|---|
|
||||
| Docker image | `postgres:{version}` | `ghcr.io/baosystems/postgis:{version}` |
|
||||
| Default extensions | `hstore, pgcrypto` | `hstore, pgcrypto, postgis` |
|
||||
| Default port | 6432 | 6432 |
|
||||
| Optional LW mode | — | `.useLW(true)` (see Optional section) |
|
||||
|
||||
### A.4 — Write a test
|
||||
|
||||
Annotate the test class with `@InjectTest` and inject `Database` with `@Inject`:
|
||||
|
||||
```java
|
||||
package com.example.testconfig;
|
||||
|
||||
import io.avaje.inject.test.InjectTest;
|
||||
import io.ebean.Database;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@InjectTest
|
||||
class DatabaseTest {
|
||||
|
||||
@Inject
|
||||
Database database;
|
||||
|
||||
@Test
|
||||
void database_isAvailable() {
|
||||
assertThat(database).isNotNull();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### A.5 — Verify
|
||||
|
||||
```bash
|
||||
mvn verify
|
||||
```
|
||||
|
||||
Expected log output:
|
||||
|
||||
```
|
||||
INFO Container ut_postgres running with port:6432 ...
|
||||
INFO connectivity confirmed for ut_postgres
|
||||
INFO DataSourcePool [my_app] autoCommit[false] ...
|
||||
INFO DatabasePlatform name:my_app platform:postgres
|
||||
INFO Executing db-create-all.sql - ...
|
||||
```
|
||||
|
||||
**Important:** Verify this step passes with `mvn verify` before proceeding to
|
||||
Step 3 (production database configuration).
|
||||
|
||||
---
|
||||
|
||||
## Path B — Programmatic with Spring
|
||||
|
||||
Use Spring’s `@TestConfiguration` to provide the container and `Database` beans.
|
||||
|
||||
### B.1 — Create a `@TestConfiguration` class
|
||||
|
||||
```java
|
||||
package com.example.testconfig;
|
||||
|
||||
import io.ebean.Database;
|
||||
import io.ebean.test.containers.PostgresContainer;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
@TestConfiguration
|
||||
class TestDatabaseConfig {
|
||||
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("17")
|
||||
.dbName("my_app")
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
@Primary
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For PostGIS, use `PostgisContainer` instead (same pattern as Path A).
|
||||
|
||||
### B.2 — Write a test
|
||||
|
||||
```java
|
||||
package com.example;
|
||||
|
||||
import io.ebean.Database;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
class DatabaseTest {
|
||||
|
||||
@Autowired
|
||||
Database database;
|
||||
|
||||
@Test
|
||||
void database_isAvailable() {
|
||||
assertThat(database).isNotNull();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### B.3 — Verify
|
||||
|
||||
Run `mvn verify` and confirm the same log output as Path A.
|
||||
|
||||
---
|
||||
|
||||
## Path C — Declarative (no DI framework)
|
||||
|
||||
This is the simplest approach but offers less control. `ebean-test` reads a
|
||||
YAML config file and automatically manages the Docker container and `Database`
|
||||
instance. Use this when the project has no DI framework.
|
||||
|
||||
### C.1 — Create `application-test.yaml`
|
||||
|
||||
Create `src/test/resources/application-test.yaml`:
|
||||
|
||||
```yaml
|
||||
ebean:
|
||||
test:
|
||||
platform: postgres
|
||||
ddlMode: dropCreate
|
||||
dbName: my_app
|
||||
```
|
||||
|
||||
For PostGIS, use `platform: postgis` instead.
|
||||
|
||||
### C.2 — Write a test
|
||||
|
||||
Use `DB.getDefault()` to obtain the `Database` instance:
|
||||
|
||||
```java
|
||||
package com.example;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.Database;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DatabaseTest {
|
||||
|
||||
@Test
|
||||
void database_isAvailable() {
|
||||
Database database = DB.getDefault();
|
||||
assertThat(database).isNotNull();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### C.3 — Verify
|
||||
|
||||
```bash
|
||||
mvn verify
|
||||
```
|
||||
|
||||
Expected log output:
|
||||
|
||||
```
|
||||
INFO Container ut_postgres running with port:6432 ...
|
||||
INFO connectivity confirmed for ut_postgres
|
||||
INFO DataSourcePool [my_app] autoCommit[false] ...
|
||||
INFO DatabasePlatform name:my_app platform:postgres
|
||||
```
|
||||
|
||||
**Important:** Verify this passes before proceeding to Step 3.
|
||||
|
||||
Skip to [Optional configurations](#optional-configurations) or proceed to Step 3.
|
||||
|
||||
---
|
||||
|
||||
## Optional configurations
|
||||
|
||||
### Image mirror (for CI / private registry)
|
||||
|
||||
If CI builds pull images from a private registry (e.g., AWS ECR) instead of Docker Hub
|
||||
or GitHub Container Registry, specify a mirror. The mirror is **only used in CI** -
|
||||
it is ignored on local developer machines (where Docker Hub / GHCR is used directly).
|
||||
|
||||
```java
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("16")
|
||||
.dbName("my_app")
|
||||
.mirror("123456789.dkr.ecr.ap-southeast-2.amazonaws.com/mirrored")
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, set the mirror globally via a system property or
|
||||
`ebean.test.containers.mirror` in a properties file, avoiding code changes per project.
|
||||
|
||||
### Read-only datasource (for tests using read-replica simulation)
|
||||
|
||||
Call `.autoReadOnlyDataSource(true)` on the `DatabaseBuilder` to automatically
|
||||
create a second read-only datasource pointing at the same container:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.autoReadOnlyDataSource(true) // test read-only queries against same container
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### Dump metrics on shutdown
|
||||
|
||||
Useful for performance analysis during test runs:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.dumpMetricsOnShutdown(true)
|
||||
.dumpMetricsOptions("loc,sql,hash")
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### PostGIS: LW mode (HexWKB)
|
||||
|
||||
For PostGIS with DriverWrapperLW (HexWKB binary geometry encoding), set `.useLW(true)`.
|
||||
This switches the JDBC URL prefix to `jdbc:postgresql_lwgis://` and requires the
|
||||
`net.postgis:postgis-jdbc` dependency on the test classpath:
|
||||
|
||||
```xml
|
||||
<!-- add to pom.xml test dependencies when using useLW(true) -->
|
||||
<dependency>
|
||||
<groupId>net.postgis</groupId>
|
||||
<artifactId>postgis-jdbc</artifactId>
|
||||
<version>2024.1.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
```java
|
||||
@Bean
|
||||
PostgisContainer postgres() {
|
||||
return PostgisContainer.builder("16")
|
||||
.dbName("my_app")
|
||||
.useLW(true) // use HexWKB + DriverWrapperLW
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: LW mode is not required for most PostGIS use cases. Only enable it if
|
||||
> your entities use binary geometry types (e.g., `net.postgis.jdbc.geometry.Geometry`)
|
||||
> that require the `DriverWrapperLW` driver.
|
||||
|
||||
---
|
||||
|
||||
## Keeping the container running (local development)
|
||||
|
||||
By default, `ebean-test` stops the Docker container when tests finish. To keep it
|
||||
running between test runs (much faster for local development), create a marker file:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.ebean && touch ~/.ebean/ignore-docker-shutdown
|
||||
```
|
||||
|
||||
On CI servers, omit this file so containers are cleaned up after each build.
|
||||
@@ -0,0 +1,797 @@
|
||||
# Entity Bean Creation Guide for AI Agents
|
||||
|
||||
**Target Audience:** AI systems (Claude, Copilot, ChatGPT, etc.)
|
||||
**Purpose:** Learn how to generate clean, idiomatic Ebean entity beans
|
||||
**Key Insight:** Ebean entity fields must be non-public (no public fields). Accessors don't need JavaBeans naming conventions; no manual equals/hashCode implementation is needed
|
||||
**Language:** Java
|
||||
**Framework:** Ebean ORM
|
||||
|
||||
---
|
||||
|
||||
## Quick Rules
|
||||
|
||||
Before writing entity code, remember:
|
||||
|
||||
| Requirement | Needed? | Notes |
|
||||
|-------------|---------|-----------------------------------------------------------------------------------------------------------------------|
|
||||
| `@Entity` annotation | ✅ **YES** | Marks class as persistent entity |
|
||||
| `@Id` annotation | ✅ **YES** | Marks primary key field |
|
||||
| Getters/setters (or other accessors) | ✅ **YES** | Needed for application code to access fields. Naming can be JavaBeans, fluent, or custom — no specific convention required. |
|
||||
| Default constructor | ❌ **NO** | Not required. Ebean can instantiate without it. |
|
||||
| equals/hashCode | ❌ **NO** | Ebean auto-enhances these at compile time. |
|
||||
| toString() | ❌ **NO** | Ebean auto-enhances this. Don't implement with getters. |
|
||||
| `@Version` | ⚠️ **OPTIONAL** | Use for optimistic locking. Highly recommended. |
|
||||
| `@WhenCreated` | ⚠️ **OPTIONAL** | Auto-timestamp creation time. Highly recommended. Use for audit trail. |
|
||||
| `@WhenModified` | ⚠️ **OPTIONAL** | Auto-timestamp modification time. Highly recommended. Use for audit trail. |
|
||||
|
||||
**Critical:**
|
||||
- Prefer primitive `long` for `@Id` and `@Version`, NOT `Long` object.
|
||||
- Fields should be non-public: **private**, **protected**, or package-private.
|
||||
- If you add accessors, they do NOT need to follow Java bean conventions.
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions: The D* (Domain) Prefix Pattern
|
||||
|
||||
Entity beans represent internal domain/persistence model details. It's a common best practice in Ebean projects to use the **D* prefix** (D for Domain) for entity class names.
|
||||
|
||||
**Why use D* prefix?**
|
||||
|
||||
1. **Avoid name clashes with DTOs** - Your public API may have `Customer` (DTO), but your entity is `DCustomer` (Domain). They're clearly different.
|
||||
2. **Signal intent clearly** - The D prefix immediately tells developers "this is an internal domain class, not part of the public API"
|
||||
3. **Clarify conversions** - When converting `DCustomer` → `Customer` (DTO), the direction is obvious
|
||||
4. **Separate concerns** - API classes in one package (no prefix), domain classes in another (with D prefix)
|
||||
|
||||
**Example naming pattern:**
|
||||
- Entity: `DCustomer`, `DOrder`, `DProduct`, `DInvoice`
|
||||
- DTO: `Customer`, `Order`, `Product`, `Invoice`
|
||||
- Converter: `DCustomerMapper.toDTO(DCustomer)` → `Customer`
|
||||
|
||||
**Where to place entities:**
|
||||
- Entities: `com.example.domain.entity` (or `persistence`)
|
||||
- DTOs: `com.example.api.model` or `com.example.dto`
|
||||
|
||||
**When to use D* prefix:**
|
||||
- ✅ **DO** use for entity beans (internal domain model)
|
||||
- ✅ **DO** use when you have parallel DTO classes with similar names
|
||||
- ❌ **DON'T** use for DTOs or public API classes
|
||||
- ❌ **DON'T** use if you have no DTOs and entities are your public API
|
||||
|
||||
Example with and without prefix:
|
||||
|
||||
```java
|
||||
// With D* prefix (recommended - allows both entity and DTO to exist)
|
||||
@Entity
|
||||
public class DCustomer {
|
||||
@Id private long id;
|
||||
private String name;
|
||||
// ... entity-specific fields and methods
|
||||
}
|
||||
|
||||
// Public API DTO (no D prefix)
|
||||
public record Customer(long id, String name) {
|
||||
// ... conversion method
|
||||
}
|
||||
|
||||
// Conversion
|
||||
public static Customer toDTO(DCustomer entity) {
|
||||
return new Customer(entity.getId(), entity.getName());
|
||||
}
|
||||
```
|
||||
|
||||
This naming convention is optional but highly recommended for projects with separate domain and API layers.
|
||||
|
||||
---
|
||||
|
||||
## Minimal Entity (No Boilerplate)
|
||||
|
||||
This is a complete, valid Ebean entity:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- ✅ `@Entity` marks it as persistent
|
||||
- ✅ `@Id private long id` is the primary key
|
||||
- ✅ Private fields (Ebean does NOT support public fields without expert flags enabled)
|
||||
- ✅ Accessors can follow any naming convention, and can be omitted when field access is preferred
|
||||
- ✅ No default constructor needed
|
||||
- ✅ No equals/hashCode needed (Ebean enhances these)
|
||||
|
||||
**What Ebean does at compile time:**
|
||||
- Enhances equals/hashCode based on @Id
|
||||
- Adds field change tracking
|
||||
- Enables lazy loading
|
||||
- Enhances toString()
|
||||
|
||||
**Result:** Your entity is now fully functional with zero boilerplate.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: Basic Entity
|
||||
|
||||
**Use this when:** You need a simple persistent object.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Product {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private BigDecimal price;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(BigDecimal price) {
|
||||
this.price = price;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**What you get:**
|
||||
- Primary key: `id` (private field, accessed via getter)
|
||||
- Three properties: `name`, `description`, `price` (private fields, accessed via getters/setters)
|
||||
- Automatic equals/hashCode based on id
|
||||
- Full ORM functionality
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: Entity with Audit Trail
|
||||
|
||||
**Use this when:** You need to track who/when created/modified data.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
@Id
|
||||
private long id;
|
||||
@Version
|
||||
private long version;
|
||||
@WhenCreated
|
||||
private Instant createdAt;
|
||||
@WhenModified
|
||||
private Instant modifiedAt;
|
||||
|
||||
private String orderNumber;
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Instant getModifiedAt() {
|
||||
return modifiedAt;
|
||||
}
|
||||
|
||||
public String getOrderNumber() {
|
||||
return orderNumber;
|
||||
}
|
||||
|
||||
public void setOrderNumber(String orderNumber) {
|
||||
this.orderNumber = orderNumber;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalAmount() {
|
||||
return totalAmount;
|
||||
}
|
||||
|
||||
public void setTotalAmount(BigDecimal totalAmount) {
|
||||
this.totalAmount = totalAmount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**What you get:**
|
||||
- `version`: Optimistic locking (prevents concurrent update conflicts)
|
||||
- `createdAt`: Automatically set when inserted (Ebean manages this)
|
||||
- `modifiedAt`: Automatically updated on every modification (Ebean manages this)
|
||||
|
||||
**Example usage:**
|
||||
```java
|
||||
// Create
|
||||
Order order = new Order();
|
||||
order.setOrderNumber("ORD-001");
|
||||
order.setTotalAmount(new BigDecimal("99.99"));
|
||||
database.save(order); // createdAt is automatically set by Ebean
|
||||
|
||||
// Modify
|
||||
order.setTotalAmount(new BigDecimal("109.99"));
|
||||
database.update(order); // version incremented, modifiedAt updated automatically
|
||||
|
||||
// Check when modified
|
||||
System.out.println(order.getModifiedAt()); // Current timestamp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Entity with Constructor
|
||||
|
||||
**Use this when:** Domain logic requires initialization or validation.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Invoice {
|
||||
@Id
|
||||
private long id;
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
private String invoiceNumber;
|
||||
private String customerName;
|
||||
private BigDecimal amount;
|
||||
|
||||
public Invoice(String invoiceNumber, String customerName, BigDecimal amount) {
|
||||
this.invoiceNumber = invoiceNumber;
|
||||
this.customerName = customerName;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getInvoiceNumber() {
|
||||
return invoiceNumber;
|
||||
}
|
||||
|
||||
public String getCustomerName() {
|
||||
return customerName;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to add a constructor:**
|
||||
- ✅ Required fields must be set during creation
|
||||
- ✅ Validation needs to happen on initialization
|
||||
- ✅ Domain logic needs setup
|
||||
|
||||
**When NOT to add:**
|
||||
- ❌ If users will just set fields afterwards anyway
|
||||
- ❌ If there are many optional fields
|
||||
|
||||
---
|
||||
|
||||
## Pattern 4: Entity with Relationships
|
||||
|
||||
**Use this when:** You need associations to other entities.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
@Version
|
||||
private long version;
|
||||
@WhenCreated
|
||||
private Instant createdAt;
|
||||
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
@OneToMany(mappedBy = "customer")
|
||||
private List<Order> orders; // Use List, not Set
|
||||
|
||||
@ManyToOne
|
||||
private Address billingAddress;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public List<Order> getOrders() {
|
||||
return orders;
|
||||
}
|
||||
|
||||
public Address getBillingAddress() {
|
||||
return billingAddress;
|
||||
}
|
||||
|
||||
public void setBillingAddress(Address billingAddress) {
|
||||
this.billingAddress = billingAddress;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important:**
|
||||
- Use `List<>` not `Set<>` for collections (Set calls equals/hashCode before beans have IDs)
|
||||
- `mappedBy` means Order.customer is the owner
|
||||
- Relationships are lazy-loaded by default
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Do (Anti-Patterns)
|
||||
|
||||
### ❌ Anti-Pattern 1: Public Fields
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id public long id; // ❌ Public field - not supported
|
||||
public String name; // ❌ Public field - not supported
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id; // ✅ Private field with getter
|
||||
private String name; // ✅ Private field with accessors
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Ebean does NOT support public fields. Fields must be private and accessed via getters/setters or other accessor methods. Public fields bypass Ebean's tracking mechanisms and will cause data consistency issues.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 2: Use Long Object Instead of Primitive
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private Long id; // ❌ Object type
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id; // ✅ Primitive type
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Performance, nullability semantics, Ebean optimization.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 3: Implement equals/hashCode
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) { // ❌ Unnecessary
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Customer customer = (Customer) o;
|
||||
return id == customer.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() { // ❌ Unnecessary
|
||||
return Objects.hash(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
// Ebean enhances equals/hashCode automatically
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Ebean's enhancement is optimized for ORM operations. Your implementation might conflict with Ebean's tracking.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 4: Use Set for Collections
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@OneToMany(mappedBy = "customer")
|
||||
private Set<Order> orders; // ❌ Set calls equals/hashCode before IDs assigned
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@OneToMany(mappedBy = "customer")
|
||||
private List<Order> orders; // ✅ List doesn't require equals/hashCode on unsaved beans
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Set calls equals/hashCode immediately. New beans don't have IDs yet, causing issues.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 5: toString() with Getters
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public String toString() { // ❌ Uses getters
|
||||
return "Customer{" +
|
||||
"id=" + getId() +
|
||||
", name='" + getName() + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
public long getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** In a debugger, toString() is called automatically. Getters can trigger lazy loading, changing debug behavior.
|
||||
|
||||
**DO:** Either don't implement toString(), or access fields directly:
|
||||
```java
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Customer{" +
|
||||
"id=" + id +
|
||||
", name='" + name + '\'' +
|
||||
'}';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 6: @Column(name=...) for Naming Convention
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@Column(name = "first_name") // ❌ Unnecessary
|
||||
private String firstName;
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
private String firstName; // ✅ Ebean uses naming convention: first_name
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Ebean's naming convention handles this automatically. Only use @Column when your database column doesn't match the convention.
|
||||
|
||||
---
|
||||
|
||||
## What Ebean Enhancement Provides
|
||||
|
||||
At compile time, Ebean enhances your entity classes:
|
||||
|
||||
1. **equals/hashCode** - Based on @Id, optimal for ORM
|
||||
2. **Field change tracking** - Knows which fields were modified
|
||||
3. **Lazy loading** - Collections and relationships load on demand
|
||||
4. **Persistence context** - Manages identity and state
|
||||
5. **toString()** - Auto-implemented (don't override with getters)
|
||||
|
||||
**Result:** Your entity bean is minimal, but fully featured.
|
||||
|
||||
---
|
||||
|
||||
## Field Types
|
||||
|
||||
**Recommended for ID/Version:**
|
||||
- `long` (primitive) ✅ Use this
|
||||
- `int` (primitive) ✅ Use this
|
||||
- `UUID` ✅ Use this
|
||||
|
||||
**Not recommended:**
|
||||
- `Long` object ⚠️ Avoid (use primitive long)
|
||||
- `Integer` object ⚠️ Avoid (use primitive int)
|
||||
|
||||
**For other fields:**
|
||||
- Use standard Java types: `String`, `BigDecimal`, `Instant`, `LocalDate`, etc.
|
||||
- Use primitives where nullable semantics don't apply: `int`, `long`, `boolean`
|
||||
- Use objects where null has meaning: `String`, `BigDecimal`, `LocalDate`
|
||||
|
||||
---
|
||||
|
||||
## Example: Building an Entity Step by Step
|
||||
|
||||
Start minimal, add what you need:
|
||||
|
||||
**Step 1: Minimal**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
String title;
|
||||
String content;
|
||||
|
||||
public long getId() { return id; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Add audit trail**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
@Version long version;
|
||||
@WhenCreated Instant createdAt;
|
||||
@WhenModified Instant modifiedAt;
|
||||
|
||||
String title;
|
||||
String content;
|
||||
|
||||
public long getId() { return id; }
|
||||
public long getVersion() { return version; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public Instant getModifiedAt() { return modifiedAt; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Add author relationship**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
@Version long version;
|
||||
@WhenCreated Instant createdAt;
|
||||
@WhenModified Instant modifiedAt;
|
||||
|
||||
String title;
|
||||
String content;
|
||||
|
||||
@ManyToOne
|
||||
Author author;
|
||||
|
||||
public long getId() { return id; }
|
||||
public long getVersion() { return version; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public Instant getModifiedAt() { return modifiedAt; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public Author getAuthor() { return author; }
|
||||
public void setAuthor(Author author) { this.author = author; }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Add constructor if needed**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
@Version long version;
|
||||
@WhenCreated Instant createdAt;
|
||||
@WhenModified Instant modifiedAt;
|
||||
|
||||
String title;
|
||||
String content;
|
||||
|
||||
@ManyToOne
|
||||
Author author;
|
||||
|
||||
public BlogPost(String title, String content, Author author) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public long getId() { return id; }
|
||||
public long getVersion() { return version; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public Instant getModifiedAt() { return modifiedAt; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public Author getAuthor() { return author; }
|
||||
public void setAuthor(Author author) { this.author = author; }
|
||||
}
|
||||
```
|
||||
|
||||
Each step adds only what's necessary for the entity structure. Accessors are always included so application code can use the entity.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating and saving
|
||||
```java
|
||||
Customer customer = new Customer();
|
||||
customer.setName("Alice");
|
||||
database.save(customer); // id auto-generated
|
||||
```
|
||||
|
||||
### Finding
|
||||
```java
|
||||
Customer found = database.find(Customer.class, 1);
|
||||
System.out.println(found.getName());
|
||||
```
|
||||
|
||||
### Updating
|
||||
```java
|
||||
found.setName("Bob");
|
||||
database.update(found); // version auto-incremented
|
||||
```
|
||||
|
||||
### Collections (relationships)
|
||||
```java
|
||||
Customer customer = database.find(Customer.class, 1);
|
||||
List<Order> orders = customer.getOrders(); // Lazy loads automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary for AI Agents
|
||||
|
||||
When generating Ebean entity beans:
|
||||
|
||||
✅ **DO:**
|
||||
- Use primitive `long` for @Id and @Version
|
||||
- Keep entities minimal (just fields + @Entity + @Id)
|
||||
- Use @Version for concurrency control
|
||||
- Use @WhenCreated/@WhenModified for audit trail
|
||||
- Use List for collections, not Set
|
||||
- Add constructors only if domain logic requires it
|
||||
- Add getters/setters for all fields that application code needs to read or write
|
||||
|
||||
❌ **DON'T:**
|
||||
- Use Long object for @Id/@Version
|
||||
- Implement equals/hashCode
|
||||
- Implement toString() with getters
|
||||
- Use Set for @OneToMany/@ManyToMany
|
||||
- Add unnecessary @Column annotations
|
||||
- Add default constructors "just in case"
|
||||
|
||||
**Result:** Clean, readable, maintainable entity beans with full ORM functionality and zero boilerplate.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- Entity Bean Best Practices: `/docs/best-practice/`
|
||||
- JPA Mapping Reference: `/docs/mapping/jpa/`
|
||||
- Ebean Extensions: `/docs/mapping/extensions/`
|
||||
- First Entity Guide: `/docs/intro/first-entity/`
|
||||
@@ -0,0 +1,136 @@
|
||||
# Immutable bean cache for read-only references
|
||||
|
||||
This guide shows how to use `ImmutableBeanCache` for read-mostly assoc-one
|
||||
references (for example `Label` references reused across many entities).
|
||||
|
||||
Use this when you want:
|
||||
|
||||
- fewer lazy-load SQL calls for assoc-one references via caching
|
||||
- reusable fetch-group-based loading for cache misses
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Build an immutable cache (typical via builder)
|
||||
|
||||
```java
|
||||
FetchGroup<Label> fetchGroup = FetchGroup.of(Label.class)
|
||||
.select("version")
|
||||
.fetch("labelTexts", "locale, localeText")
|
||||
.build();
|
||||
|
||||
ImmutableBeanCache<Label> labelCache = ImmutableBeanCaches.builder(Label.class)
|
||||
.loading(database, fetchGroup)
|
||||
.maxSize(10_000)
|
||||
.maxIdleSeconds(300)
|
||||
.maxSecondsToLive(6_000)
|
||||
.build();
|
||||
```
|
||||
|
||||
`loading(...)` uses the query shape:
|
||||
|
||||
- `select(fetchGroup)`
|
||||
- `setUnmodifiable(true)`
|
||||
- `where().idIn(ids)`
|
||||
- `findMap()`
|
||||
|
||||
Alternative domain example:
|
||||
|
||||
```java
|
||||
FetchGroup<Customer> customerGroup = FetchGroup.of(Customer.class)
|
||||
.select("name,version")
|
||||
.fetch("billingAddress", "line1,city")
|
||||
.fetch("shippingAddress", "line1,city")
|
||||
.build();
|
||||
|
||||
ImmutableBeanCache<Customer> customerCache = ImmutableBeanCaches.builder(Customer.class)
|
||||
.loading(database, customerGroup)
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Attach cache to the root query
|
||||
|
||||
```java
|
||||
AttributeDescriptor one = DB.find(AttributeDescriptor.class)
|
||||
.setId(id)
|
||||
.setUnmodifiable(true)
|
||||
.using(labelCache)
|
||||
.findOne();
|
||||
```
|
||||
|
||||
`using(...)` is on the root query. Ebean will use this cache for matching
|
||||
bean types when resolving references.
|
||||
|
||||
---
|
||||
|
||||
## Use loading helper for simple memoization
|
||||
|
||||
If you don't need policy controls, use the shorthand helper:
|
||||
|
||||
```java
|
||||
ImmutableBeanCache<Label> labelCache =
|
||||
ImmutableBeanCaches.loading(Label.class, database, FetchGroup.of(Label.class, "version"));
|
||||
```
|
||||
|
||||
With `ebean-core` on the classpath, builder policy settings are backed by core
|
||||
cache implementation (including periodic trim / eviction).
|
||||
|
||||
---
|
||||
|
||||
## Unmodifiable vs mutable query behavior
|
||||
|
||||
### Unmodifiable query path
|
||||
|
||||
`setUnmodifiable(true)` disables lazy loading. If you need association content
|
||||
in cached beans make sure that is included in the fetch group.
|
||||
|
||||
```java
|
||||
FetchGroup<Label> withTexts = FetchGroup.of(Label.class)
|
||||
.select("version")
|
||||
.fetch("labelTexts", "locale, localeText")
|
||||
.build();
|
||||
```
|
||||
|
||||
### Mutable query path
|
||||
|
||||
On a mutable query (no `setUnmodifiable(true)`), references populated from the
|
||||
immutable cache are still mutable beans in that object graph. Additional
|
||||
unloaded properties can still lazy load as normal.
|
||||
|
||||
Typical pattern:
|
||||
|
||||
1. cache serves already-loaded reference properties (for example `version`)
|
||||
2. later access to unloaded properties (for example `labelTexts`) triggers
|
||||
normal lazy loading
|
||||
|
||||
---
|
||||
|
||||
## Understand secondary query behavior (`+query`, `+lazy`)
|
||||
|
||||
When root queries execute secondary loads (`fetchQuery(...)` or `fetchLazy(...)`),
|
||||
the immutable caches configured on the root query are propagated to those
|
||||
secondary queries.
|
||||
|
||||
That means assoc-one references resolved in secondary query paths can still hit
|
||||
the immutable cache.
|
||||
|
||||
---
|
||||
|
||||
## Operational note (TTL / max size)
|
||||
|
||||
Use `ImmutableBeanCaches.builder(...)` when you need explicit TTL/max-size
|
||||
policy. `ImmutableBeanCaches.loading(...)` remains the simple helper for
|
||||
loader-based memoization.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
1. Hit / partial hit / miss behavior for `getAll(ids)`
|
||||
2. Unmodifiable path: no lazy SQL when reading loaded reference properties
|
||||
3. Mutable path: additional unloaded properties can still lazy load
|
||||
4. Secondary `fetchQuery` and `fetchLazy` paths inherit immutable caches
|
||||
5. If needed associations are in fetch group, assert no extra SQL for those
|
||||
accesses
|
||||
@@ -0,0 +1,206 @@
|
||||
# Guide: Using Lombok with Ebean Entity Beans
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains which Lombok annotations are safe and recommended for Ebean
|
||||
entity beans, which ones to avoid, and why. It is written as prescriptive instructions
|
||||
for AI agents and developers.
|
||||
|
||||
---
|
||||
|
||||
## The Core Rule
|
||||
|
||||
> **Do NOT use `@Data` on Ebean entity beans.**
|
||||
|
||||
Use `@Getter` + `@Setter` instead, with the optional `@Accessors(chain = true)` for
|
||||
a fluent setter style.
|
||||
|
||||
---
|
||||
|
||||
## Why `@Data` is Incompatible with Ebean
|
||||
|
||||
`@Data` is a convenience annotation that is equivalent to applying `@Getter`,
|
||||
`@Setter`, `@RequiredArgsConstructor`, `@ToString`, and `@EqualsAndHashCode` together.
|
||||
Three of those are problematic for Ebean entity beans:
|
||||
|
||||
### 1. `@EqualsAndHashCode` (included in `@Data`) — breaks entity identity
|
||||
|
||||
`@Data` generates `hashCode()` and `equals()` based on all non-static, non-transient
|
||||
fields. Ebean entity beans have identity semantics — two references to the same database
|
||||
row should be considered equal based on their `@Id` value, not field-by-field comparison.
|
||||
|
||||
Problems caused:
|
||||
- Inconsistent `hashCode` before and after persist (the `@Id` field is `0` on a new
|
||||
entity, then changes after insert — violating the `hashCode` contract for collections)
|
||||
- Entities placed in a `Set` or `HashMap` before saving will be unfindable after saving
|
||||
- Ebean's internal identity map and dirty checking can be confused
|
||||
|
||||
### 2. `@ToString` (included in `@Data`) — triggers unexpected lazy loading
|
||||
|
||||
`@Data` generates a `toString()` that accesses **all** fields, including
|
||||
`@OneToMany` and `@ManyToOne` associations. Accessing an unloaded lazy association
|
||||
outside of a transaction triggers a `LazyInitialisationException` or fires an unexpected
|
||||
SQL query, which can:
|
||||
- Cause subtle bugs in logging statements
|
||||
- Trigger N+1 queries in test output or debug logging
|
||||
- Fail with an exception if no active transaction exists
|
||||
|
||||
### 3. `@RequiredArgsConstructor` (included in `@Data`) — unnecessary for Ebean
|
||||
|
||||
Ebean does not require a default constructor — it can construct entity instances without
|
||||
one. `@RequiredArgsConstructor` therefore adds nothing useful to entity beans.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Annotation Set
|
||||
|
||||
Use exactly these three Lombok annotations on every Ebean entity bean:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@Accessors(chain = true)
|
||||
@Table(name = "my_table")
|
||||
public class MyEntity {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
| Annotation | Purpose |
|
||||
|---|---|
|
||||
| `@Getter` | Generates `getFoo()` / `isFoo()` accessor methods |
|
||||
| `@Setter` | Generates `setFoo(value)` mutator methods; Ebean enhancement intercepts these for dirty tracking |
|
||||
| `@Accessors(chain = true)` | Makes setters return `this`, enabling fluent/builder-style property setting |
|
||||
|
||||
---
|
||||
|
||||
## `@Accessors(chain = true)` — Fluent Setter Style
|
||||
|
||||
With `chain = true`, setters return `this` instead of `void`, allowing method chaining:
|
||||
|
||||
```java
|
||||
// without chain = true (void setters)
|
||||
CMachine machine = new CMachine();
|
||||
machine.setMake("Toyota");
|
||||
machine.setModel("Hilux");
|
||||
machine.setStatus("active");
|
||||
|
||||
// with @Accessors(chain = true)
|
||||
CMachine machine = new CMachine()
|
||||
.setMake("Toyota")
|
||||
.setModel("Hilux")
|
||||
.setStatus("active");
|
||||
```
|
||||
|
||||
This is particularly useful when building test data:
|
||||
|
||||
```java
|
||||
CMachine machine = new CMachine()
|
||||
.setGid(UUID.randomUUID())
|
||||
.setMachineType("HV")
|
||||
.setStatus("active")
|
||||
.setMake("Komatsu")
|
||||
.setModel("PC200");
|
||||
|
||||
database.save(machine);
|
||||
```
|
||||
|
||||
Ebean's bytecode enhancement is fully compatible with chained setters — the
|
||||
enhancement intercepts each `setFoo()` call to record which fields have been modified
|
||||
(dirty checking), regardless of whether the setter returns `void` or `this`.
|
||||
|
||||
---
|
||||
|
||||
## `@Accessors(fluent = true)` — also compatible
|
||||
|
||||
`@Accessors(fluent = true)` removes the `get`/`set`/`is` prefix, generating `name()`
|
||||
(getter) and `name(value)` (setter) instead of `getName()` and `setName(value)`.
|
||||
|
||||
Ebean does **not** require JavaBeans naming conventions — it can work with any accessor
|
||||
method style, including fluent accessors with no prefix. `@Accessors(fluent = true)` is
|
||||
therefore compatible with Ebean.
|
||||
|
||||
`@Accessors(chain = true)` is the more common choice in practice (it keeps the familiar
|
||||
`get`/`set` prefix while adding method chaining), but `fluent = true` is a valid
|
||||
alternative if that style is preferred consistently across the codebase.
|
||||
|
||||
---
|
||||
|
||||
## Full Entity Bean Example
|
||||
|
||||
```java
|
||||
package com.example.repository.data;
|
||||
|
||||
import io.ebean.annotation.WhenCreated;
|
||||
import io.ebean.annotation.WhenModified;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@Accessors(chain = true)
|
||||
@Table(name = "machine")
|
||||
public class CMachine {
|
||||
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private UUID gid;
|
||||
|
||||
@Column(nullable = false, length = 10)
|
||||
private String machineType;
|
||||
|
||||
@Column(length = 200)
|
||||
private String make;
|
||||
|
||||
@Column(length = 200)
|
||||
private String model;
|
||||
|
||||
@WhenCreated
|
||||
private Instant created;
|
||||
|
||||
@WhenModified
|
||||
private Instant lastModified;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary: Lombok Annotations and Ebean Compatibility
|
||||
|
||||
| Lombok Annotation | Compatible? | Notes |
|
||||
|---|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `@Getter` | ✅ Safe | Use on every entity bean |
|
||||
| `@Setter` | ✅ Safe | Use on every entity bean; enhancement intercepts these |
|
||||
| `@Accessors(chain = true)` | ✅ Safe | Recommended for fluent construction style |
|
||||
| `@ToString` | ❌ Avoid | Ebean does a better job and handles recursion |
|
||||
| `@EqualsAndHashCode` | ❌ Avoid | Breaks entity identity and `@Id`-based equality |
|
||||
| `@Data` | ❌ Avoid | Includes `@EqualsAndHashCode` and `@ToString` — both problematic |
|
||||
| `@Value` | ❌ Avoid | Makes fields final — incompatible with Ebean's field-level bytecode enhancement |
|
||||
| `@Accessors(fluent = true)` | ✅ Safe | Removes `get`/`set` prefix — Ebean does not require JavaBeans naming conventions and works with any accessor style |
|
||||
| `@Builder` | ⚠️ Careful | Usable on non-entity helper/factory classes; on entity beans it requires a no-arg constructor alongside it and offers no advantage over `@Accessors(chain = true)` |
|
||||
|
||||
---
|
||||
|
||||
## Relationship with Ebean Bytecode Enhancement
|
||||
|
||||
Ebean's bytecode enhancement (applied by `ebean-maven-plugin` at build time) modifies
|
||||
the `setXxx()` methods of entity beans to:
|
||||
1. Mark the field as dirty (changed) so only modified fields are included in UPDATE statements
|
||||
2. Support lazy loading of associations when a getter is called on an unloaded field
|
||||
|
||||
For this to work correctly, Ebean needs:
|
||||
- Accessor methods for each persistent field (any naming style is fine — `getFoo()`, `foo()`, or no accessors at all; Ebean can also access fields directly)
|
||||
- No override of `hashCode()` / `equals()` that would interfere with the identity map — which means **no `@Data` or `@EqualsAndHashCode`**
|
||||
@@ -0,0 +1,447 @@
|
||||
# Guide: Persist Changes and Manage Transactions with Ebean
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide gives step-by-step instructions for AI agents and developers to save,
|
||||
update, delete, and batch changes with Ebean while choosing the correct
|
||||
transaction boundary.
|
||||
|
||||
Use this guide when you need to:
|
||||
|
||||
- create a new entity row
|
||||
- update one or more existing rows
|
||||
- delete rows safely
|
||||
- decide between implicit transactions, `@Transactional`, and explicit
|
||||
transactions
|
||||
- batch or bulk-write many rows efficiently
|
||||
|
||||
The default recommendation is:
|
||||
|
||||
1. Choose the correct persistence operation first
|
||||
2. Use implicit transactions for a single isolated write
|
||||
3. Use `@Transactional` for multi-step application workflows
|
||||
4. Use explicit transactions only when you need explicit control
|
||||
5. Use bulk update or batching for large write sets
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The project already uses Ebean ORM
|
||||
- Entity beans and database configuration already exist
|
||||
- You know which `Database` is being used (`DB.getDefault()` or a named database)
|
||||
|
||||
If the project is not yet configured, first follow:
|
||||
|
||||
- [`add-ebean-postgres-database-config.md`](add-ebean-postgres-database-config.md)
|
||||
- [`entity-bean-creation.md`](entity-bean-creation.md)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Choose the correct persistence operation before editing code
|
||||
|
||||
Do not start with `database.save(...)` by habit. First decide what kind of change the
|
||||
caller is making.
|
||||
|
||||
| Need | Preferred API | Use when |
|
||||
|------|---------------|----------|
|
||||
| Insert a bean that is definitely new | `database.insert(bean)` | New-create flow, seed data, fixture setup |
|
||||
| Save a bean that may be new or existing | `database.save(bean)` | Common default when bean state determines insert vs update |
|
||||
| Update a bean that is definitely existing | `database.update(bean)` | Existing row should be updated only |
|
||||
| Delete one bean | `database.delete(bean)` | Remove a loaded entity bean |
|
||||
| Update many rows without loading beans | `database.update(...)` or `query.asUpdate()` | Set-based write, not per-row business logic |
|
||||
| Delete many rows without loading beans | bulk update/delete API or `database.sqlUpdate(...)` | Set-based deletion |
|
||||
|
||||
### Agent rule
|
||||
|
||||
Choose the operation that matches intent:
|
||||
|
||||
- known new row -> `insert`
|
||||
- known existing row -> `update`
|
||||
- uncertain/new-or-existing -> `save`
|
||||
- many rows -> bulk update/delete, not a loop of individual saves
|
||||
|
||||
### Style note
|
||||
|
||||
Use a `Database` instance for all persistence operations: `database.save(bean)`,
|
||||
`database.insert(bean)`, `database.update(bean)`, `database.delete(bean)`.
|
||||
Inject the `Database` bean or obtain it via `DB.getDefault()`. Avoid using the
|
||||
static `DB.*` convenience methods.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Persist single-bean changes with the correct API
|
||||
|
||||
### Example - insert a known new bean
|
||||
|
||||
```java
|
||||
Customer customer = new Customer();
|
||||
customer.setName("Rob");
|
||||
customer.setEmail("rob@example.com");
|
||||
|
||||
database.insert(customer);
|
||||
```
|
||||
|
||||
### Example - update an existing bean
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.id.equalTo(customerId)
|
||||
.findOne();
|
||||
|
||||
customer.setStatus(Customer.Status.ACTIVE);
|
||||
|
||||
database.update(customer);
|
||||
```
|
||||
|
||||
### When to prefer `insert()` over `save()`
|
||||
|
||||
Use `insert()` when the code is creating a brand new row and should fail if the
|
||||
operation does not behave like an insert.
|
||||
|
||||
### When to prefer `update()` over `save()`
|
||||
|
||||
Use `update()` when the bean is definitely existing and the method should not
|
||||
silently behave like an insert.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Check cascade mappings before assuming related beans will persist or delete
|
||||
|
||||
Ebean follows cascade rules defined on mapping annotations such as
|
||||
`@OneToMany`, `@OneToOne`, `@ManyToOne`, and `@ManyToMany`.
|
||||
|
||||
The default is **no cascade**.
|
||||
|
||||
### Example
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
|
||||
@ManyToOne
|
||||
private Customer customer; // no cascade by default
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL)
|
||||
private List<OrderDetail> details; // save + delete cascade
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
database.save(order);
|
||||
```
|
||||
|
||||
With the mapping above:
|
||||
|
||||
- `details` are cascaded
|
||||
- `customer` is **not** cascaded
|
||||
|
||||
### Agent rules for cascades
|
||||
|
||||
1. Inspect the mapping before writing save/delete logic
|
||||
2. Do not assume `@ManyToOne` cascades
|
||||
3. Avoid adding cascade to shared parent references unless ownership is truly
|
||||
intended
|
||||
4. If a relationship should not cascade, save/delete related beans explicitly
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - Let Ebean use an implicit transaction for a single isolated write
|
||||
|
||||
If the method performs one isolated persistence operation, Ebean can manage the
|
||||
transaction implicitly.
|
||||
|
||||
### Good fit for implicit transaction
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.id.equalTo(customerId)
|
||||
.findOne();
|
||||
|
||||
customer.setStatus(Customer.Status.INACTIVE);
|
||||
database.save(customer);
|
||||
```
|
||||
|
||||
### Good fit
|
||||
|
||||
- one save
|
||||
- one update
|
||||
- one delete
|
||||
- small helper method with a single write
|
||||
|
||||
### Poor fit
|
||||
|
||||
- multiple writes that must commit or roll back together
|
||||
- query + save + save workflow
|
||||
- any method where later failure must roll back earlier writes
|
||||
|
||||
### Important
|
||||
|
||||
Queries also use implicit transactions when needed. You generally do **not**
|
||||
need to wrap ordinary read queries in an explicit transaction "just in case".
|
||||
|
||||
---
|
||||
|
||||
## Step 5 - Use `@Transactional` for multi-step service workflows
|
||||
|
||||
When multiple Ebean operations belong to one unit of work, use
|
||||
`@Transactional`.
|
||||
|
||||
### Example - service method
|
||||
|
||||
```java
|
||||
import io.ebean.annotation.Transactional;
|
||||
|
||||
@Transactional
|
||||
public void shipOrder(long orderId) {
|
||||
|
||||
Order order = new QOrder()
|
||||
.id.equalTo(orderId)
|
||||
.findOne();
|
||||
|
||||
order.setStatus(Order.Status.SHIPPED);
|
||||
database.save(order);
|
||||
|
||||
Shipment shipment = new Shipment(order, Instant.now());
|
||||
database.insert(shipment);
|
||||
}
|
||||
```
|
||||
|
||||
All database work inside the method runs in one transaction and commits only if
|
||||
the method completes successfully.
|
||||
|
||||
### Use `Transaction.current()` only when needed
|
||||
|
||||
If the method needs access to the current transaction itself:
|
||||
|
||||
```java
|
||||
Transaction txn = Transaction.current();
|
||||
```
|
||||
|
||||
Do this only for transaction-specific behavior such as comments, savepoints, or
|
||||
other advanced control. Do not fetch the current transaction if the method does
|
||||
not need it.
|
||||
|
||||
### Agent rules for `@Transactional`
|
||||
|
||||
1. Put it on application/service workflow methods, not everywhere by default
|
||||
2. Keep the transaction focused on database work
|
||||
3. Avoid remote HTTP calls, message publishing, or long-running CPU work inside
|
||||
the transaction if those can be moved outside
|
||||
|
||||
### Named database note
|
||||
|
||||
If the method uses a non-default database, obtain that `Database` instance via
|
||||
`DB.byName("...")` and consistently use that database for both queries and
|
||||
writes.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 - Use `beginTransaction()` when you need explicit control
|
||||
|
||||
Use an explicit transaction when you need manual `commit()`, batching, explicit
|
||||
flush, savepoints, or other low-level transaction control.
|
||||
|
||||
### Example - explicit transaction with try-with-resources
|
||||
|
||||
```java
|
||||
try (Transaction txn = database.beginTransaction()) {
|
||||
|
||||
Order order = new QOrder()
|
||||
.id.equalTo(orderId)
|
||||
.findOne();
|
||||
|
||||
order.cancel();
|
||||
database.save(order);
|
||||
|
||||
AuditLog auditLog = new AuditLog("order-cancelled", orderId);
|
||||
database.insert(auditLog);
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
```
|
||||
|
||||
If `commit()` is not reached, closing the transaction rolls it back.
|
||||
|
||||
### Useful explicit controls
|
||||
|
||||
- `txn.commit()` - commit current work
|
||||
- `txn.setRollbackOnly()` - force rollback-only behavior
|
||||
- `txn.flush()` - push batched statements to the database now
|
||||
|
||||
### Agent rule
|
||||
|
||||
Prefer `@Transactional` unless explicit transaction control is actually needed.
|
||||
Do not use `beginTransaction()` only because it feels "safer".
|
||||
|
||||
---
|
||||
|
||||
## Step 7 - Use `createTransaction()` only for non-thread-local transaction handling
|
||||
|
||||
`createTransaction()` creates a transaction that is **not** placed into the
|
||||
thread-local scope. This is a specialized tool.
|
||||
|
||||
Use it when:
|
||||
|
||||
- the transaction will be passed explicitly
|
||||
- you need more than one transaction in the same thread
|
||||
- you are coordinating work across threads or lower-level APIs
|
||||
|
||||
### Example - explicit transaction passed to query and save
|
||||
|
||||
```java
|
||||
Database database = DB.getDefault();
|
||||
|
||||
try (Transaction txn = database.createTransaction()) {
|
||||
|
||||
Customer customer = new QCustomer(txn)
|
||||
.email.equalTo(email)
|
||||
.findOne();
|
||||
|
||||
customer.setInactive(true);
|
||||
database.save(customer, txn);
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
If you are not deliberately bypassing thread-local transaction scope, do **not**
|
||||
use `createTransaction()`. Most service code should use `@Transactional` or
|
||||
`beginTransaction()`.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 - Use bulk update/delete or JDBC batch for many-row writes
|
||||
|
||||
Loops of `database.save(...)` are often the wrong tool for large write sets.
|
||||
|
||||
### Prefer bulk update for set-based changes
|
||||
|
||||
If the update can be expressed as "change all rows matching this predicate",
|
||||
perform one bulk update instead of loading and saving each bean.
|
||||
|
||||
### Example - bulk update with query beans
|
||||
|
||||
```java
|
||||
var cust = QCustomer.alias();
|
||||
|
||||
int rows = new QCustomer()
|
||||
.status.equalTo(Customer.Status.NEW)
|
||||
.asUpdate()
|
||||
.set(cust.status, Customer.Status.ACTIVE)
|
||||
.update();
|
||||
```
|
||||
|
||||
### Example - bulk update with `database.update(...)`
|
||||
|
||||
```java
|
||||
int rows = database.update(Customer.class)
|
||||
.set("status", Customer.Status.ACTIVE)
|
||||
.where()
|
||||
.eq("status", Customer.Status.NEW)
|
||||
.update();
|
||||
```
|
||||
|
||||
### Prefer JDBC batch for many individual inserts/updates
|
||||
|
||||
If each row has different values and must still go through per-bean persistence,
|
||||
use batching.
|
||||
|
||||
```java
|
||||
Database database = DB.getDefault();
|
||||
|
||||
try (Transaction txn = database.beginTransaction()) {
|
||||
txn.setBatchMode(true);
|
||||
txn.setBatchSize(100);
|
||||
txn.setGetGeneratedKeys(false);
|
||||
|
||||
for (Customer customer : customersToInsert) {
|
||||
database.insert(customer, txn);
|
||||
}
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
```
|
||||
|
||||
### Alternative - annotation-driven batching
|
||||
|
||||
```java
|
||||
@Transactional(batchSize = 50)
|
||||
public void importCustomers(List<Customer> customers) {
|
||||
for (Customer customer : customers) {
|
||||
database.insert(customer);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Batch caveats
|
||||
|
||||
- Executing a query inside a batched transaction can flush the batch
|
||||
- Mixing bean persistence and `SqlUpdate` can also flush the batch
|
||||
- Accessing generated/unloaded properties on batched beans can flush the batch
|
||||
|
||||
If the workflow depends on delayed flushing, review the batch-flush rules before
|
||||
adding more queries inside the same transaction.
|
||||
|
||||
---
|
||||
|
||||
## Common anti-patterns
|
||||
|
||||
### Anti-pattern 1 - Saving many rows one by one without batch or bulk update
|
||||
|
||||
If you are changing hundreds or thousands of rows, first ask whether it should
|
||||
be a bulk update or a batched transaction.
|
||||
|
||||
### Anti-pattern 2 - Assuming child beans cascade automatically
|
||||
|
||||
Cascade is not automatic. Inspect the mapping first.
|
||||
|
||||
### Anti-pattern 3 - Wrapping external calls inside the database transaction
|
||||
|
||||
Do not keep transactions open while waiting on HTTP calls, queues, or other
|
||||
slow external systems unless the design genuinely requires it.
|
||||
|
||||
### Anti-pattern 4 - Using `createTransaction()` for ordinary service code
|
||||
|
||||
Most service code should not bypass thread-local transaction handling.
|
||||
|
||||
### Anti-pattern 5 - Using `save()` when you really need `insert()` or `update()`
|
||||
|
||||
If operation intent matters, choose the more specific API.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| Child beans were not saved or deleted | Missing cascade mapping | Inspect annotations and add explicit save/delete or the correct cascade |
|
||||
| Earlier writes committed even though later work failed | The whole workflow was not inside one transaction | Wrap the unit of work in `@Transactional` or an explicit transaction |
|
||||
| `OptimisticLockException` on update/delete | Concurrent modification or stale version | Re-fetch, merge, or handle concurrency explicitly |
|
||||
| Batch writes flush earlier than expected | Query, mixed SQL, or property access triggered flush | Review batch flush rules and transaction flow |
|
||||
| Explicit transaction example does not affect the expected database | Mixed default DB and named DB usage | Use the same `Database` instance consistently for query and write |
|
||||
|
||||
---
|
||||
|
||||
## Summary workflow for AI agents
|
||||
|
||||
When asked to add persistence logic:
|
||||
|
||||
1. Choose `insert`, `save`, `update`, `delete`, or bulk update based on intent
|
||||
2. Inspect cascade mappings before assuming related beans will persist/delete
|
||||
3. Use implicit transactions for one isolated write
|
||||
4. Use `@Transactional` for multi-step units of work
|
||||
5. Use `beginTransaction()` only when explicit transaction control is needed
|
||||
6. Use `createTransaction()` only for explicit, non-thread-local handling
|
||||
7. Use bulk update or batching for large write sets
|
||||
|
||||
---
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Entity Bean Creation](entity-bean-creation.md)
|
||||
- [Testing with TestEntityBuilder](testing-with-testentitybuilder.md)
|
||||
- [Ebean persist docs](https://ebean.io/docs/persist)
|
||||
- [Ebean transaction docs](https://ebean.io/docs/transactions)
|
||||
@@ -0,0 +1,817 @@
|
||||
# Guide: Testing with TestEntityBuilder
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains how to use `TestEntityBuilder` to rapidly create test entity instances with auto-populated random values. It is written as practical instructions for developers and AI agents building tests for Ebean applications.
|
||||
|
||||
`TestEntityBuilder` eliminates boilerplate test setup by automatically generating realistic test data for all scalar fields, while respecting entity constraints and relationships. This is particularly valuable for:
|
||||
|
||||
- **Integration tests** that need representative data without caring about specific values
|
||||
- **Persistence layer tests** that verify save/update/delete operations work correctly
|
||||
- **Query and filter tests** where you need multiple entities with varied data
|
||||
- **Rapid test setup** that reduces test code verbosity and improves readability
|
||||
|
||||
---
|
||||
|
||||
## Setup & Dependencies
|
||||
|
||||
### Add ebean-test to Your Project
|
||||
|
||||
The `TestEntityBuilder` class is provided by the `ebean-test` module.
|
||||
|
||||
**Maven:**
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**Gradle:**
|
||||
```gradle
|
||||
testImplementation "io.ebean:ebean-test:${ebeanVersion}"
|
||||
```
|
||||
|
||||
Use a version that matches your Ebean runtime (`ebean.version` /
|
||||
`ebeanVersion`), or replace with an explicit fixed version if your build does
|
||||
not centralize dependency versions.
|
||||
|
||||
> **Minimum version:** `TestEntityBuilder` was introduced in `ebean-test 17.5.0`. If your
|
||||
> existing Ebean version is below this, upgrade before proceeding — mismatched Ebean
|
||||
> runtime and test versions are not supported.
|
||||
|
||||
### Import the Class
|
||||
|
||||
```java
|
||||
import io.ebean.test.TestEntityBuilder;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Create a Builder Instance
|
||||
|
||||
`TestEntityBuilder` uses a builder pattern for configuration:
|
||||
|
||||
```java
|
||||
TestEntityBuilder builder = TestEntityBuilder.builder(database).build();
|
||||
```
|
||||
|
||||
The `Database` parameter specifies which Ebean database instance to use for entity type
|
||||
lookups and persistence operations. Pass the injected `Database` bean (see
|
||||
[Using with Dependency Injection](#using-with-dependency-injection) below) rather than
|
||||
`DB.getDefault()` when working in a Spring or Avaje Inject context. For the same reason,
|
||||
use the injected `database` bean for **all** persistence operations in your tests
|
||||
(`database.save()`, `database.find()`, etc.) rather than mixing in static `DB.*` calls.
|
||||
|
||||
### Build an Entity (In-Memory)
|
||||
|
||||
The `build()` method creates an instance with populated fields **without persisting to the database:**
|
||||
|
||||
```java
|
||||
Product product = builder.build(Product.class);
|
||||
|
||||
// Fields are populated:
|
||||
// - id: unset (typically 0 for primitive long, null for boxed Long)
|
||||
// - name: random UUID-based string
|
||||
// - price: random BigDecimal
|
||||
// - inStock: true
|
||||
// - createdAt: current instant
|
||||
// - etc.
|
||||
|
||||
// Not persisted yet (`@Id` is still unset until the entity is persisted).
|
||||
```
|
||||
|
||||
### Build and Save (Persist to Database)
|
||||
|
||||
The `save()` method creates, persists, and returns an entity with the database-assigned `@Id`:
|
||||
|
||||
```java
|
||||
Product product = builder.save(Product.class);
|
||||
|
||||
// Entity is now in the database:
|
||||
assert database.find(Product.class, product.getId()) != null;
|
||||
```
|
||||
|
||||
### Save Multiple Entities
|
||||
|
||||
The `saveAll()` method persists multiple pre-built entities in a single call:
|
||||
|
||||
```java
|
||||
Product p1 = builder.build(Product.class);
|
||||
Product p2 = builder.build(Product.class);
|
||||
builder.saveAll(p1, p2);
|
||||
|
||||
// Both are now in the database with assigned IDs:
|
||||
assert p1.getId() != null;
|
||||
assert p2.getId() != null;
|
||||
```
|
||||
|
||||
This is equivalent to `database.saveAll(p1, p2)` but avoids needing a separate
|
||||
`Database` reference in tests that already hold a `TestEntityBuilder`.
|
||||
|
||||
### Access the Underlying Database
|
||||
|
||||
The `database()` method returns the `Database` instance used internally by the builder.
|
||||
This is useful in tests where you want a single injected object (`TestEntityBuilder`) but
|
||||
still need to perform `find()`, `delete()`, or other database operations:
|
||||
|
||||
```java
|
||||
Product saved = builder.save(Product.class);
|
||||
|
||||
// Use builder.database() instead of injecting a separate Database bean:
|
||||
Product found = builder.database().find(Product.class, saved.getId());
|
||||
assert found != null;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using with Dependency Injection
|
||||
|
||||
Most applications using Ebean also use a DI framework. The recommended pattern is to
|
||||
register `TestEntityBuilder` as a bean in the test DI context so it can be injected
|
||||
directly into test classes — eliminating `@BeforeEach` setup boilerplate entirely.
|
||||
|
||||
### Avaje Inject — `@TestScope @Factory`
|
||||
|
||||
Add a `@Bean` method to your test-scoped `@Factory` class:
|
||||
|
||||
```java
|
||||
import io.ebean.Database;
|
||||
import io.ebean.test.ContainerDatabase;
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.avaje.inject.test.TestScope;
|
||||
import io.ebean.test.TestEntityBuilder;
|
||||
|
||||
@TestScope
|
||||
@Factory
|
||||
class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("17") // Postgres image version
|
||||
.dbName("my_app") // database to create inside the container
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then inject it directly into test classes using `@InjectTest`:
|
||||
|
||||
```java
|
||||
@InjectTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Inject Database database;
|
||||
@Inject TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void findByStatus() {
|
||||
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
|
||||
database.save(order);
|
||||
|
||||
// ... test assertions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Both patterns produce a single shared `TestEntityBuilder` instance, wired
|
||||
from the managed `Database` bean — no `@BeforeEach` required.
|
||||
|
||||
### Spring Boot — `@TestConfiguration`
|
||||
|
||||
Add a `@TestConfiguration` class that provides `TestEntityBuilder` as a bean:
|
||||
|
||||
```java
|
||||
@TestConfiguration
|
||||
class TestConfig {
|
||||
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("17") // Postgres image version
|
||||
.dbName("my_app") // database to create inside the container
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
// use @Primary if your main application context also wires a Database bean
|
||||
// or conditionally wire the main Database bean to exclude it from tests
|
||||
@Primary
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then inject it directly into test classes:
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Autowired Database database;
|
||||
@Autowired TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void findByStatus() {
|
||||
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
|
||||
database.save(order);
|
||||
|
||||
// ... test assertions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type-Specific Value Generation
|
||||
|
||||
`TestEntityBuilder` generates appropriate random values for each Java/SQL type. Customize this behavior by subclassing `RandomValueGenerator` (see "Custom Value Generators" below).
|
||||
|
||||
| Type | Generated Value | Notes |
|
||||
|------|-----------------|-------|
|
||||
| `String` | UUID-derived (8 chars by default) | Truncated to column length if `@Column(length=...)` is set |
|
||||
| Email fields | `uuid@domain.com` format | Detected when property name contains "email" (case-insensitive) |
|
||||
| `Integer` / `int` | Random in `[1, 1_000)` | |
|
||||
| `Long` / `long` | Random in `[1, 100_000)` | |
|
||||
| `Short` / `short` | Random in `[1, 100)` | See note on flag fields below |
|
||||
| `Double` / `double` | Random in `[1, 100)` | |
|
||||
| `Float` / `float` | Random in `[1, 100)` | |
|
||||
| `BigDecimal` | Respects precision and scale | Precision and scale from `@Column(precision=..., scale=...)` |
|
||||
| `Boolean` / `boolean` | `true` | Override in custom generator if needed |
|
||||
| `UUID` | Random UUID | Via `UUID.randomUUID()` |
|
||||
| `LocalDate` | Today's date | Via `LocalDate.now()` |
|
||||
| `LocalDateTime` | Current datetime | Via `LocalDateTime.now()` |
|
||||
| `Instant` | Current instant | Via `Instant.now()` |
|
||||
| `OffsetDateTime` | Current time with zone | Via `OffsetDateTime.now()` |
|
||||
| `ZonedDateTime` | Current time with zone | Via `ZonedDateTime.now()` |
|
||||
| `Enum` | First constant | Override in custom generator if needed |
|
||||
| Other types | `null` | Set these fields manually in tests |
|
||||
|
||||
### String Length Constraints
|
||||
|
||||
`TestEntityBuilder` respects column length constraints defined in the entity:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class User {
|
||||
@Column(length = 50)
|
||||
private String username;
|
||||
}
|
||||
|
||||
User user = builder.build(User.class);
|
||||
assert user.getUsername().length() <= 50; // ✅ Constraint respected
|
||||
```
|
||||
|
||||
### BigDecimal Precision and Scale
|
||||
|
||||
For `BigDecimal` fields, the builder respects the database column precision and scale:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class LineItem {
|
||||
@Column(precision = 10, scale = 2) // max 99_999_999.99
|
||||
private BigDecimal amount;
|
||||
}
|
||||
|
||||
LineItem item = builder.build(LineItem.class);
|
||||
assert item.getAmount().scale() == 2;
|
||||
```
|
||||
|
||||
### Short Fields Used as Boolean Flags
|
||||
|
||||
Some legacy schemas use `short` to represent boolean-like flags (e.g. `active = 1`
|
||||
means active, `0` means inactive). `TestEntityBuilder` generates a random short in
|
||||
`[1, 100)`, which will be non-zero but not necessarily `1`. If your application
|
||||
code checks `entity.getActive() == 1` specifically, override the field after building:
|
||||
|
||||
```java
|
||||
Organisation org = builder.build(Organisation.class)
|
||||
.setActive((short) 1); // explicit override — random short won't do
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entity Relationships
|
||||
|
||||
### Cascade-Persist Relationships: Recursively Built
|
||||
|
||||
Relationships marked with `cascade = PERSIST` are recursively populated:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
@ManyToOne(cascade = CascadeType.PERSIST)
|
||||
private Customer customer;
|
||||
}
|
||||
|
||||
Order order = builder.build(Order.class);
|
||||
|
||||
// Both order and customer are built:
|
||||
assert order != null;
|
||||
assert order.getCustomer() != null;
|
||||
// Before persist, @Id values are typically unset
|
||||
// (0 for primitive IDs, null for boxed IDs).
|
||||
|
||||
// When saved, cascade handles both:
|
||||
Order saved = builder.save(Order.class);
|
||||
assert saved.getId() != null;
|
||||
assert saved.getCustomer().getId() != null; // parent also saved
|
||||
```
|
||||
|
||||
### Non-Cascade Relationships: Left Null
|
||||
|
||||
Relationships without cascade persist are not auto-created — even if marked `optional = false`.
|
||||
Create and save the related entity first (the builder works well here), then assign it manually
|
||||
before saving the parent:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@ManyToOne
|
||||
private Author author; // No cascade = left null by builder
|
||||
}
|
||||
|
||||
BlogPost post = builder.build(BlogPost.class);
|
||||
assert post.getAuthor() == null;
|
||||
|
||||
// Use the builder to create the related entity, then set it manually:
|
||||
Author author = builder.save(Author.class);
|
||||
post.setAuthor(author);
|
||||
database.save(post);
|
||||
```
|
||||
|
||||
### Collection Relationships: Left Empty
|
||||
|
||||
Collection relationships (`@OneToMany`, `@ManyToMany`) are left empty. On Ebean-enhanced
|
||||
entities these fields are initialised to empty Ebean-managed lists (not `null`), so calling
|
||||
`.add()` or `.addAll()` directly is safe:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Author {
|
||||
@OneToMany(mappedBy = "author")
|
||||
private List<BlogPost> posts; // Left empty
|
||||
}
|
||||
|
||||
Author author = builder.build(Author.class);
|
||||
assert author.getPosts().isEmpty();
|
||||
|
||||
// Populate if needed for testing:
|
||||
author.getPosts().addAll(Arrays.asList(post1, post2, post3));
|
||||
```
|
||||
|
||||
### Cycle Detection: Prevents Infinite Recursion
|
||||
|
||||
If two entities reference each other with cascade persist, the builder detects the cycle and breaks it by leaving one reference null:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Person {
|
||||
@ManyToOne(cascade = CascadeType.PERSIST)
|
||||
private Organization org;
|
||||
}
|
||||
|
||||
@Entity
|
||||
public class Organization {
|
||||
@ManyToOne(cascade = CascadeType.PERSIST)
|
||||
private Person founder;
|
||||
}
|
||||
|
||||
Person person = builder.build(Person.class);
|
||||
// One reference will be null to break the cycle:
|
||||
// either person.org or person.org.founder is null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Value Generators
|
||||
|
||||
### Why Customize?
|
||||
|
||||
The default `RandomValueGenerator` uses generic random values. For domain-specific testing, you may want:
|
||||
|
||||
- Email addresses with your company domain
|
||||
- Realistic phone numbers
|
||||
- Product SKUs following a pattern
|
||||
- Addresses in specific regions
|
||||
- Monetary amounts within realistic ranges
|
||||
|
||||
### Creating a Custom Generator
|
||||
|
||||
Subclass `RandomValueGenerator` and override individual `random*()` methods:
|
||||
|
||||
```java
|
||||
class CompanyTestDataGenerator extends RandomValueGenerator {
|
||||
|
||||
@Override
|
||||
protected String randomString(String propName, int maxLength) {
|
||||
if (propName != null && propName.toLowerCase().contains("email")) {
|
||||
// Use company domain instead of generic @domain.com
|
||||
String localPart = UUID.randomUUID().toString().substring(0, 8);
|
||||
String email = localPart + "@mycompany.com";
|
||||
if (maxLength > 0 && email.length() > maxLength) {
|
||||
return email.substring(0, maxLength);
|
||||
}
|
||||
return email;
|
||||
}
|
||||
return super.randomString(propName, maxLength);
|
||||
}
|
||||
|
||||
// Override other methods as needed:
|
||||
@Override
|
||||
protected Object randomEnum(Class<?> type) {
|
||||
if (type == OrderStatus.class) {
|
||||
// Bias towards common statuses for realistic test data
|
||||
return ThreadLocalRandom.current().nextDouble() < 0.8
|
||||
? OrderStatus.PENDING
|
||||
: OrderStatus.COMPLETED;
|
||||
}
|
||||
return super.randomEnum(type);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using a Custom Generator
|
||||
|
||||
Pass the custom generator when building:
|
||||
|
||||
```java
|
||||
TestEntityBuilder builder = TestEntityBuilder.builder(database)
|
||||
.valueGenerator(new CompanyTestDataGenerator())
|
||||
.build();
|
||||
|
||||
User user = builder.build(User.class);
|
||||
assert user.getEmail().endsWith("@mycompany.com");
|
||||
```
|
||||
|
||||
In a DI context, register this as the bean:
|
||||
|
||||
```java
|
||||
// Spring Boot
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database)
|
||||
.valueGenerator(new CompanyTestDataGenerator())
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Money Type
|
||||
|
||||
```java
|
||||
public class MoneyValueGenerator extends RandomValueGenerator {
|
||||
|
||||
@Override
|
||||
protected BigDecimal randomBigDecimal(int precision, int scale) {
|
||||
// Generate prices in a realistic range: $5.00 to $999.99
|
||||
BigDecimal price = BigDecimal.valueOf(
|
||||
ThreadLocalRandom.current().nextDouble(5.0, 1000.0)
|
||||
);
|
||||
return price.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use for Integration Tests, Not Unit Tests
|
||||
|
||||
✅ **Good:** Integration test with database
|
||||
```java
|
||||
@Test
|
||||
void whenSaving_thenCanRetrieve() {
|
||||
Product product = builder.save(Product.class);
|
||||
Product found = database.find(Product.class, product.getId());
|
||||
assertThat(found).isNotNull();
|
||||
}
|
||||
```
|
||||
|
||||
❌ **Poor:** Validation test requiring specific values
|
||||
```java
|
||||
@Test
|
||||
void whenNameIsBlank_thenThrowException() {
|
||||
Product product = builder.build(Product.class); // name is random!
|
||||
product.setName(""); // have to override anyway
|
||||
// ... test proceeds
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Override Values for Specific Test Scenarios
|
||||
|
||||
When test requirements demand specific field values, manually override after building:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void whenStockIsLow_thenShowWarning() {
|
||||
Product product = builder.build(Product.class);
|
||||
product.setQuantity(2); // Specific value for this test
|
||||
|
||||
boolean shouldWarn = product.shouldShowLowStockWarning();
|
||||
assertThat(shouldWarn).isTrue();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Fixture Factories for Common Patterns
|
||||
|
||||
For shared domain-specific setup, encapsulate build patterns in an instance helper class
|
||||
rather than a static factory. In a DI context, this class can be registered as a bean
|
||||
alongside `TestEntityBuilder`:
|
||||
|
||||
```java
|
||||
// Spring Boot
|
||||
@TestConfiguration
|
||||
class TestConfig {
|
||||
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OrderTestFactory orderTestFactory(TestEntityBuilder builder, Database database) {
|
||||
return new OrderTestFactory(builder, database);
|
||||
}
|
||||
}
|
||||
|
||||
public class OrderTestFactory {
|
||||
|
||||
private final TestEntityBuilder builder;
|
||||
private final Database database;
|
||||
|
||||
public OrderTestFactory(TestEntityBuilder builder, Database database) {
|
||||
this.builder = builder;
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public Order savePendingOrder() {
|
||||
Order order = builder.build(Order.class);
|
||||
order.setStatus(OrderStatus.PENDING);
|
||||
database.save(order);
|
||||
return order;
|
||||
}
|
||||
|
||||
public Order saveShippedOrder() {
|
||||
Order order = builder.build(Order.class);
|
||||
order.setStatus(OrderStatus.SHIPPED);
|
||||
order.setShippedAt(Instant.now());
|
||||
database.save(order);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in tests:
|
||||
@SpringBootTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Autowired OrderTestFactory orderFactory;
|
||||
|
||||
@Test
|
||||
void whenOrderPending_thenCanUpdate() {
|
||||
Order order = orderFactory.savePendingOrder();
|
||||
// ... test logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Build Multiple Distinct Instances
|
||||
|
||||
Each call to `build()` or `save()` produces a new instance with fresh random values:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void whenFetchingMultipleOrders_thenAllUnique() {
|
||||
Order order1 = builder.save(Order.class);
|
||||
Order order2 = builder.save(Order.class);
|
||||
Order order3 = builder.save(Order.class);
|
||||
|
||||
assertThat(order1.getId()).isNotEqualTo(order2.getId());
|
||||
assertThat(order2.getId()).isNotEqualTo(order3.getId());
|
||||
assertThat(order1.getOrderNumber()).isNotEqualTo(order2.getOrderNumber());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Example 1: Integration Test with Spring Boot
|
||||
|
||||
Register `TestEntityBuilder` as a `@TestConfiguration` bean, then inject it alongside
|
||||
the repository under test:
|
||||
|
||||
```java
|
||||
@TestConfiguration
|
||||
class TestConfig {
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
|
||||
@SpringBootTest
|
||||
class OrderRepositoryTest {
|
||||
|
||||
@Autowired OrderRepository orderRepository;
|
||||
@Autowired TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void whenFindingOrdersByStatus_thenReturnsMatching() {
|
||||
Order pending1 = builder.build(Order.class);
|
||||
pending1.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order pending2 = builder.build(Order.class);
|
||||
pending2.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order shipped = builder.build(Order.class);
|
||||
shipped.setStatus(OrderStatus.SHIPPED);
|
||||
|
||||
builder.saveAll(pending1, pending2, shipped);
|
||||
|
||||
List<Order> pending = orderRepository.findByStatus(OrderStatus.PENDING);
|
||||
assertThat(pending).hasSize(2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Integration Test with Avaje Inject
|
||||
|
||||
```java
|
||||
@TestScope
|
||||
@Factory
|
||||
class TestConfiguration {
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
|
||||
@InjectTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Inject TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void whenFindingOrdersByStatus_thenReturnsMatching() {
|
||||
Order pending1 = builder.build(Order.class);
|
||||
pending1.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order pending2 = builder.build(Order.class);
|
||||
pending2.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order shipped = builder.build(Order.class);
|
||||
shipped.setStatus(OrderStatus.SHIPPED);
|
||||
|
||||
builder.saveAll(pending1, pending2, shipped);
|
||||
|
||||
// ... test assertions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Recursive Relationship Building
|
||||
|
||||
```java
|
||||
@Test
|
||||
void whenBuildingOrderWithCustomer_thenBothPopulated() {
|
||||
Order order = builder.build(Order.class);
|
||||
|
||||
// Customer is recursively built because of @ManyToOne(cascade=PERSIST)
|
||||
assertThat(order.getCustomer()).isNotNull();
|
||||
// Before persist, @Id values are typically unset
|
||||
// (0 for primitive IDs, null for boxed IDs).
|
||||
assertThat(order.getCustomer().getName()).isNotNull();
|
||||
|
||||
// Saving cascades to customer:
|
||||
Order saved = builder.save(Order.class);
|
||||
assertThat(saved.getId()).isNotNull();
|
||||
assertThat(saved.getCustomer().getId()).isNotNull();
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Custom Generator for Domain Values
|
||||
|
||||
```java
|
||||
// Custom generator for your domain
|
||||
class ECommerceTestDataGenerator extends RandomValueGenerator {
|
||||
@Override
|
||||
protected BigDecimal randomBigDecimal(int precision, int scale) {
|
||||
// Product prices typically range $10-$500
|
||||
return BigDecimal.valueOf(
|
||||
ThreadLocalRandom.current().nextDouble(10.0, 500.0)
|
||||
).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void usingCustomGenerator() {
|
||||
TestEntityBuilder builder = TestEntityBuilder.builder(database)
|
||||
.valueGenerator(new ECommerceTestDataGenerator())
|
||||
.build();
|
||||
|
||||
Product product = builder.build(Product.class);
|
||||
assertThat(product.getPrice())
|
||||
.isBetween(BigDecimal.TEN, BigDecimal.valueOf(500.0));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No BeanDescriptor found for [Class] — is it an @Entity?"
|
||||
|
||||
**Cause:** The class you're trying to build is not registered as an Ebean entity.
|
||||
|
||||
**Solution:** Ensure the class is annotated with `@Entity` and registered with the Database:
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "products")
|
||||
public class Product {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Fields are unset even though I expected them to be populated
|
||||
|
||||
**Cause:** `TestEntityBuilder` does **not** populate:
|
||||
- `@Id` fields (identity/primary key; left unset until persist)
|
||||
- `@Version` fields (optimistic locking; left unset until persist)
|
||||
- `@Transient` fields
|
||||
- `@OneToMany` collections
|
||||
- Non-cascade `@ManyToOne` relationships
|
||||
|
||||
**Solution:** Set only the fields your test scenario cares about, then persist.
|
||||
`@Id` and `@Version` are usually database-managed and should typically be left
|
||||
unset before save:
|
||||
```java
|
||||
Product product = builder.build(Product.class);
|
||||
product.setName("specific-name"); // test-specific override
|
||||
database.save(product); // database assigns @Id/@Version
|
||||
```
|
||||
|
||||
### Building recursive relationships causes StackOverflowError
|
||||
|
||||
**Cause:** Two or more entities mutually reference each other without cycle detection.
|
||||
|
||||
**Solution:** This should be handled automatically by cycle detection. If not, manually set one reference to null:
|
||||
```java
|
||||
Person person = builder.build(Person.class);
|
||||
person.getOrganization().setFounder(null); // Break cycle
|
||||
```
|
||||
|
||||
### Values generated are "too random" for my test
|
||||
|
||||
**Cause:** Default `RandomValueGenerator` uses true random values, which aren't suitable when your test needs predictable data.
|
||||
|
||||
**Solution:** Create a custom generator that produces deterministic values:
|
||||
```java
|
||||
class DeterministicTestDataGenerator extends RandomValueGenerator {
|
||||
private int counter = 0;
|
||||
|
||||
@Override
|
||||
protected String randomString(String propName, int maxLength) {
|
||||
return "test_" + (counter++);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`TestEntityBuilder` accelerates test development by:
|
||||
|
||||
1. **Reducing boilerplate** — No need to manually set every field
|
||||
2. **Improving readability** — Tests focus on what matters, not setup
|
||||
3. **Enabling variety** — Each build produces distinct random values
|
||||
4. **Respecting constraints** — Column lengths and decimal scales are enforced
|
||||
5. **Supporting customization** — Extend `RandomValueGenerator` for domain needs
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
# Guide: Write Ebean Queries with Query Beans
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide gives step-by-step instructions for AI agents and developers to write
|
||||
application queries using Ebean query beans.
|
||||
|
||||
Use this guide when the project already has Ebean configured and you need to:
|
||||
|
||||
- add a repository/service query
|
||||
- replace string-based ORM queries with type-safe query beans
|
||||
- tune what data is fetched to avoid over-fetching or N+1 issues
|
||||
- return DTO projections for list screens or API responses
|
||||
|
||||
The default recommendation is:
|
||||
|
||||
1. Prefer query beans first
|
||||
2. Prefer entity queries for domain logic
|
||||
3. For read-only entity graphs, prefer `setUnmodifiable(true)`
|
||||
4. Prefer DTO projection for summary/read-model use cases
|
||||
5. Only drop to raw SQL when the ORM query cannot express the requirement cleanly
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The project already uses Ebean ORM
|
||||
- Query bean generation is configured (for Maven this usually means
|
||||
`querybean-generator` is registered as an annotation processor)
|
||||
- Entity beans already exist
|
||||
- A compile/build has run successfully since the last entity model change
|
||||
|
||||
If query beans are not yet configured, first follow:
|
||||
[`add-ebean-postgres-maven-pom.md`](add-ebean-postgres-maven-pom.md)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Verify the generated `Q*` query bean exists
|
||||
|
||||
For each entity bean, Ebean generates a query bean with the same name prefixed
|
||||
with `Q`.
|
||||
|
||||
Examples:
|
||||
|
||||
- `Customer` -> `QCustomer`
|
||||
- `Order` -> `QOrder`
|
||||
- `Contact` -> `QContact`
|
||||
|
||||
Import the generated type from the query bean package:
|
||||
|
||||
```java
|
||||
import org.example.domain.query.QCustomer;
|
||||
```
|
||||
|
||||
If the `Q*` type does not exist or the IDE cannot resolve it:
|
||||
|
||||
1. Confirm the entity compiled successfully
|
||||
2. Run a normal project compile/build
|
||||
3. If the entity was renamed or moved, run a full rebuild rather than relying on
|
||||
incremental compilation
|
||||
|
||||
### Important caveat - entity rename
|
||||
|
||||
After refactoring an entity name, old generated query beans can remain on disk
|
||||
until the next full build. If both old and new `Q*` types appear to exist, do a
|
||||
clean rebuild before editing application queries.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Choose the terminal query method before writing predicates
|
||||
|
||||
Decide what the caller actually needs. This determines the terminal method and
|
||||
often the right query shape.
|
||||
|
||||
| Need | Preferred method | Notes |
|
||||
|------|------------------|-------|
|
||||
| Check if at least one row exists | `exists()` | Cheapest choice for boolean existence checks |
|
||||
| Load exactly one row by ID or unique key | `findOne()` | Only use when the predicate is truly unique |
|
||||
| Load a list of entity beans | `findList()` | Default for list screens and domain logic |
|
||||
| Count matching rows | `findCount()` | Prefer over loading entities just to count |
|
||||
| Load a page plus optional total row count | `findPagedList()` | Use when the caller needs pagination metadata |
|
||||
| Return DTO/read-model rows | `asDto(...).findList()` | Prefer this over partially loaded entities for API/view models |
|
||||
|
||||
### Example - existence check
|
||||
|
||||
```java
|
||||
boolean alreadyUsed = new QCustomer()
|
||||
.email.equalTo(email)
|
||||
.exists();
|
||||
```
|
||||
|
||||
### Example - unique lookup
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.email.equalTo(email)
|
||||
.findOne();
|
||||
```
|
||||
|
||||
Do **not** use `findOne()` for predicates that can match multiple rows.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Build predicates by traversing properties and associations
|
||||
|
||||
With query beans, write predicates directly against properties. When you
|
||||
traverse an association, Ebean adds the necessary joins automatically.
|
||||
|
||||
### Example - root property predicates
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.name.istartsWith("rob")
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - association traversal
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.billingAddress.city.equalTo("Auckland")
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - collection predicate
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.contacts.isEmpty()
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
When adding a new query:
|
||||
|
||||
1. Start from the root entity that the caller wants back
|
||||
2. Add predicates with query bean properties
|
||||
3. Traverse relationships instead of writing manual join SQL
|
||||
4. Keep property references type-safe; avoid string property names unless the API
|
||||
specifically requires them
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - Add ordering, limits, and pagination deliberately
|
||||
|
||||
Do not leave list queries unordered unless the call site truly does not care.
|
||||
For UI lists, APIs, and background jobs, explicit ordering is usually better.
|
||||
|
||||
### Example - ordered list with limit
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().name.asc()
|
||||
.setMaxRows(50)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - offset/limit pagination
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().id.asc()
|
||||
.setFirstRow(offset)
|
||||
.setMaxRows(pageSize)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - paged list with total count
|
||||
|
||||
```java
|
||||
PagedList<Customer> page = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().id.asc()
|
||||
.setFirstRow(offset)
|
||||
.setMaxRows(pageSize)
|
||||
.findPagedList();
|
||||
|
||||
page.loadRowCount();
|
||||
List<Customer> customers = page.getList();
|
||||
int totalRowCount = page.getTotalRowCount();
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
- Use `findList()` when the caller only needs rows
|
||||
- Use `findPagedList()` when the caller also needs page metadata or total counts
|
||||
- Pair pagination with a stable `orderBy()` so page boundaries stay predictable
|
||||
|
||||
---
|
||||
|
||||
## Step 5 - Control fetched data with `select()` and `fetch()`
|
||||
|
||||
By default, entity queries can load more of the object graph than the caller
|
||||
actually needs. Use `select()` and `fetch()` to control the root and association
|
||||
properties that are loaded.
|
||||
|
||||
### Root properties with `select()`
|
||||
|
||||
Use `select()` to define which properties should be fetched on the root entity.
|
||||
|
||||
### Associated bean properties with `fetch()`
|
||||
|
||||
Use `fetch()` to define what should be fetched on associated paths.
|
||||
|
||||
### Example - partial entity query
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
private static final QContact CONT = QContact.alias();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.select(CUST.name, CUST.status, CUST.whenCreated)
|
||||
.contacts.fetch(CONT.email)
|
||||
.name.istartsWith("rob")
|
||||
.findList();
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
- `select(...)` tunes the root `Customer` properties
|
||||
- `contacts.fetch(...)` tunes the associated `Contact` properties
|
||||
- the query still returns `Customer` entity beans
|
||||
|
||||
### Agent rules for partial entity queries
|
||||
|
||||
1. Only use `select()`/`fetch()` when you know what the caller will read next
|
||||
2. Do not treat partially loaded entities like fully populated API DTOs
|
||||
3. If the caller only needs summary fields, prefer a DTO projection instead
|
||||
|
||||
---
|
||||
|
||||
## Step 6 - Use `setUnmodifiable(true)` for read-only entity graphs
|
||||
|
||||
`setUnmodifiable(true)` turns the returned object graph into an unmodifiable,
|
||||
read-only graph.
|
||||
|
||||
This means:
|
||||
|
||||
- setters cannot mutate returned beans
|
||||
- associated collections are unmodifiable
|
||||
- lazy loading is disabled
|
||||
- accessing an unloaded property throws `LazyInitialisationException`
|
||||
- the query uses `PersistenceContextScope.QUERY`
|
||||
|
||||
### Example - read-only entity graph
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
private static final QContact CONT = QContact.alias();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.select(CUST.name, CUST.status, CUST.whenCreated)
|
||||
.contacts.fetch(CONT.email)
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.setUnmodifiable(true)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### When to prefer `setUnmodifiable(true)`
|
||||
|
||||
Use it when the result is meant to be read-only, such as:
|
||||
|
||||
- service/query methods returning entity graphs for display or serialization
|
||||
- query results you want the application to treat as immutable
|
||||
- cached query results or other shared read models backed by entity graphs
|
||||
- partial entity graphs where you want accidental lazy loading to fail fast
|
||||
|
||||
### When **not** to use it
|
||||
|
||||
Do **not** use `setUnmodifiable(true)` when the caller will:
|
||||
|
||||
- modify the beans and save them later
|
||||
- rely on lazy loading of associations or unloaded scalar properties
|
||||
- treat the result as a working persistence model rather than a read-only view
|
||||
|
||||
### Agent rule
|
||||
|
||||
If you are returning entity beans for read-only use, `setUnmodifiable(true)`
|
||||
should be the default recommendation. If the caller needs a mutable model or a
|
||||
serialized summary shape, choose mutable entities or DTO projection instead.
|
||||
|
||||
If you need cached assoc-one references for unmodifiable graphs, see
|
||||
[Immutable bean cache for read-only references](immutable-bean-cache.md).
|
||||
|
||||
---
|
||||
|
||||
## Step 7 - Use `fetchQuery()` for to-many paths and `FetchGroup` for reusable query shapes
|
||||
|
||||
Ebean applies important SQL rules when translating ORM queries:
|
||||
|
||||
1. It does not generate SQL cartesian products
|
||||
2. It honors `maxRows` in SQL
|
||||
|
||||
This means to-many paths often need special handling.
|
||||
|
||||
### Use `fetchQuery()` when:
|
||||
|
||||
- the query includes a `OneToMany` or `ManyToMany` path
|
||||
- the query includes `setMaxRows(...)`
|
||||
- the query loads multiple to-many paths
|
||||
- you want the query shape to make the secondary-query behavior explicit
|
||||
|
||||
### Example - explicit secondary queries for to-many paths
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
|
||||
List<Order> orders = new QOrder()
|
||||
.customer.fetch(CUST.name)
|
||||
.lines.fetchQuery()
|
||||
.shipments.fetchQuery()
|
||||
.status.equalTo(Order.Status.NEW)
|
||||
.setMaxRows(100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Use `FetchGroup` when:
|
||||
|
||||
- the same fetch shape is reused in multiple places
|
||||
- you want to separate predicate logic from fetch-shape tuning
|
||||
- you want an immutable, static query-shape definition
|
||||
|
||||
### Example - reusable fetch group
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
|
||||
private static final FetchGroup<Customer> CUSTOMER_SUMMARY =
|
||||
QCustomer.forFetchGroup()
|
||||
.select(CUST.name, CUST.status, CUST.whenCreated)
|
||||
.billingAddress.fetch()
|
||||
.buildFetchGroup();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.select(CUSTOMER_SUMMARY)
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
If the caller needs multiple to-many paths or a paged query, be suspicious of a
|
||||
plain `fetch(...)` on those paths. `fetchQuery()` is often the safer default.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 - Use DTO projection when the caller does not need entity beans
|
||||
|
||||
For list screens, API summaries, exports, or read-model views, the caller often
|
||||
does **not** need managed entity beans. In those cases, project directly to a
|
||||
DTO using `asDto(...)`.
|
||||
|
||||
### Example - DTO projection with query beans
|
||||
|
||||
```java
|
||||
import static org.example.domain.query.QCustomer.Alias.id;
|
||||
import static org.example.domain.query.QCustomer.Alias.name;
|
||||
|
||||
public record CustomerSummary(long id, String name) {}
|
||||
|
||||
List<CustomerSummary> summaries = new QCustomer()
|
||||
.select(id, name)
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().name.asc()
|
||||
.asDto(CustomerSummary.class)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Prefer DTO projection when:
|
||||
|
||||
- the caller will serialize the result directly
|
||||
- only a subset of fields is needed
|
||||
- the result is not going to be updated and saved back as an entity
|
||||
- the query contains formulas or aggregation intended for a read model
|
||||
|
||||
---
|
||||
|
||||
## Step 9 - Only fall back to raw SQL when the ORM query is not a good fit
|
||||
|
||||
Prefer the following order:
|
||||
|
||||
1. Query bean query
|
||||
2. Query bean query + `asDto(...)`
|
||||
3. `database.findDto(...)` or DTO query
|
||||
4. Native SQL / `SqlQuery` / `RawSql`
|
||||
|
||||
### Typical reasons to use raw SQL
|
||||
|
||||
- vendor-specific SQL that query beans do not express well
|
||||
- advanced aggregation or database functions
|
||||
- hand-tuned reporting queries
|
||||
- stored procedures or raw JDBC workflows
|
||||
|
||||
Do **not** jump to raw SQL just because the query joins multiple tables. Query
|
||||
beans already handle ordinary relationship traversal well.
|
||||
|
||||
---
|
||||
|
||||
## Common anti-patterns
|
||||
|
||||
### Anti-pattern 1 - Using raw SQL first
|
||||
|
||||
**Avoid:**
|
||||
|
||||
```java
|
||||
List<Customer> customers = database.findNative(Customer.class,
|
||||
"select c.* from customer c join address a on a.id = c.billing_address_id where a.city = ?")
|
||||
.setParameter(1, city)
|
||||
.findList();
|
||||
```
|
||||
|
||||
**Prefer:**
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.billingAddress.city.equalTo(city)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Anti-pattern 2 - Using `findOne()` on a non-unique predicate
|
||||
|
||||
**Avoid:**
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.findOne();
|
||||
```
|
||||
|
||||
**Why:** Many rows can match; this is not a unique lookup.
|
||||
|
||||
### Anti-pattern 3 - Returning partially loaded entities as API models
|
||||
|
||||
If the caller only needs summary fields, return a DTO instead of partially
|
||||
loaded entities that might later trigger more loading or confuse serializers.
|
||||
|
||||
### Anti-pattern 4 - Returning mutable entity graphs for read-only use
|
||||
|
||||
If the caller is only meant to read the result, prefer `setUnmodifiable(true)`
|
||||
so accidental setter calls, collection mutation, and lazy loading fail fast.
|
||||
|
||||
### Anti-pattern 5 - Fetching every relationship "just in case"
|
||||
|
||||
Do not eagerly fetch large object graphs unless the immediate caller will use
|
||||
them. Query tuning is part of the job.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| `Cannot resolve symbol QCustomer` | Query bean generation not configured or build not run | Check the annotation processor and run a build |
|
||||
| Old `Q*` class still appears after entity rename | Stale generated source/class output | Run a clean rebuild |
|
||||
| `findOne()` fails because multiple rows match | Predicate is not unique | Use `findList()` or tighten the predicate |
|
||||
| Returned entities only have some fields loaded | `select()` or `FetchGroup` limited the query shape | Add the required fields or switch to DTO projection |
|
||||
| Setter calls or collection mutation fail on query results | `setUnmodifiable(true)` returned a read-only graph | Remove `setUnmodifiable(true)` or treat the result as read-only |
|
||||
| Accessing an unloaded property throws `LazyInitialisationException` | `setUnmodifiable(true)` disables lazy loading | Fetch the property up front or use DTO projection |
|
||||
| Ebean executes secondary queries for a to-many path | ORM rules avoided cartesian product or honored `maxRows` | This is expected; use `fetchQuery()` explicitly when appropriate |
|
||||
|
||||
---
|
||||
|
||||
## Summary workflow for AI agents
|
||||
|
||||
When asked to add or modify an Ebean query:
|
||||
|
||||
1. Verify the relevant `Q*` type exists
|
||||
2. Choose the terminal method first (`exists`, `findOne`, `findList`, `findPagedList`, `asDto`)
|
||||
3. Add predicates with query bean properties and association traversal
|
||||
4. Add explicit ordering and pagination if relevant
|
||||
5. If the result is read-only entity data, consider `setUnmodifiable(true)`
|
||||
6. Tune the fetch shape with `select()` / `fetch()` / `fetchQuery()` / `FetchGroup`
|
||||
7. Prefer DTO projection for read models and serialized responses
|
||||
8. Only use raw SQL if the ORM query is genuinely the wrong tool
|
||||
|
||||
---
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [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)
|
||||
- [Ebean query docs](https://ebean.io/docs/query/)
|
||||
+5
-26
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>14.0.2</version>
|
||||
<version>16.6.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean api</name>
|
||||
@@ -26,20 +26,16 @@
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
Class retention Nonnull and Nullable annotations
|
||||
to assist with IDE auto-completion with Ebean API
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-lang</artifactId>
|
||||
<version>1.1</version>
|
||||
<groupId>org.jspecify</groupId>
|
||||
<artifactId>jspecify</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-config</artifactId>
|
||||
<version>3.12</version>
|
||||
<version>4.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -90,23 +86,6 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- JAVAX-DEPENDENCY-START ___
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
____ JAVAX-DEPENDENCY-END -->
|
||||
<!-- JAKARTA-DEPENDENCY-START -->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>6.0.0</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- JAKARTA-DEPENDENCY-END -->
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -20,7 +19,7 @@ import java.util.concurrent.TimeUnit;
|
||||
* This also propagates MDC context from the current thread to the
|
||||
* background task if defined.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public interface BackgroundExecutor {
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.ebean;
|
||||
|
||||
/**
|
||||
* Unsupported access of a property on an entity bean.
|
||||
* <p>
|
||||
* Attempted a lazy load operation on a bean that has disabled lazy loading
|
||||
* or attempt to mutate an unmodifiable bean.
|
||||
*/
|
||||
public class BeanAccessException extends UnsupportedOperationException {
|
||||
private static final long serialVersionUID = 1;
|
||||
|
||||
/**
|
||||
* Create with no message.
|
||||
*/
|
||||
public BeanAccessException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create with message.
|
||||
*/
|
||||
public BeanAccessException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.Optional;
|
||||
*
|
||||
* @see BeanRepository
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public abstract class BeanFinder<I,T> {
|
||||
|
||||
protected final Database database;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import io.ebean.bean.EntityBean;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -36,7 +36,7 @@ import java.util.Collection;
|
||||
* @param <I> The ID type
|
||||
* @param <T> The Bean type
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public abstract class BeanRepository<I, T> extends BeanFinder<I, T> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -89,12 +89,7 @@ public interface BeanState {
|
||||
* <p>
|
||||
* If a setter is called on a readOnly bean it will throw an exception.
|
||||
*/
|
||||
boolean isReadOnly();
|
||||
|
||||
/**
|
||||
* Set the readOnly status for the bean.
|
||||
*/
|
||||
void setReadOnly(boolean readOnly);
|
||||
boolean isUnmodifiable();
|
||||
|
||||
/**
|
||||
* Advanced - Used to programmatically build a partially or fully loaded
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.ebean.annotation.TxIsolation;
|
||||
import io.ebean.cache.ServerCacheManager;
|
||||
import io.ebean.plugin.Property;
|
||||
@@ -57,7 +57,7 @@ import java.util.concurrent.Callable;
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public final class DB {
|
||||
|
||||
private static final DbContext context = DbContext.getInstance();
|
||||
@@ -605,7 +605,7 @@ public final class DB {
|
||||
* // find orders and their customers
|
||||
* List<Order> list = DB.find(Order.class)
|
||||
* .fetch("customer")
|
||||
* .order("id")
|
||||
* .orderBy("id")
|
||||
* .findList();
|
||||
*
|
||||
* // sort by customer name ascending, then by order shipDate
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.ebean;
|
||||
|
||||
final class DPaging implements Paging {
|
||||
|
||||
static final Paging NONE = new DPaging(0, 0, null);
|
||||
|
||||
static Paging build(int pgIndex, int pgSize, OrderBy<?> orderBy) {
|
||||
return new DPaging(pgIndex, pgSize, orderBy);
|
||||
}
|
||||
|
||||
static Paging build(int pgIndex, int pgSize) {
|
||||
return new DPaging(pgIndex, pgSize, null);
|
||||
}
|
||||
|
||||
private final int pageNumber;
|
||||
private final int pageSize;
|
||||
private final OrderBy<?> orderBy;
|
||||
|
||||
DPaging(int pageNumber, int pageSize, OrderBy<?> orderBy) {
|
||||
this.pageNumber = pageNumber;
|
||||
this.pageSize = pageSize;
|
||||
this.orderBy = orderBy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int pageIndex() {
|
||||
return pageNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int pageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderBy<?> orderBy() {
|
||||
return orderBy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Paging withPage(int pageNumber) {
|
||||
return new DPaging(pageNumber, pageSize, orderBy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Paging withOrderBy(String orderByClause) {
|
||||
return new DPaging(pageNumber, pageSize, OrderBy.of(orderByClause));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.ebean.annotation.Platform;
|
||||
import io.ebean.annotation.TxIsolation;
|
||||
import io.ebean.cache.ServerCacheManager;
|
||||
@@ -83,7 +83,7 @@ import java.util.concurrent.Callable;
|
||||
* @see DatabaseFactory
|
||||
* @see DatabaseConfig
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public interface Database {
|
||||
|
||||
/**
|
||||
@@ -137,6 +137,7 @@ public interface Database {
|
||||
/**
|
||||
* Return the associated read only DataSource for this Database instance (can be null).
|
||||
*/
|
||||
@Nullable
|
||||
DataSource readOnlyDataSource();
|
||||
|
||||
/**
|
||||
@@ -400,7 +401,7 @@ public interface Database {
|
||||
* // find orders and their customers
|
||||
* List<Order> list = database.find(Order.class)
|
||||
* .fetch("customer")
|
||||
* .order("id")
|
||||
* .orderBy("id")
|
||||
* .findList();
|
||||
*
|
||||
* // sort by customer name ascending, then by order shipDate
|
||||
@@ -772,18 +773,6 @@ public interface Database {
|
||||
*/
|
||||
<T> T reference(Class<T> beanType, Object id);
|
||||
|
||||
/**
|
||||
* Return the extended API for Database.
|
||||
* <p>
|
||||
* The extended API has the options for executing queries that take an explicit
|
||||
* transaction as an argument.
|
||||
* <p>
|
||||
* Typically, we only need to use the extended API when we do NOT want to use the
|
||||
* usual ThreadLocal based mechanism to obtain the current transaction but instead
|
||||
* supply the transaction explicitly.
|
||||
*/
|
||||
ExtendedServer extended();
|
||||
|
||||
/**
|
||||
* Either Insert or Update the bean depending on its state.
|
||||
* <p>
|
||||
|
||||
@@ -989,6 +989,14 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setNamingConvention(NamingConvention namingConvention);
|
||||
|
||||
/**
|
||||
* Set the AggregateFormulaContext which is used to determine if a database function
|
||||
* is an aggregate function (like sum, min, max, avg etc).
|
||||
* <p>
|
||||
* Use this to override the default known aggregation functions.
|
||||
*/
|
||||
DatabaseConfig aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
|
||||
|
||||
/**
|
||||
* Set to true if all DB column and table names should use quoted identifiers.
|
||||
* <p>
|
||||
@@ -1062,6 +1070,11 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
DatabaseBuilder readOnlyDatabase(boolean readOnlyDatabase);
|
||||
|
||||
/**
|
||||
* Set to false such that the instance does not register a JVM shutdown hook.
|
||||
*/
|
||||
DatabaseBuilder shutdownHook(boolean shutdownHook);
|
||||
|
||||
/**
|
||||
* Set a DataSource.
|
||||
*/
|
||||
@@ -1968,8 +1981,8 @@ public interface DatabaseBuilder {
|
||||
DatabaseBuilder setLocalOnlyL2Cache(boolean localOnlyL2Cache);
|
||||
|
||||
/**
|
||||
* Controls if Ebean should ignore <code>&x64;javax.validation.contstraints.NotNull</code> or
|
||||
* <code>&x64;jakarta.validation.contstraints.NotNull</code>
|
||||
* Controls if Ebean should ignore <code>@javax.validation.contstraints.NotNull</code> or
|
||||
* <code>@jakarta.validation.contstraints.NotNull</code>
|
||||
* with respect to generating a <code>NOT NULL</code> column.
|
||||
* <p>
|
||||
* Normally when Ebean sees javax NotNull annotation it means that column is defined as NOT NULL.
|
||||
@@ -2019,6 +2032,11 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setQueryPlanTTLSeconds(int queryPlanTTLSeconds);
|
||||
|
||||
/**
|
||||
* Set the EXPLAIN (with options) to use for query plan capture.
|
||||
*/
|
||||
DatabaseBuilder queryPlanExplain(String queryPlanExplain);
|
||||
|
||||
/**
|
||||
* Create a new PlatformConfig based of the one held but with overridden properties by reading
|
||||
* properties with the given path and prefix.
|
||||
@@ -2600,6 +2618,11 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
NamingConvention getNamingConvention();
|
||||
|
||||
/**
|
||||
* Return the AggregateFormulaContext.
|
||||
*/
|
||||
AggregateFormulaContext aggregateFormulaContext();
|
||||
|
||||
/**
|
||||
* Return true if all DB column and table names should use quoted identifiers.
|
||||
*/
|
||||
@@ -2638,6 +2661,11 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
boolean readOnlyDatabase();
|
||||
|
||||
/**
|
||||
* Return if a JVM shutdown hook should be registered.
|
||||
*/
|
||||
boolean shutdownHook();
|
||||
|
||||
/**
|
||||
* Return the DataSource.
|
||||
*/
|
||||
@@ -3044,6 +3072,11 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
int getQueryPlanTTLSeconds();
|
||||
|
||||
/**
|
||||
* Return the EXPLAIN (with options) to use for capturing query plans.
|
||||
*/
|
||||
String getQueryPlanExplain();
|
||||
|
||||
/**
|
||||
* Return mapping locations to search for xml mapping via class path search.
|
||||
*/
|
||||
|
||||
@@ -5,8 +5,6 @@ import io.ebean.service.SpiContainer;
|
||||
import io.ebean.service.SpiContainerFactory;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
@@ -157,10 +155,10 @@ public final class DatabaseFactory {
|
||||
* Create the container instance using the configuration.
|
||||
*/
|
||||
private static SpiContainer createContainer(ContainerConfig containerConfig) {
|
||||
Iterator<SpiContainerFactory> factories = ServiceLoader.load(SpiContainerFactory.class).iterator();
|
||||
if (factories.hasNext()) {
|
||||
return factories.next().create(containerConfig);
|
||||
SpiContainerFactory factory = XBootstrapService.containerFactory();
|
||||
if (factory == null) {
|
||||
throw new IllegalStateException("Service loader didn't find a SpiContainerFactory?");
|
||||
}
|
||||
throw new IllegalStateException("Service loader didn't find a SpiContainerFactory?");
|
||||
return factory.create(containerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.ebean.docstore.DocQueryContext;
|
||||
import io.ebean.docstore.RawDoc;
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.function.Predicate;
|
||||
/**
|
||||
* Document storage operations.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public interface DocumentStore {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
@@ -42,7 +42,7 @@ import java.util.stream.Stream;
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public interface DtoQuery<T> extends CancelableQuery {
|
||||
|
||||
/**
|
||||
@@ -140,6 +140,17 @@ public interface DtoQuery<T> extends CancelableQuery {
|
||||
* Bind the named multi-value array parameter which we would use with Postgres ANY.
|
||||
* <p>
|
||||
* For Postgres this binds an ARRAY rather than expands into multiple bind values.
|
||||
* <pre>{@code
|
||||
*
|
||||
* String sql = "select id, name from o_customer where id = any(:idList)";
|
||||
*
|
||||
* var ids = List.of(1, 2, 3);
|
||||
*
|
||||
* List<CustomerDto> list2 = DB.findDto(CustomerDto.class, sql)
|
||||
* .setArrayParameter("idList", ids)
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
DtoQuery<T> setArrayParameter(String name, Collection<?> values);
|
||||
|
||||
@@ -223,5 +234,16 @@ public interface DtoQuery<T> extends CancelableQuery {
|
||||
* source. We use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* against the master data source.
|
||||
*/
|
||||
DtoQuery<T> usingMaster();
|
||||
default DtoQuery<T> usingMaster() {
|
||||
return usingMaster(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see #usingMaster()
|
||||
*/
|
||||
DtoQuery<T> usingMaster(boolean useMaster);
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* @deprecated migrate to using {@link PagedList#emptyList()} only.
|
||||
* <p>
|
||||
* An empty PagedList.
|
||||
* <p>
|
||||
* For use in application code when we need to return a PagedList but don't want to
|
||||
@@ -17,7 +19,8 @@ import java.util.concurrent.Future;
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
public class EmptyPagedList<T> implements PagedList<T> {
|
||||
@Deprecated(forRemoval = true)
|
||||
public final class EmptyPagedList<T> implements PagedList<T> {
|
||||
|
||||
@Override
|
||||
public void loadCount() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.ebean.search.*;
|
||||
|
||||
import jakarta.persistence.NonUniqueResultException;
|
||||
@@ -31,7 +31,7 @@ import java.util.function.Predicate;
|
||||
*
|
||||
* @see Query#where()
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public interface ExpressionList<T> {
|
||||
|
||||
/**
|
||||
@@ -53,14 +53,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
Query<T> orderById(boolean orderById);
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #orderBy(String)}
|
||||
*/
|
||||
@Deprecated(since = "13.19", forRemoval = true)
|
||||
default ExpressionList<T> order(String orderByClause) {
|
||||
return orderBy(orderByClause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the order by clause replacing the existing order by clause if there is
|
||||
* one.
|
||||
@@ -71,14 +63,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> orderBy(String orderBy);
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #orderBy()}.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
default OrderBy<T> order() {
|
||||
return orderBy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the OrderBy so that you can append an ascending or descending
|
||||
* property to the order by clause.
|
||||
@@ -221,18 +205,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
int delete();
|
||||
|
||||
/**
|
||||
* Execute as a delete query deleting the 'root level' beans that match the predicates
|
||||
* in the query.
|
||||
* <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 deleted.
|
||||
*/
|
||||
int delete(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute as a update query.
|
||||
*
|
||||
@@ -241,14 +213,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
int update();
|
||||
|
||||
/**
|
||||
* Execute as a update query with the given transaction.
|
||||
*
|
||||
* @return the number of rows that were updated.
|
||||
* @see UpdateQuery
|
||||
*/
|
||||
int update(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query returning true if a row is found.
|
||||
* <p>
|
||||
@@ -349,7 +313,7 @@ public interface ExpressionList<T> {
|
||||
* List<String> names =
|
||||
* DB.find(Customer.class)
|
||||
* .select("name")
|
||||
* .order().asc("name")
|
||||
* .orderBy().asc("name")
|
||||
* .findSingleAttributeList();
|
||||
*
|
||||
* }</pre>
|
||||
@@ -362,7 +326,7 @@ public interface ExpressionList<T> {
|
||||
* .setDistinct(true)
|
||||
* .select("name")
|
||||
* .where().eq("status", Customer.Status.NEW)
|
||||
* .order().asc("name")
|
||||
* .orderBy().asc("name")
|
||||
* .setMaxRows(100)
|
||||
* .findSingleAttributeList();
|
||||
*
|
||||
@@ -1109,6 +1073,14 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> like(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is LIKE if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>likeIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> likeIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Case insensitive Like - property like value where the value contains the
|
||||
* SQL wild card characters % (percentage) and _ (underscore). Typically uses
|
||||
@@ -1116,17 +1088,41 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> ilike(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is case insensitive LIKE if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>ilikeIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> ilikeIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Starts With - property like value%.
|
||||
*/
|
||||
ExpressionList<T> startsWith(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is STARTS WITH if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>startsWithIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> startsWithIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Case insensitive Starts With - property like value%. Typically uses a
|
||||
* lower() function to make the expression case insensitive.
|
||||
*/
|
||||
ExpressionList<T> istartsWith(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is case insensitive STARTS WITH if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>istartsWithIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> istartsWithIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Ends With - property like %value.
|
||||
*/
|
||||
@@ -1143,12 +1139,28 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> contains(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is CONTAINS if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>containsIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> containsIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Case insensitive Contains - property like %value%. Typically uses a lower()
|
||||
* function to make the expression case insensitive.
|
||||
*/
|
||||
ExpressionList<T> icontains(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is case insensitive CONTAINS if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>icontainsIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> icontainsIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* In expression using pairs of value objects.
|
||||
*/
|
||||
@@ -1707,7 +1719,7 @@ public interface ExpressionList<T> {
|
||||
* .eq("status", Customer.Status.ACTIVE)
|
||||
* .gt("id", 0)
|
||||
* .endAnd()
|
||||
* .order().asc("name")
|
||||
* .orderBy().asc("name")
|
||||
* .findList();
|
||||
* }</pre>
|
||||
*/
|
||||
@@ -1728,7 +1740,7 @@ public interface ExpressionList<T> {
|
||||
* .or()
|
||||
* .eq("status", Customer.Status.ACTIVE)
|
||||
* .isNull("anniversary")
|
||||
* .order().asc("name")
|
||||
* .orderBy().asc("name")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
@@ -1748,7 +1760,7 @@ public interface ExpressionList<T> {
|
||||
* .eq("status", Customer.Status.ACTIVE)
|
||||
* .gt("id", 0)
|
||||
* .endAnd()
|
||||
* .order().asc("name")
|
||||
* .orderBy().asc("name")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
@@ -1782,7 +1794,7 @@ public interface ExpressionList<T> {
|
||||
* .gt("id", 1)
|
||||
* .eq("anniversary", onAfter)
|
||||
* .endNot()
|
||||
* .order()
|
||||
* .orderBy()
|
||||
* .asc("name")
|
||||
* .findList();
|
||||
*
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import java.time.Clock;
|
||||
|
||||
/**
|
||||
* The extended API for Database.
|
||||
*/
|
||||
public interface ExtendedServer {
|
||||
|
||||
/**
|
||||
* Deprecated but no yet determined suitable replacement (to support testing only change of clock).
|
||||
* <p>
|
||||
* Set the Clock to use for <code>@WhenCreated</code> and <code>@WhenModified</code>.
|
||||
* <p>
|
||||
* Note that we only expect to change the Clock for testing purposes.
|
||||
* </p>
|
||||
*/
|
||||
@Deprecated
|
||||
void setClock(Clock clock);
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import io.ebean.service.SpiFetchGroupQuery;
|
||||
|
||||
/**
|
||||
@@ -61,7 +61,7 @@ import io.ebean.service.SpiFetchGroupQuery;
|
||||
*
|
||||
* @param <T> The bean type the Fetch group can be applied to
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public interface FetchGroup<T> {
|
||||
|
||||
/**
|
||||
@@ -84,7 +84,7 @@ public interface FetchGroup<T> {
|
||||
* @return The FetchGroup with the given select clause
|
||||
*/
|
||||
static <T> FetchGroup<T> of(Class<T> cls, String select) {
|
||||
return XServiceProvider.fetchGroupOf(cls, select);
|
||||
return XBootstrapService.fetchGroupOf(cls, select);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,14 +108,14 @@ public interface FetchGroup<T> {
|
||||
* @return The FetchGroupBuilder with the given select clause which we will add fetch clauses to
|
||||
*/
|
||||
static <T> FetchGroupBuilder<T> of(Class<T> cls) {
|
||||
return XServiceProvider.fetchGroupOf(cls);
|
||||
return XBootstrapService.fetchGroupOf(cls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a query to be used by query beans for constructing FetchGroup.
|
||||
*/
|
||||
static <T> SpiFetchGroupQuery<T> queryFor(Class<T> beanType) {
|
||||
return XServiceProvider.fetchGroupQueryFor(beanType);
|
||||
return XBootstrapService.fetchGroupQueryFor(beanType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
/**
|
||||
* Builds a FetchGroup by adding fetch clauses.
|
||||
@@ -23,7 +23,7 @@ import io.avaje.lang.NonNullApi;
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public interface FetchGroupBuilder<T> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -79,7 +79,7 @@ import java.util.Set;
|
||||
*
|
||||
* @param <T> the entity bean type
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public interface Filter<T> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ import java.util.List;
|
||||
* public List<Customer> findNew() {
|
||||
* return query().where()
|
||||
* .eq("status", Customer.Status.NEW)
|
||||
* .order("name")
|
||||
* .orderBy("name")
|
||||
* .findList()
|
||||
* }
|
||||
* }
|
||||
@@ -60,7 +60,7 @@ import java.util.List;
|
||||
* @see BeanRepository
|
||||
* @see BeanFinder
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public class Finder<I, T> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package io.ebean;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* FutureMap represents the result of a background query execution that will
|
||||
* return a map of entities.
|
||||
* <p>
|
||||
* It extends the java.util.concurrent.Future with the ability to cancel the
|
||||
* query, check if it is finished and get the resulting list waiting for the
|
||||
* query to finish (ie. the standard features of java.util.concurrent.Future).
|
||||
* </p>
|
||||
* <p>
|
||||
* A simple example:
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // create a query to find all orders
|
||||
* Query<Long,Order> query = DB.find(Order.class)
|
||||
* .setMapKey("id");
|
||||
*
|
||||
* // execute the query in a background thread
|
||||
* // immediately returning the futureMap
|
||||
* FutureMap<Long,Order> futureMap = query.findFutureMap();
|
||||
*
|
||||
* // do something else ...
|
||||
*
|
||||
* if (!futureMap.isDone()){
|
||||
* // we can cancel the query execution. This will cancel
|
||||
* // the underlying query if that is supported by the JDBC
|
||||
* // driver and database
|
||||
* futureMap.cancel(true);
|
||||
* }
|
||||
*
|
||||
* if (!futureMap.isCancelled()){
|
||||
* // wait for the query to finish and return the map
|
||||
* Map<Long,Order> map = futureMap.get();
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
public interface FutureMap<K, T> extends Future<Map<K, T>> {
|
||||
|
||||
/**
|
||||
* Return the query that is being executed by a background thread.
|
||||
*/
|
||||
Query<T> getQuery();
|
||||
|
||||
/**
|
||||
* Same as {@link #get()} but wraps InterruptedException and ExecutionException in the
|
||||
* unchecked PersistenceException.
|
||||
*
|
||||
* @return The query list result
|
||||
* @throws PersistenceException when a InterruptedException or ExecutionException occurs.
|
||||
*/
|
||||
Map<K, T> getUnchecked();
|
||||
|
||||
/**
|
||||
* Same as {@link #get(long, TimeUnit)} but wraps InterruptedException
|
||||
* and ExecutionException in the unchecked PersistenceException.
|
||||
*
|
||||
* @return The query list result
|
||||
* @throws TimeoutException if the wait timed out
|
||||
* @throws PersistenceException if a InterruptedException or ExecutionException occurs.
|
||||
*/
|
||||
Map<K, T> getUnchecked(long timeout, TimeUnit unit) throws TimeoutException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Query-scoped immutable bean cache.
|
||||
*
|
||||
* <p>Typical use is to attach an immutable cache to a query and let Ebean use it when
|
||||
* resolving assoc-one references.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FetchGroup<Customer> customerGroup = FetchGroup.of(Customer.class)
|
||||
* .select("name,version")
|
||||
* .fetch("billingAddress", "line1,city")
|
||||
* .fetch("shippingAddress", "line1,city")
|
||||
* .build();
|
||||
*
|
||||
* ImmutableBeanCache<Customer> customerCache = ImmutableBeanCaches.builder(Customer.class)
|
||||
* .loading(database, customerGroup)
|
||||
* .build();
|
||||
*
|
||||
* Order order = database.find(Order.class)
|
||||
* .setId(id)
|
||||
* .setUnmodifiable(true)
|
||||
* .using(customerCache)
|
||||
* .findOne();
|
||||
* }</pre>
|
||||
*
|
||||
* @param <T> The bean type.
|
||||
*
|
||||
* @see ImmutableBeanCaches#builder(Class)
|
||||
*/
|
||||
@NullMarked
|
||||
public interface ImmutableBeanCache<T> {
|
||||
|
||||
/**
|
||||
* Return the bean type this cache provides values for.
|
||||
*/
|
||||
Class<T> type();
|
||||
|
||||
/**
|
||||
* Return immutable cached beans by id (loading and populating misses as needed).
|
||||
*/
|
||||
Map<Object, T> getAll(Set<Object> ids);
|
||||
|
||||
/**
|
||||
* Return a cached bean for the given id if it is already present.
|
||||
* <p>
|
||||
* This does not trigger loading or record a miss.
|
||||
*/
|
||||
default @Nullable T getIfPresent(Object id) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import io.ebean.service.SpiImmutableCacheFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* Utility factory methods for {@link ImmutableBeanCache}.
|
||||
*
|
||||
* <p>Use {@link #builder(Class)} when you want explicit cache policy controls (for example
|
||||
* max size or TTL). Use {@link #loading(Class, Database, FetchGroup)} as a shorthand for
|
||||
* query-loader-backed memoization.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FetchGroup<MyRef> fetchGroup = FetchGroup.of(MyRef.class)
|
||||
* .select("version")
|
||||
* .build();
|
||||
*
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
|
||||
* .loading(database, fetchGroup)
|
||||
* .maxSize(10_000)
|
||||
* .maxIdleSeconds(300)
|
||||
* .maxSecondsToLive(1_800)
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
@NullMarked
|
||||
public final class ImmutableBeanCaches {
|
||||
|
||||
private ImmutableBeanCaches() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a builder for immutable bean caches.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
|
||||
* .loading(database, FetchGroup.of(MyRef.class, "version"))
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
public static <T> ImmutableCacheBuilder<T> builder(Class<T> type) {
|
||||
SpiImmutableCacheFactory factory = XBootstrapService.immutableCacheFactory();
|
||||
if (factory != null) {
|
||||
return factory.builder(type);
|
||||
}
|
||||
return new LoadingBuilder<>(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a loader-backed immutable bean cache that memoizes both hits and misses.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.loading(MyRef.class, ids ->
|
||||
* database.find(MyRef.class)
|
||||
* .setUnmodifiable(true)
|
||||
* .where().idIn(ids)
|
||||
* .findMap()
|
||||
* );
|
||||
* }</pre>
|
||||
*
|
||||
* @param type The bean type.
|
||||
* @param loader Batch loader for unresolved ids.
|
||||
*/
|
||||
public static <T> ImmutableBeanCache<T> loading(Class<T> type, Function<Set<Object>, Map<Object, T>> loader) {
|
||||
return builder(type).loader(loader).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a query-loader-backed immutable bean cache.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.loading(
|
||||
* MyRef.class,
|
||||
* database,
|
||||
* FetchGroup.of(MyRef.class, "version")
|
||||
* );
|
||||
* }</pre>
|
||||
*/
|
||||
public static <T> ImmutableBeanCache<T> loading(Class<T> type, Database db, FetchGroup<T> fetchGroup) {
|
||||
return builder(type).loading(db, fetchGroup).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a batch loader backed by an unmodifiable query using the given fetch group.
|
||||
*/
|
||||
public static <T> Function<Set<Object>, Map<Object, T>> queryLoader(Database db, Class<T> type, FetchGroup<T> fetchGroup) {
|
||||
return new QueryLoader<>(type, db, fetchGroup);
|
||||
}
|
||||
|
||||
private static final class QueryLoader<T> implements Function<Set<Object>, Map<Object, T>> {
|
||||
|
||||
private final Class<T> type;
|
||||
private final Database db;
|
||||
private final FetchGroup<T> fetchGroup;
|
||||
|
||||
QueryLoader(Class<T> type, Database db, FetchGroup<T> fetchGroup) {
|
||||
this.type = requireNonNull(type);
|
||||
this.db = requireNonNull(db);
|
||||
this.fetchGroup = requireNonNull(fetchGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Object, T> apply(Set<Object> ids) {
|
||||
if (ids.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return db.find(type)
|
||||
.select(fetchGroup)
|
||||
.setUnmodifiable(true)
|
||||
.where().idIn(ids)
|
||||
.findMap();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LoadingBuilder<T> implements ImmutableCacheBuilder<T> {
|
||||
|
||||
private final Class<T> type;
|
||||
private Function<Set<Object>, Map<Object, T>> loader;
|
||||
private int maxSize;
|
||||
private int maxIdleSeconds;
|
||||
private int maxSecondsToLive;
|
||||
|
||||
private LoadingBuilder(Class<T> type) {
|
||||
this.type = requireNonNull(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> loader(Function<Set<Object>, Map<Object, T>> loader) {
|
||||
this.loader = requireNonNull(loader);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> loading(Database db, FetchGroup<T> fetchGroup) {
|
||||
this.loader = new QueryLoader<>(type, db, fetchGroup);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxSize(int maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxIdleSeconds(int maxIdleSeconds) {
|
||||
this.maxIdleSeconds = maxIdleSeconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxSecondsToLive(int maxSecondsToLive) {
|
||||
this.maxSecondsToLive = maxSecondsToLive;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableBeanCache<T> build() {
|
||||
if (loader == null) {
|
||||
throw new IllegalStateException("No loader defined. Call loader(...) or loading(...) before build().");
|
||||
}
|
||||
if (maxSize > 0 || maxIdleSeconds > 0 || maxSecondsToLive > 0) {
|
||||
throw new IllegalStateException("Cache policy options require SpiImmutableCacheFactory (ebean-core).");
|
||||
}
|
||||
return new LoadingCache<>(type, loader);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LoadingCache<T> implements ImmutableBeanCache<T> {
|
||||
|
||||
private final Class<T> type;
|
||||
private final Function<Set<Object>, Map<Object, T>> loader;
|
||||
private final ConcurrentHashMap<Object, T> cache = new ConcurrentHashMap<>();
|
||||
private final Set<Object> misses = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private LoadingCache(Class<T> type, Function<Set<Object>, Map<Object, T>> loader) {
|
||||
this.type = requireNonNull(type);
|
||||
this.loader = requireNonNull(loader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<T> type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable T getIfPresent(Object id) {
|
||||
return cache.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Object, T> getAll(Set<Object> ids) {
|
||||
if (ids.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Set<Object> loadIds = null;
|
||||
for (Object id : ids) {
|
||||
if (!cache.containsKey(id) && !misses.contains(id)) {
|
||||
if (loadIds == null) {
|
||||
loadIds = new LinkedHashSet<>();
|
||||
}
|
||||
loadIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadIds != null && !loadIds.isEmpty()) {
|
||||
Map<Object, T> loaded = loader.apply(loadIds);
|
||||
if (loaded == null) {
|
||||
loaded = Collections.emptyMap();
|
||||
}
|
||||
for (Map.Entry<Object, T> entry : loaded.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
cache.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
for (Object id : loadIds) {
|
||||
if (!cache.containsKey(id)) {
|
||||
misses.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<Object, T> result = new LinkedHashMap<>();
|
||||
for (Object id : ids) {
|
||||
T bean = cache.get(id);
|
||||
if (bean != null) {
|
||||
result.put(id, bean);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Builder for creating {@link ImmutableBeanCache} instances.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FetchGroup<MyRef> fetchGroup = FetchGroup.of(MyRef.class)
|
||||
* .select("version")
|
||||
* .fetch("names", "locale,text")
|
||||
* .build();
|
||||
*
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
|
||||
* .loading(database, fetchGroup)
|
||||
* .maxSize(10_000)
|
||||
* .maxIdleSeconds(300)
|
||||
* .maxSecondsToLive(1_800)
|
||||
* .build();
|
||||
* }</pre>
|
||||
*
|
||||
* @see ImmutableBeanCaches#builder(Class)
|
||||
*/
|
||||
@NullMarked
|
||||
public interface ImmutableCacheBuilder<T> {
|
||||
|
||||
/**
|
||||
* Set the batch loader used for unresolved ids.
|
||||
*/
|
||||
ImmutableCacheBuilder<T> loader(Function<Set<Object>, Map<Object, T>> loader);
|
||||
|
||||
/**
|
||||
* Configure a query-based loader using the given database and fetch group.
|
||||
*/
|
||||
ImmutableCacheBuilder<T> loading(Database db, FetchGroup<T> fetchGroup);
|
||||
|
||||
/**
|
||||
* Configure max cache size (0 means unbounded).
|
||||
*/
|
||||
ImmutableCacheBuilder<T> maxSize(int maxSize);
|
||||
|
||||
/**
|
||||
* Configure max idle time in seconds (0 means disabled).
|
||||
*/
|
||||
ImmutableCacheBuilder<T> maxIdleSeconds(int maxIdleSeconds);
|
||||
|
||||
/**
|
||||
* Configure max time-to-live in seconds (0 means disabled).
|
||||
*/
|
||||
ImmutableCacheBuilder<T> maxSecondsToLive(int maxSecondsToLive);
|
||||
|
||||
/**
|
||||
* Build the immutable bean cache.
|
||||
*/
|
||||
ImmutableBeanCache<T> build();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.Nullable;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Options to be used with insert such as ON CONFLICT DO UPDATE | NOTHING.
|
||||
|
||||
@@ -61,7 +61,7 @@ package io.ebean;
|
||||
* .eq("status", Customer.Status.ACTIVE)
|
||||
* .gt("id", 0)
|
||||
* .endAnd()
|
||||
* .order().asc("name");
|
||||
* .orderBy().asc("name");
|
||||
*
|
||||
* q.findList();
|
||||
* String s = q.getGeneratedSql();
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.ebean;
|
||||
|
||||
/**
|
||||
* Thrown when trying to access a property that isn't loaded on an entity
|
||||
* that is unmodifiable or has disabled lazy loading.
|
||||
* <p>
|
||||
* On a normal mutable entity accessing the property would invoke lazy loading. On
|
||||
* a unmodifiable entity with lazy loading disabled, accessing an unloaded property
|
||||
* throws this LazyInitialisationException instead.
|
||||
*/
|
||||
public class LazyInitialisationException extends BeanAccessException {
|
||||
|
||||
/**
|
||||
* Create specifying the property that was being accessed.
|
||||
*/
|
||||
public LazyInitialisationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,7 @@ public interface ModifyAwareType {
|
||||
*/
|
||||
void setMarkedDirty(boolean markedDirty);
|
||||
|
||||
default Object freeze() {
|
||||
return this; // throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,11 @@ import java.util.Objects;
|
||||
/**
|
||||
* Represents an Order By for a Query.
|
||||
* <p>
|
||||
* Is a ordered list of OrderBy.Property objects each specifying a property and
|
||||
* Is an ordered list of OrderBy.Property objects each specifying a property and
|
||||
* whether it is ascending or descending order.
|
||||
* </p>
|
||||
* <p>
|
||||
* Typically you will not construct an OrderBy yourself but use one that exists
|
||||
* Typically, you will not construct an OrderBy yourself but use one that exists
|
||||
* on the Query object.
|
||||
* </p>
|
||||
*/
|
||||
public class OrderBy<T> implements Serializable {
|
||||
|
||||
@@ -25,8 +23,22 @@ public class OrderBy<T> implements Serializable {
|
||||
private final List<Property> list;
|
||||
|
||||
/**
|
||||
* Create an OrderBy parsing the given order by clause.
|
||||
* <p>
|
||||
* The order by clause follows SQL order by clause with comma's between each
|
||||
* property and optionally "asc" or "desc" to represent ascending or
|
||||
* descending order respectively.
|
||||
*/
|
||||
public static <P> OrderBy<P> of(String orderByClause) {
|
||||
return new OrderBy<>(orderByClause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed from public API.
|
||||
* <p>
|
||||
* Create an empty OrderBy with no associated query.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public OrderBy() {
|
||||
this.list = new ArrayList<>(3);
|
||||
}
|
||||
@@ -36,20 +48,17 @@ public class OrderBy<T> implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an orderBy parsing the order by clause.
|
||||
* <p>
|
||||
* The order by clause follows SQL order by clause with comma's between each
|
||||
* property and optionally "asc" or "desc" to represent ascending or
|
||||
* descending order respectively.
|
||||
* </p>
|
||||
* @deprecated migrate to {@link OrderBy#of(String)}.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public OrderBy(String orderByClause) {
|
||||
this(null, orderByClause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct with a given query and order by clause.
|
||||
* @deprecated This method will be removed from public API.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public OrderBy(Query<T> query, String orderByClause) {
|
||||
this.query = query;
|
||||
this.list = new ArrayList<>(3);
|
||||
@@ -110,8 +119,11 @@ public class OrderBy<T> implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method will become internal only API.
|
||||
* <p>
|
||||
* Return a copy of this OrderBy with the path trimmed.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public OrderBy<T> copyWithTrim(String path) {
|
||||
List<Property> newList = new ArrayList<>(list.size());
|
||||
for (Property aList : list) {
|
||||
@@ -186,15 +198,15 @@ public class OrderBy<T> implements Serializable {
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
var append = new StringAppend();
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
Property property = list.get(i);
|
||||
if (i > 0) {
|
||||
sb.append(", ");
|
||||
append.append(", ");
|
||||
}
|
||||
sb.append(property.toStringFormat());
|
||||
property.toStringFormat(append);
|
||||
}
|
||||
return sb.toString();
|
||||
return append.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -231,6 +243,55 @@ public class OrderBy<T> implements Serializable {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the order by clause.
|
||||
*/
|
||||
public interface Append {
|
||||
|
||||
/**
|
||||
* Append a property expression.
|
||||
*/
|
||||
Append property(String property);
|
||||
|
||||
/**
|
||||
* Append a literal.
|
||||
*/
|
||||
Append append(String literal);
|
||||
|
||||
/**
|
||||
* Parse and append an expression.
|
||||
*/
|
||||
Append parse(String expression);
|
||||
}
|
||||
|
||||
private static final class StringAppend implements Append {
|
||||
|
||||
private final StringBuilder builder = new StringBuilder();
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Append property(String property) {
|
||||
builder.append(property);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Append append(String literal) {
|
||||
builder.append(literal);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Append parse(String raw) {
|
||||
builder.append(raw);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A property and its ascending descending order.
|
||||
*/
|
||||
@@ -309,36 +370,25 @@ public class OrderBy<T> implements Serializable {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toStringFormat();
|
||||
return property;
|
||||
}
|
||||
|
||||
public String toStringFormat() {
|
||||
if (nulls == null && collation == null) {
|
||||
if (ascending) {
|
||||
return property;
|
||||
public void toStringFormat(Append append) {
|
||||
if (collation != null) {
|
||||
if (collation.contains("${}")) {
|
||||
// this is a complex collation, e.g. DB2 - we must replace the property
|
||||
append.parse(collation.replace("${}", property));
|
||||
} else {
|
||||
return property + " desc";
|
||||
append.property(property).append(" collate ").append(collation);
|
||||
}
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (collation != null) {
|
||||
if (collation.contains("${}")) {
|
||||
// this is a complex collation, e.g. DB2 - we must replace the property
|
||||
sb.append(collation.replace("${}", property));
|
||||
} else {
|
||||
sb.append(property);
|
||||
sb.append(" collate ").append(collation);
|
||||
}
|
||||
} else {
|
||||
sb.append(property);
|
||||
}
|
||||
if (!ascending) {
|
||||
sb.append(' ').append("desc");
|
||||
}
|
||||
if (nulls != null) {
|
||||
sb.append(' ').append(nulls).append(' ').append(highLow);
|
||||
}
|
||||
return sb.toString();
|
||||
append.property(property);
|
||||
}
|
||||
if (!ascending) {
|
||||
append.append(" desc");
|
||||
}
|
||||
if (nulls != null) {
|
||||
append.append(" ").append(nulls).append(" ").append(highLow);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.concurrent.Future;
|
||||
*
|
||||
* PagedList<Order> pagedList = DB.find(Order.class)
|
||||
* .where().eq("status", Order.Status.NEW)
|
||||
* .order().asc("id")
|
||||
* .orderBy().asc("id")
|
||||
* .setFirstRow(0)
|
||||
* .setMaxRows(50)
|
||||
* .findPagedList();
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Used to specify Paging on a Query as an alternative to setting each of the
|
||||
* maxRows, firstRow and orderBy clause via:
|
||||
* {@link Query#setMaxRows(int)} + {@link Query#setFirstRow(int)} + {@link Query#setOrderBy(OrderBy)}.
|
||||
* <p>
|
||||
* Example use:
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* var orderBy = OrderBy.of("lastName desc nulls first, firstName asc");
|
||||
* var paging = Paging.of(0, 100, orderBy);
|
||||
*
|
||||
* new QCustomer()
|
||||
* .name.isNotNull()
|
||||
* .setPaging(paging)
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
public interface Paging {
|
||||
|
||||
/**
|
||||
* Create a Paging with the given page index size and orderBy.
|
||||
*
|
||||
* @param pageIndex the page index starting from zero
|
||||
* @param pageSize the page size (effectively max rows)
|
||||
* @param orderBy order by for the query result
|
||||
*/
|
||||
static Paging of(int pageIndex, int pageSize, @Nullable OrderBy<?> orderBy) {
|
||||
return DPaging.build(pageIndex, pageSize, orderBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Paging with a raw order by clause.
|
||||
*
|
||||
* @param pageIndex the page index starting from zero
|
||||
* @param pageSize the page size (effectively max rows)
|
||||
* @param orderByClause raw order by clause for ordering the query result
|
||||
*/
|
||||
static Paging of(int pageIndex, int pageSize, @Nullable String orderByClause) {
|
||||
return of(pageIndex, pageSize, OrderBy.of(orderByClause));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Paging that will use the id property for ordering.
|
||||
*
|
||||
* @param pageIndex the page index starting from zero
|
||||
* @param pageSize the page size (effectively max rows)
|
||||
*/
|
||||
static Paging of(int pageIndex, int pageSize) {
|
||||
return DPaging.build(pageIndex, pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Paging that will not apply any pagination to a query.
|
||||
*/
|
||||
static Paging ofNone() {
|
||||
return DPaging.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the page index.
|
||||
*/
|
||||
int pageIndex();
|
||||
|
||||
/**
|
||||
* Return the page size.
|
||||
*/
|
||||
int pageSize();
|
||||
|
||||
/**
|
||||
* Return the order by.
|
||||
*/
|
||||
OrderBy<?> orderBy();
|
||||
|
||||
/**
|
||||
* Return a Paging using the given page index.
|
||||
*/
|
||||
Paging withPage(int pageIndex);
|
||||
|
||||
/**
|
||||
* Return a Paging using the given order by clause.
|
||||
*/
|
||||
Paging withOrderBy(String orderByClause);
|
||||
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import java.util.Objects;
|
||||
* .where()
|
||||
* .eq("store", "def")
|
||||
* .inPairs(pairs) // IN clause with 'pairs' of values
|
||||
* .order("sku desc")
|
||||
* .orderBy("sku desc")
|
||||
*
|
||||
* // query expressions cover the natural key properties
|
||||
* // so we can choose to hit the L2 bean cache if we want
|
||||
|
||||
@@ -13,21 +13,21 @@ public interface ProfileLocation {
|
||||
* Create and return a new ProfileLocation.
|
||||
*/
|
||||
static ProfileLocation create() {
|
||||
return XServiceProvider.profileLocationFactory().create();
|
||||
return XBootstrapService.profileLocationFactory().create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new ProfileLocation with line number.
|
||||
*/
|
||||
static ProfileLocation createWithLine() {
|
||||
return XServiceProvider.profileLocationFactory().createWithLine();
|
||||
return XBootstrapService.profileLocationFactory().createWithLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new ProfileLocation with a given lineNumber and label.
|
||||
*/
|
||||
static ProfileLocation create(String label) {
|
||||
return XServiceProvider.profileLocationFactory().create(label);
|
||||
return XBootstrapService.profileLocationFactory().create(label);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,249 @@
|
||||
package io.ebean;
|
||||
|
||||
/**
|
||||
* Builder for ORM Query projection (the select and fetch part).
|
||||
*
|
||||
* @param <SELF> The builder type
|
||||
* @param <T> The entity bean type
|
||||
*/
|
||||
public interface QueryBuilderProjection<SELF extends QueryBuilderProjection<SELF, T>, T> {
|
||||
|
||||
/**
|
||||
* Apply the path properties replacing the select and fetch clauses.
|
||||
* <p>
|
||||
* This is typically used when the FetchPath is applied to both the query and the JSON output.
|
||||
*/
|
||||
SELF apply(FetchPath fetchPath);
|
||||
|
||||
/**
|
||||
* Specify the properties to fetch on the root level entity bean in comma delimited format.
|
||||
* <p>
|
||||
* The Id property is automatically included in the properties to fetch unless setDistinct(true)
|
||||
* is set on the query.
|
||||
* </p>
|
||||
* <p>
|
||||
* Use {@link #fetch(String, String)} to specify specific properties to fetch
|
||||
* on other non-root level paths of the object graph.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* List<Customer> customers = DB.find(Customer.class)
|
||||
* // Only fetch the customer id, name and status.
|
||||
* // This is described as a "Partial Object"
|
||||
* .select("name, status")
|
||||
* .where.ilike("name", "rob%")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param fetchProperties the properties to fetch for this bean (* = all properties).
|
||||
*/
|
||||
SELF select(String fetchProperties);
|
||||
|
||||
/**
|
||||
* Set DISTINCT ON clause. This is a Postgres only SQL feature.
|
||||
*
|
||||
* @param distinctOn The properties to include in the DISTINCT ON clause.
|
||||
*/
|
||||
SELF distinctOn(String distinctOn);
|
||||
|
||||
/**
|
||||
* Apply the fetchGroup which defines what part of the object graph to load.
|
||||
*/
|
||||
SELF select(FetchGroup<T> fetchGroup);
|
||||
|
||||
/**
|
||||
* Specify a path to fetch eagerly including specific properties.
|
||||
* <p>
|
||||
* Ebean will endeavour to fetch this path using a SQL join. If Ebean determines that it can
|
||||
* not use a SQL join (due to maxRows or because it would result in a cartesian product) Ebean
|
||||
* will automatically convert this fetch query into a "query join" - i.e. use fetchQuery().
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // query orders...
|
||||
* List<Order> orders = DB.find(Order.class)
|
||||
* // fetch the customer...
|
||||
* // ... getting the customers name and phone number
|
||||
* .fetch("customer", "name, phoneNumber")
|
||||
*
|
||||
* // ... also fetch the customers billing address (* = all properties)
|
||||
* .fetch("customer.billingAddress", "*")
|
||||
* .findList();
|
||||
* }</pre>
|
||||
* <p>
|
||||
* If columns is null or "*" then all columns/properties for that path are fetched.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // fetch customers (their id, name and status)
|
||||
* List<Customer> customers = DB.find(Customer.class)
|
||||
* .select("name, status")
|
||||
* .fetch("contacts", "firstName,lastName,email")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param path the property path we wish to fetch eagerly.
|
||||
* @param fetchProperties properties of the associated bean that you want to include in the
|
||||
* fetch (* means all properties, null also means all properties).
|
||||
*/
|
||||
SELF fetch(String path, String fetchProperties);
|
||||
|
||||
/**
|
||||
* Fetch the path and properties using a "query join" (separate SQL query).
|
||||
* <p>
|
||||
* This is the same as:
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* fetch(path, fetchProperties, FetchConfig.ofQuery())
|
||||
*
|
||||
* }</pre>
|
||||
* <p>
|
||||
* This would be used instead of a fetch() when we use a separate SQL query to fetch this
|
||||
* part of the object graph rather than a SQL join.
|
||||
* <p>
|
||||
* We might typically get a performance benefit when the path to fetch is a OneToMany
|
||||
* or ManyToMany, the 'width' of the 'root bean' is wide and the cardinality of the many
|
||||
* is high.
|
||||
*
|
||||
* @param path the property path we wish to fetch eagerly.
|
||||
* @param fetchProperties properties of the associated bean that you want to include in the
|
||||
* fetch (* means all properties, null also means all properties).
|
||||
*/
|
||||
SELF fetchQuery(String path, String fetchProperties);
|
||||
|
||||
/**
|
||||
* Fetch the path and properties using L2 bean cache.
|
||||
*
|
||||
* @param path The path of the beans we are fetching from L2 cache.
|
||||
* @param fetchProperties The properties that should be loaded.
|
||||
*/
|
||||
SELF fetchCache(String path, String fetchProperties);
|
||||
|
||||
/**
|
||||
* Fetch the path and properties lazily (via batch lazy loading).
|
||||
* <p>
|
||||
* This is the same as:
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* fetch(path, fetchProperties, FetchConfig.ofLazy())
|
||||
*
|
||||
* }</pre>
|
||||
* <p>
|
||||
* The reason for using fetchLazy() is to either:
|
||||
* <ul>
|
||||
* <li>Control/tune what is fetched as part of lazy loading</li>
|
||||
* <li>Make use of the L2 cache, build this part of the graph from L2 cache</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param path the property path we wish to fetch lazily.
|
||||
* @param fetchProperties properties of the associated bean that you want to include in the
|
||||
* fetch (* means all properties, null also means all properties).
|
||||
*/
|
||||
SELF fetchLazy(String path, String fetchProperties);
|
||||
|
||||
/**
|
||||
* Additionally specify a FetchConfig to use a separate query or lazy loading
|
||||
* to load this path.
|
||||
* <pre>{@code
|
||||
*
|
||||
* // fetch customers (their id, name and status)
|
||||
* List<Customer> customers = DB.find(Customer.class)
|
||||
* .select("name, status")
|
||||
* .fetch("contacts", "firstName,lastName,email", FetchConfig.ofLazy(10))
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param path the property path we wish to fetch eagerly.
|
||||
*/
|
||||
SELF fetch(String path, String fetchProperties, FetchConfig fetchConfig);
|
||||
|
||||
/**
|
||||
* Specify a path to fetch eagerly including all its properties.
|
||||
* <p>
|
||||
* Ebean will endeavour to fetch this path using a SQL join. If Ebean determines that it can
|
||||
* not use a SQL join (due to maxRows or because it would result in a cartesian product) Ebean
|
||||
* will automatically convert this fetch query into a "query join" - i.e. use fetchQuery().
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // fetch customers (their id, name and status)
|
||||
* List<Customer> customers = DB.find(Customer.class)
|
||||
* // eager fetch the contacts
|
||||
* .fetch("contacts")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param path the property path we wish to fetch eagerly.
|
||||
*/
|
||||
SELF fetch(String path);
|
||||
|
||||
/**
|
||||
* Fetch the path eagerly using a "query join" (separate SQL query).
|
||||
* <p>
|
||||
* This is the same as:
|
||||
* <pre>{@code
|
||||
*
|
||||
* fetch(path, FetchConfig.ofQuery())
|
||||
*
|
||||
* }</pre>
|
||||
* <p>
|
||||
* This would be used instead of a fetch() when we use a separate SQL query to fetch this
|
||||
* part of the object graph rather than a SQL join.
|
||||
* <p>
|
||||
* We might typically get a performance benefit when the path to fetch is a OneToMany
|
||||
* or ManyToMany, the 'width' of the 'root bean' is wide and the cardinality of the many
|
||||
* is high.
|
||||
*
|
||||
* @param path the property path we wish to fetch eagerly
|
||||
*/
|
||||
SELF fetchQuery(String path);
|
||||
|
||||
/**
|
||||
* Fetch the path eagerly using L2 cache.
|
||||
*/
|
||||
SELF fetchCache(String path);
|
||||
|
||||
/**
|
||||
* Fetch the path lazily (via batch lazy loading).
|
||||
* <p>
|
||||
* This is the same as:
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* fetch(path, FetchConfig.ofLazy())
|
||||
*
|
||||
* }</pre>
|
||||
* <p>
|
||||
* The reason for using fetchLazy() is to either:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Control/tune what is fetched as part of lazy loading</li>
|
||||
* <li>Make use of the L2 cache, build this part of the graph from L2 cache</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param path the property path we wish to fetch lazily.
|
||||
*/
|
||||
SELF fetchLazy(String path);
|
||||
|
||||
/**
|
||||
* Additionally specify a JoinConfig to specify a "query join" and or define
|
||||
* the lazy loading query.
|
||||
* <pre>{@code
|
||||
*
|
||||
* // fetch customers (their id, name and status)
|
||||
* List<Customer> customers = DB.find(Customer.class)
|
||||
* // lazy fetch contacts with a batch size of 100
|
||||
* .fetch("contacts", FetchConfig.ofLazy(100))
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
SELF fetch(String path, FetchConfig fetchConfig);
|
||||
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import java.util.Iterator;
|
||||
*
|
||||
* Query<Customer> query = database.find(Customer.class)
|
||||
* .where().gt("id", 0)
|
||||
* .order("id")
|
||||
* .orderBy("id")
|
||||
* .setMaxRows(2);
|
||||
*
|
||||
* QueryIterator<Customer> it = query.findIterate();
|
||||
|
||||
@@ -111,7 +111,7 @@ package io.ebean;
|
||||
* .fetch("order.customer", "name")
|
||||
* .where().gt("order.id", 0)
|
||||
* .having().gt("totalAmount", 20)
|
||||
* .order().desc("totalAmount")
|
||||
* .orderBy().desc("totalAmount")
|
||||
* .setMaxRows(10)
|
||||
* .findList();
|
||||
*
|
||||
|
||||
@@ -22,14 +22,14 @@ public interface RawSqlBuilder {
|
||||
* resultSet.
|
||||
*/
|
||||
static RawSql resultSet(ResultSet resultSet, String... propertyNames) {
|
||||
return XServiceProvider.rawSql().resultSet(resultSet, propertyNames);
|
||||
return XBootstrapService.rawSql().resultSet(resultSet, propertyNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a SqlRow based on the resultSet with dbTrueValue and binaryOptimizedUUID options.
|
||||
*/
|
||||
static SqlRow sqlRow(ResultSet resultSet, final String dbTrueValue, boolean binaryOptimizedUUID) throws SQLException {
|
||||
return XServiceProvider.rawSql().sqlRow(resultSet, dbTrueValue, binaryOptimizedUUID);
|
||||
return XBootstrapService.rawSql().sqlRow(resultSet, dbTrueValue, binaryOptimizedUUID);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,7 +38,7 @@ public interface RawSqlBuilder {
|
||||
* this query.
|
||||
*/
|
||||
static RawSqlBuilder unparsed(String sql) {
|
||||
return XServiceProvider.rawSql().unparsed(sql);
|
||||
return XBootstrapService.rawSql().unparsed(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ public interface RawSqlBuilder {
|
||||
* </p>
|
||||
*/
|
||||
static RawSqlBuilder parse(String sql) {
|
||||
return XServiceProvider.rawSql().parsed(sql);
|
||||
return XBootstrapService.rawSql().parsed(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.Serializable;
|
||||
import java.sql.Connection;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
@@ -39,7 +40,7 @@ import java.util.function.Predicate;
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public interface SqlQuery extends Serializable, CancelableQuery {
|
||||
|
||||
/**
|
||||
@@ -62,7 +63,17 @@ public interface SqlQuery extends Serializable, CancelableQuery {
|
||||
* source. We use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* against the master data source.
|
||||
*/
|
||||
SqlQuery usingMaster();
|
||||
default SqlQuery usingMaster() {
|
||||
return usingMaster(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see #usingMaster()
|
||||
*/
|
||||
SqlQuery usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Execute the query returning a list.
|
||||
@@ -171,7 +182,7 @@ public interface SqlQuery extends Serializable, CancelableQuery {
|
||||
* List<SqlRow> list =
|
||||
* DB.sqlQuery(sql)
|
||||
* .setParameter("Rob")
|
||||
* .setParameter("Status.NEW)
|
||||
* .setParameter(Status.NEW)
|
||||
* .findList();
|
||||
*
|
||||
* // the same as ...
|
||||
@@ -241,11 +252,45 @@ public interface SqlQuery extends Serializable, CancelableQuery {
|
||||
*/
|
||||
SqlQuery setParameter(int position, Object value);
|
||||
|
||||
/**
|
||||
* Bind the array parameter by its index position for use with Postgres ANY.
|
||||
* <p>
|
||||
* For Postgres this binds an ARRAY rather than expands into multiple bind values.
|
||||
* <pre>{@code
|
||||
*
|
||||
* String sql = "select name from customer where id = any(?)";
|
||||
*
|
||||
* List<SqlRow> list =
|
||||
* DB.sqlQuery(sql)
|
||||
* .setArrayParameter(1, List.of(1, 2, 3))
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
SqlQuery setArrayParameter(int position, Collection<?> value);
|
||||
|
||||
/**
|
||||
* Bind the named parameter value.
|
||||
*/
|
||||
SqlQuery setParameter(String name, Object value);
|
||||
|
||||
/**
|
||||
* Bind the named array parameter which we would use with Postgres ANY.
|
||||
* <p>
|
||||
* For Postgres this binds an ARRAY rather than expands into multiple bind values.
|
||||
* <pre>{@code
|
||||
*
|
||||
* String sql = "select name from customer where id = any(:idList)";
|
||||
*
|
||||
* List<SqlRow> list =
|
||||
* DB.sqlQuery(sql)
|
||||
* .setArrayParameter("idList", List.of(1, 2, 3))
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
SqlQuery setArrayParameter(String name, Collection<?> value);
|
||||
|
||||
/**
|
||||
* Set the index of the first row of the results to return.
|
||||
*/
|
||||
@@ -322,6 +367,14 @@ public interface SqlQuery extends Serializable, CancelableQuery {
|
||||
*/
|
||||
interface TypeQuery<T> {
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see SqlQuery#usingMaster(boolean)
|
||||
*/
|
||||
TypeQuery<T> usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Execute the query using the given transaction.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.ebean;
|
||||
|
||||
/**
|
||||
* Attempted to modify a read only entity.
|
||||
*/
|
||||
public class UnmodifiableEntityException extends BeanAccessException {
|
||||
private static final long serialVersionUID = 1;
|
||||
|
||||
/**
|
||||
* Create with no message.
|
||||
*/
|
||||
public UnmodifiableEntityException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create with message.
|
||||
*/
|
||||
public UnmodifiableEntityException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.ebean.metric.MetricFactory;
|
||||
import io.ebean.service.*;
|
||||
|
||||
import java.util.ServiceLoader;
|
||||
|
||||
/**
|
||||
* Bootstrap internal services.
|
||||
*/
|
||||
public final class XBootstrapService {
|
||||
|
||||
private static final SpiContainerFactory containerFactory;
|
||||
private static final SpiRawSqlService rawSqlService;
|
||||
private static final SpiProfileLocationFactory profileLocationFactory;
|
||||
private static final SpiFetchGroupService fetchGroupService;
|
||||
private static final SpiImmutableCacheFactory immutableCacheFactory;
|
||||
private static final MetricFactory metricFactory;
|
||||
private static final SpiJsonService jsonService;
|
||||
static {
|
||||
SpiContainerFactory _factory = null;
|
||||
SpiRawSqlService _raw = null;
|
||||
SpiProfileLocationFactory _profile = null;
|
||||
SpiFetchGroupService _fetch = null;
|
||||
SpiImmutableCacheFactory _immutable = null;
|
||||
MetricFactory _metric = null;
|
||||
SpiJsonService _json = null;
|
||||
for (BootstrapService extension : ServiceLoader.load(BootstrapService.class)) {
|
||||
if (extension instanceof SpiContainerFactory) {
|
||||
_factory = (SpiContainerFactory)extension;
|
||||
} else if (extension instanceof SpiRawSqlService) {
|
||||
_raw = (SpiRawSqlService)extension;
|
||||
} else if (extension instanceof SpiProfileLocationFactory) {
|
||||
_profile = (SpiProfileLocationFactory)extension;
|
||||
} else if (extension instanceof SpiFetchGroupService) {
|
||||
_fetch = (SpiFetchGroupService)extension;
|
||||
} else if (extension instanceof SpiImmutableCacheFactory) {
|
||||
_immutable = (SpiImmutableCacheFactory) extension;
|
||||
} else if (extension instanceof MetricFactory) {
|
||||
_metric = (MetricFactory)extension;
|
||||
} else if (extension instanceof SpiJsonService) {
|
||||
_json = (SpiJsonService)extension;
|
||||
}
|
||||
}
|
||||
containerFactory = _factory;
|
||||
rawSqlService = _raw;
|
||||
profileLocationFactory = _profile;
|
||||
fetchGroupService = _fetch;
|
||||
immutableCacheFactory = _immutable;
|
||||
metricFactory = _metric;
|
||||
jsonService = _json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MetricFactory found in boostrap service loading.
|
||||
*/
|
||||
public static MetricFactory metricFactory() {
|
||||
return metricFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SpiJsonService found in boostrap service loading.
|
||||
*/
|
||||
public static SpiJsonService jsonService() {
|
||||
return jsonService;
|
||||
}
|
||||
|
||||
static SpiContainerFactory containerFactory() {
|
||||
return containerFactory;
|
||||
}
|
||||
|
||||
static SpiRawSqlService rawSql() {
|
||||
return rawSqlService;
|
||||
}
|
||||
|
||||
static SpiProfileLocationFactory profileLocationFactory() {
|
||||
return profileLocationFactory;
|
||||
}
|
||||
|
||||
static SpiImmutableCacheFactory immutableCacheFactory() {
|
||||
return immutableCacheFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FetchGroup with the given select clause.
|
||||
*/
|
||||
static <T> FetchGroup<T> fetchGroupOf(Class<T> cls, String select) {
|
||||
return fetchGroupService.of(cls, select);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FetchGroupBuilder with the given select clause.
|
||||
*/
|
||||
static <T> FetchGroupBuilder<T> fetchGroupOf(Class<T> cls) {
|
||||
return fetchGroupService.of(cls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FetchGroup Query for building fetch groups via query beans.
|
||||
*/
|
||||
static <T> SpiFetchGroupQuery<T> fetchGroupQueryFor(Class<T> cls) {
|
||||
return fetchGroupService.queryFor(cls);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.ebean.service.SpiFetchGroupQuery;
|
||||
import io.ebean.service.SpiFetchGroupService;
|
||||
import io.ebean.service.SpiProfileLocationFactory;
|
||||
import io.ebean.service.SpiRawSqlService;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.ServiceLoader;
|
||||
|
||||
/**
|
||||
* Lookup internal services.
|
||||
*/
|
||||
final class XServiceProvider {
|
||||
|
||||
private static SpiRawSqlService rawSqlService = initRawSql();
|
||||
|
||||
private static SpiProfileLocationFactory profileLocationFactory = initProfileLocation();
|
||||
|
||||
private static SpiFetchGroupService fetchGroupService = initSpiFetchGroupService();
|
||||
|
||||
private static SpiFetchGroupService initSpiFetchGroupService() {
|
||||
return loadFirstService(SpiFetchGroupService.class);
|
||||
}
|
||||
|
||||
private static SpiRawSqlService initRawSql() {
|
||||
return loadFirstService(SpiRawSqlService.class);
|
||||
}
|
||||
|
||||
private static SpiProfileLocationFactory initProfileLocation() {
|
||||
return loadFirstService(SpiProfileLocationFactory.class);
|
||||
}
|
||||
|
||||
private static <T> T loadFirstService(Class<T> cls) {
|
||||
Iterator<T> loader = ServiceLoader.load(cls).iterator();
|
||||
if (loader.hasNext()) {
|
||||
return loader.next();
|
||||
}
|
||||
throw new IllegalStateException("No service implementation found for " + cls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the RawSqlService implementation.
|
||||
*/
|
||||
static SpiRawSqlService rawSql() {
|
||||
return rawSqlService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the RawSqlService implementation.
|
||||
*/
|
||||
static SpiProfileLocationFactory profileLocationFactory() {
|
||||
return profileLocationFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FetchGroup with the given select clause.
|
||||
*/
|
||||
static <T> FetchGroup<T> fetchGroupOf(Class<T> cls, String select) {
|
||||
return fetchGroupService.of(cls, select);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FetchGroupBuilder with the given select clause.
|
||||
*/
|
||||
static <T> FetchGroupBuilder<T> fetchGroupOf(Class<T> cls) {
|
||||
return fetchGroupService.of(cls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FetchGroup Query for building fetch groups via query beans.
|
||||
*/
|
||||
static <T> SpiFetchGroupQuery<T> fetchGroupQueryFor(Class<T> cls) {
|
||||
return fetchGroupService.queryFor(cls);
|
||||
}
|
||||
}
|
||||
@@ -119,18 +119,6 @@ public interface BeanCollection<E> extends Serializable, ToStringAware {
|
||||
*/
|
||||
void setLoader(BeanCollectionLoader beanLoader);
|
||||
|
||||
/**
|
||||
* Set to true if you want the BeanCollection to be treated as read only. This
|
||||
* means no elements can be added or removed etc.
|
||||
*/
|
||||
void setReadOnly(boolean readOnly);
|
||||
|
||||
/**
|
||||
* Return true if the collection should be treated as readOnly and no elements
|
||||
* can be added or removed etc.
|
||||
*/
|
||||
boolean isReadOnly();
|
||||
|
||||
/**
|
||||
* Add the bean to the collection. This is disallowed for BeanMap.
|
||||
*/
|
||||
@@ -237,7 +225,7 @@ public interface BeanCollection<E> extends Serializable, ToStringAware {
|
||||
boolean wasTouched();
|
||||
|
||||
/**
|
||||
* Return a shallow copy of this collection that is modifiable.
|
||||
* Freeze the collection returning an unmodifiable version.
|
||||
*/
|
||||
BeanCollection<E> shallowCopy();
|
||||
Object freeze();
|
||||
}
|
||||
|
||||
@@ -148,18 +148,6 @@ public interface EntityBeanIntercept extends Serializable {
|
||||
*/
|
||||
boolean isLoadedFromCache();
|
||||
|
||||
/**
|
||||
* Return true if the bean should be treated as readOnly. If a setter method
|
||||
* is called when it is readOnly an Exception is thrown.
|
||||
*/
|
||||
boolean isReadOnly();
|
||||
|
||||
/**
|
||||
* Set the readOnly status. If readOnly then calls to setter methods through
|
||||
* an exception.
|
||||
*/
|
||||
void setReadOnly(boolean readOnly);
|
||||
|
||||
/**
|
||||
* Set the bean to be updated when persisted (for merge).
|
||||
*/
|
||||
@@ -552,4 +540,9 @@ public interface EntityBeanIntercept extends Serializable {
|
||||
* Update the 'next' mutable info returning the content that was obtained via dirty detection.
|
||||
*/
|
||||
String mutableNext(int propertyIndex);
|
||||
|
||||
/**
|
||||
* Return true if this entity bean should be frozen. Used to handle recursive freezing.
|
||||
*/
|
||||
boolean freeze();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package io.ebean.bean;
|
||||
|
||||
/**
|
||||
* Common base features for EntityBeanIntercept.
|
||||
*/
|
||||
abstract class InterceptBase implements EntityBeanIntercept {
|
||||
|
||||
final EntityBean owner;
|
||||
boolean fullyLoadedBean;
|
||||
|
||||
/**
|
||||
* Create with a given entity.
|
||||
*/
|
||||
InterceptBase(Object ownerBean) {
|
||||
this.owner = (EntityBean) ownerBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL - Constructor only for use by serialization frameworks.
|
||||
*/
|
||||
InterceptBase() {
|
||||
this.owner = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final EntityBean owner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isFullyLoadedBean() {
|
||||
return fullyLoadedBean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setFullyLoadedBean(boolean fullyLoadedBean) {
|
||||
this.fullyLoadedBean = fullyLoadedBean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String property(int propertyIndex) {
|
||||
if (propertyIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
return owner._ebean_getPropertyName(propertyIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int findProperty(String propertyName) {
|
||||
final String[] names = owner._ebean_getPropertyNames();
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
if (names[i].equals(propertyName)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final StringBuilder loadedPropertyKey() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
final int len = propertyLength();
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (isLoadedProperty(i)) {
|
||||
sb.append(i).append(',');
|
||||
}
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,44 @@
|
||||
package io.ebean.bean;
|
||||
|
||||
import io.ebean.LazyInitialisationException;
|
||||
import io.ebean.UnmodifiableEntityException;
|
||||
import io.ebean.ValuePair;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* EntityBeanIntercept optimised for read only use.
|
||||
* <p>
|
||||
* For the read only use this intercept doesn't need to hold any state that is normally
|
||||
* required for updates such as per property changed, loaded, dirty state, original values
|
||||
* required for updates such as per property changed, dirty state, original values
|
||||
* bean state etc.
|
||||
*/
|
||||
public class InterceptReadOnly implements EntityBeanIntercept {
|
||||
public final class InterceptReadOnly extends InterceptBase {
|
||||
|
||||
private final EntityBean owner;
|
||||
private final boolean[] loaded;
|
||||
private boolean frozen;
|
||||
|
||||
/**
|
||||
* Create with a given entity.
|
||||
*/
|
||||
public InterceptReadOnly(Object ownerBean) {
|
||||
this.owner = (EntityBean) ownerBean;
|
||||
super(ownerBean);
|
||||
this.loaded = new boolean[owner._ebean_getPropertyNames().length];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean freeze() {
|
||||
if (frozen) {
|
||||
return false;
|
||||
} else {
|
||||
frozen = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InterceptReadOnly{" + owner + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityBean owner() {
|
||||
return owner;
|
||||
return "InterceptReadOnly{frozen:" + frozen + " loaded:" + loadedPropertyNames() + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,18 +101,13 @@ public class InterceptReadOnly implements EntityBeanIntercept {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFullyLoadedBean() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFullyLoadedBean(boolean fullyLoadedBean) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPartial() {
|
||||
for (boolean flag : loaded) {
|
||||
if (!flag) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -136,7 +138,14 @@ public class InterceptReadOnly implements EntityBeanIntercept {
|
||||
|
||||
@Override
|
||||
public boolean hasIdOnly(int idIndex) {
|
||||
return false;
|
||||
for (int i = 0; i < loaded.length; i++) {
|
||||
if (i == idIndex) {
|
||||
if (!loaded[i]) return false;
|
||||
} else if (loaded[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -159,16 +168,6 @@ public class InterceptReadOnly implements EntityBeanIntercept {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadOnly(boolean readOnly) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setForceUpdate(boolean forceUpdate) {
|
||||
|
||||
@@ -234,44 +233,38 @@ public class InterceptReadOnly implements EntityBeanIntercept {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findProperty(String propertyName) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String property(int propertyIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int propertyLength() {
|
||||
return 0;
|
||||
return loaded.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPropertyLoaded(String propertyName, boolean loaded) {
|
||||
|
||||
final int position = findProperty(propertyName);
|
||||
if (position == -1) {
|
||||
throw new IllegalArgumentException("Property not found - " + propertyName);
|
||||
}
|
||||
this.loaded[position] = loaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPropertyUnloaded(int propertyIndex) {
|
||||
|
||||
loaded[propertyIndex] = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoadedProperty(int propertyIndex) {
|
||||
|
||||
loaded[propertyIndex] = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoadedPropertyAll() {
|
||||
|
||||
Arrays.fill(loaded, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoadedProperty(int propertyIndex) {
|
||||
return false;
|
||||
return loaded[propertyIndex];
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -321,7 +314,16 @@ public class InterceptReadOnly implements EntityBeanIntercept {
|
||||
|
||||
@Override
|
||||
public Set<String> loadedPropertyNames() {
|
||||
return Collections.emptySet();
|
||||
if (fullyLoadedBean) {
|
||||
return null;
|
||||
}
|
||||
final Set<String> props = new LinkedHashSet<>();
|
||||
for (int i = 0; i < loaded.length; i++) {
|
||||
if (loaded[i]) {
|
||||
props.add(property(i));
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -369,14 +371,11 @@ public class InterceptReadOnly implements EntityBeanIntercept {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringBuilder loadedPropertyKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean[] loaded() {
|
||||
return new boolean[0];
|
||||
final boolean[] ret = new boolean[loaded.length];
|
||||
System.arraycopy(loaded, 0, ret, 0, ret.length);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -401,7 +400,7 @@ public class InterceptReadOnly implements EntityBeanIntercept {
|
||||
|
||||
@Override
|
||||
public void initialisedMany(int propertyIndex) {
|
||||
|
||||
loaded[propertyIndex] = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -416,12 +415,14 @@ public class InterceptReadOnly implements EntityBeanIntercept {
|
||||
|
||||
@Override
|
||||
public void preGetter(int propertyIndex) {
|
||||
|
||||
if (!loaded[propertyIndex]) {
|
||||
throw new LazyInitialisationException("Property not loaded: " + property(propertyIndex));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetterMany(boolean interceptField, int propertyIndex, Object oldValue, Object newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -436,57 +437,57 @@ public class InterceptReadOnly implements EntityBeanIntercept {
|
||||
|
||||
@Override
|
||||
public void preSetter(boolean intercept, int propertyIndex, Object oldValue, Object newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetter(boolean intercept, int propertyIndex, boolean oldValue, boolean newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetter(boolean intercept, int propertyIndex, int oldValue, int newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetter(boolean intercept, int propertyIndex, long oldValue, long newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetter(boolean intercept, int propertyIndex, double oldValue, double newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetter(boolean intercept, int propertyIndex, float oldValue, float newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetter(boolean intercept, int propertyIndex, short oldValue, short newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetter(boolean intercept, int propertyIndex, char oldValue, char newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetter(boolean intercept, int propertyIndex, byte oldValue, byte newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetter(boolean intercept, int propertyIndex, char[] oldValue, char[] newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetter(boolean intercept, int propertyIndex, byte[] oldValue, byte[] newValue) {
|
||||
|
||||
throw new UnmodifiableEntityException("Attempting to modify " + property(propertyIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
* This provides the mechanisms to support deferred fetching of reference beans
|
||||
* and oldValues generation for concurrency checking.
|
||||
*/
|
||||
public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
public final class InterceptReadWrite extends InterceptBase {
|
||||
|
||||
private static final long serialVersionUID = -3664031775464862649L;
|
||||
|
||||
@@ -56,10 +56,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
private String ebeanServerName;
|
||||
private boolean deletedFromCollection;
|
||||
|
||||
/**
|
||||
* The actual entity bean that 'owns' this intercept.
|
||||
*/
|
||||
private final EntityBean owner;
|
||||
private EntityBean embeddedOwner;
|
||||
private int embeddedOwnerIndex;
|
||||
/**
|
||||
@@ -67,7 +63,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
*/
|
||||
private int state;
|
||||
private boolean forceUpdate;
|
||||
private boolean readOnly;
|
||||
private boolean dirty;
|
||||
/**
|
||||
* Flag set to disable lazy loading.
|
||||
@@ -77,7 +72,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
* Flag set when lazy loading failed due to the underlying bean being deleted in the DB.
|
||||
*/
|
||||
private boolean lazyLoadFailure;
|
||||
private boolean fullyLoadedBean;
|
||||
private boolean loadedFromCache;
|
||||
private final byte[] flags;
|
||||
private Object[] origValues;
|
||||
@@ -101,7 +95,7 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
* Create with a given entity.
|
||||
*/
|
||||
public InterceptReadWrite(Object ownerBean) {
|
||||
this.owner = (EntityBean) ownerBean;
|
||||
super(ownerBean);
|
||||
this.flags = new byte[owner._ebean_getPropertyNames().length];
|
||||
}
|
||||
|
||||
@@ -109,16 +103,20 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
* EXPERIMENTAL - Constructor only for use by serialization frameworks.
|
||||
*/
|
||||
public InterceptReadWrite() {
|
||||
this.owner = null;
|
||||
super();
|
||||
this.flags = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean freeze() {
|
||||
throw new UnsupportedOperationException("never expected");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InterceptReadWrite@" + hashCode() + "{state=" + state +
|
||||
(dirty ? " dirty;" : "") +
|
||||
(forceUpdate ? " forceUpdate;" : "") +
|
||||
(readOnly ? " readOnly;" : "") +
|
||||
(disableLazyLoad ? " disableLazyLoad;" : "") +
|
||||
(lazyLoadFailure ? " lazyLoadFailure;" : "") +
|
||||
(fullyLoadedBean ? " fullyLoadedBean;" : "") +
|
||||
@@ -131,11 +129,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityBean owner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersistenceContext persistenceContext() {
|
||||
return persistenceContext;
|
||||
@@ -200,16 +193,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
this.ebeanServerName = beanLoader.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFullyLoadedBean() {
|
||||
return fullyLoadedBean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFullyLoadedBean(boolean fullyLoadedBean) {
|
||||
this.fullyLoadedBean = fullyLoadedBean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPartial() {
|
||||
for (byte flag : flags) {
|
||||
@@ -298,16 +281,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
return loadedFromCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly() {
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadOnly(boolean readOnly) {
|
||||
this.readOnly = readOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setForceUpdate(boolean forceUpdate) {
|
||||
this.forceUpdate = forceUpdate;
|
||||
@@ -411,25 +384,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
return origValues[propertyIndex];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findProperty(String propertyName) {
|
||||
final String[] names = owner._ebean_getPropertyNames();
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
if (names[i].equals(propertyName)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String property(int propertyIndex) {
|
||||
if (propertyIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
return owner._ebean_getPropertyName(propertyIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int propertyLength() {
|
||||
return flags.length;
|
||||
@@ -668,18 +622,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringBuilder loadedPropertyKey() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
final int len = propertyLength();
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (isLoadedProperty(i)) {
|
||||
sb.append(i).append(',');
|
||||
}
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean[] loaded() {
|
||||
final boolean[] ret = new boolean[flags.length];
|
||||
@@ -708,7 +650,7 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
if (database == null) {
|
||||
throw new PersistenceException(ebeanServerName == null ? "No registered default server" : "Database [" + ebeanServerName + "] is not registered");
|
||||
}
|
||||
// For stand alone reference bean or after deserialisation lazy load
|
||||
// For stand-alone reference bean or after deserialisation lazy load
|
||||
// using the ebeanServer. Synchronise only on the bean.
|
||||
loadBeanInternal(loadProperty, database.pluginApi().beanLoader());
|
||||
return;
|
||||
@@ -845,18 +787,12 @@ public final class InterceptReadWrite implements EntityBeanIntercept {
|
||||
if (state == STATE_NEW) {
|
||||
setLoadedProperty(propertyIndex);
|
||||
} else {
|
||||
if (readOnly) {
|
||||
throw new IllegalStateException("This bean is readOnly");
|
||||
}
|
||||
setChangeLoaded(propertyIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChangedPropertyValue(int propertyIndex, boolean setDirtyState, Object origValue) {
|
||||
if (readOnly) {
|
||||
throw new IllegalStateException("This bean is readOnly");
|
||||
}
|
||||
setChangedProperty(propertyIndex);
|
||||
if (setDirtyState) {
|
||||
setOriginalValue(propertyIndex, origValue);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.ebean.cache;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -10,11 +11,11 @@ import java.util.Set;
|
||||
* the query cache entry is treated as invalid.
|
||||
* </p>
|
||||
*/
|
||||
public class QueryCacheEntry {
|
||||
public final class QueryCacheEntry {
|
||||
|
||||
private final Object value;
|
||||
private final Set<String> dependentTables;
|
||||
private final long timestamp;
|
||||
private final Instant timestamp;
|
||||
|
||||
/**
|
||||
* Create with dependent tables and timestamp.
|
||||
@@ -23,7 +24,7 @@ public class QueryCacheEntry {
|
||||
* @param dependentTables The extra tables the query is dependent on (joins to)
|
||||
* @param timestamp The timestamp that the query uses to check for modifications
|
||||
*/
|
||||
public QueryCacheEntry(Object value, Set<String> dependentTables, long timestamp) {
|
||||
public QueryCacheEntry(Object value, Set<String> dependentTables, Instant timestamp) {
|
||||
this.value = value;
|
||||
this.dependentTables = dependentTables;
|
||||
this.timestamp = timestamp;
|
||||
@@ -32,21 +33,21 @@ public class QueryCacheEntry {
|
||||
/**
|
||||
* Return the actual query result.
|
||||
*/
|
||||
public Object getValue() {
|
||||
public Object value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the tables the query result is dependent on.
|
||||
*/
|
||||
public Set<String> getDependentTables() {
|
||||
public Set<String> dependentTables() {
|
||||
return dependentTables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the timestamp used to check for modifications on the dependent tables.
|
||||
*/
|
||||
public long getTimestamp() {
|
||||
public Instant timestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package io.ebean.cache;
|
||||
|
||||
import io.avaje.lang.Nullable;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.ebean.meta.MetricVisitor;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user