mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 19:17:55 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61cc5e3459 | ||
|
|
23f23fa32b | ||
|
|
f65c409bfe | ||
|
|
ec49824430 | ||
|
|
819aaece4d | ||
|
|
c275953582 | ||
|
|
82e8494f42 | ||
|
|
abacda2c8f | ||
|
|
a7d1253dba | ||
|
|
a081d08621 | ||
|
|
8c37b53bad | ||
|
|
3f6d565800 | ||
|
|
1408696912 | ||
|
|
e0531a6315 | ||
|
|
0023f0ce13 | ||
|
|
032f4de857 | ||
|
|
59431814ce | ||
|
|
e7194055be | ||
|
|
806c7cd752 | ||
|
|
dfc7f92160 | ||
|
|
26351325a8 | ||
|
|
2d37f9d01e | ||
|
|
d3fd03ce5b | ||
|
|
a8e92791cd | ||
|
|
94e63cc30f | ||
|
|
3567263250 | ||
|
|
bc03f8d516 | ||
|
|
7035ff20eb | ||
|
|
f38479022a | ||
|
|
986cc905d8 | ||
|
|
8f3fe688cb | ||
|
|
eee26d9ed3 | ||
|
|
44bd60e586 | ||
|
|
20d7af6d25 | ||
|
|
e5aeda9d4e | ||
|
|
46bb1ca060 | ||
|
|
0652167101 | ||
|
|
6633293151 | ||
|
|
9e603f2848 | ||
|
|
fff345ffc8 | ||
|
|
dd1fe845aa | ||
|
|
80451c3c62 | ||
|
|
2d199737aa | ||
|
|
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 | ||
|
|
ab45ad34e0 | ||
|
|
24b514f851 | ||
|
|
582fec7ce4 | ||
|
|
0b11c541c8 | ||
|
|
540ceab96f | ||
|
|
8b858a073a | ||
|
|
02dafc9838 | ||
|
|
0816810d3b | ||
|
|
1f6e2ee844 | ||
|
|
dc00765a98 | ||
|
|
a48b2f4ebe | ||
|
|
837563544f | ||
|
|
fd199f406b | ||
|
|
a48e96c7e9 | ||
|
|
7ba6e5f67d | ||
|
|
6ce42b1ca3 | ||
|
|
89617cefbe | ||
|
|
dd377621a4 | ||
|
|
d844855462 | ||
|
|
f68c25d5f3 | ||
|
|
61cfc7a979 | ||
|
|
3c98ee7f1b | ||
|
|
0738316f15 | ||
|
|
ceb83befa3 | ||
|
|
a0b04cceda | ||
|
|
03edfb5454 | ||
|
|
62015451df | ||
|
|
ed9113efe6 | ||
|
|
334793667e | ||
|
|
351a1c4739 | ||
|
|
b2de333dcc | ||
|
|
6091f7b683 | ||
|
|
43fb84cbc0 | ||
|
|
52a4e9e19b | ||
|
|
a2cdea24ed | ||
|
|
cd780208e5 | ||
|
|
9ce0e923d7 | ||
|
|
9e461e4708 | ||
|
|
5fa09a6f90 | ||
|
|
6babb2e6f5 | ||
|
|
26abe27e42 | ||
|
|
9b18d46b78 | ||
|
|
eed487ad26 | ||
|
|
c10865dfaf | ||
|
|
2352df309c | ||
|
|
feb2bd1e15 | ||
|
|
11c9bdfc6f | ||
|
|
4a6fc98395 | ||
|
|
7322030e90 | ||
|
|
77634fc3eb | ||
|
|
ed8d4b5fc4 | ||
|
|
187cc0903d | ||
|
|
1d7a14c8f4 | ||
|
|
c58b7d3da3 | ||
|
|
6b2cdc14aa | ||
|
|
6d712a92a0 | ||
|
|
ea11b0c3b6 | ||
|
|
08e8402d05 | ||
|
|
006ad79379 | ||
|
|
0e063549cd | ||
|
|
4f0d2cabfb | ||
|
|
80b2d9f983 | ||
|
|
287d0c467d | ||
|
|
a8835379e7 | ||
|
|
e4df20b9dc | ||
|
|
261b13ef4b | ||
|
|
7639034d7c | ||
|
|
e6e53e0d93 | ||
|
|
c71971eb5c | ||
|
|
357af58db4 | ||
|
|
0abdb1550a | ||
|
|
c32fc814f6 | ||
|
|
8e035939dc | ||
|
|
edf98ee250 | ||
|
|
5f0a75a130 | ||
|
|
d2f16cbf6a | ||
|
|
04ba81d12f | ||
|
|
f63e7ed5af | ||
|
|
66940c0f14 | ||
|
|
1f48b3a6b3 | ||
|
|
7d368097e9 | ||
|
|
c849e29316 | ||
|
|
c78a3dd6c4 | ||
|
|
a4455ddbf2 | ||
|
|
9787af77e4 | ||
|
|
3786fb09a5 | ||
|
|
f34e7b6003 | ||
|
|
089dd8e364 | ||
|
|
44770382fb | ||
|
|
6645e5b9ec | ||
|
|
2a62d413cc | ||
|
|
0046d983cb | ||
|
|
006e33cb3e | ||
|
|
e80e8e027a | ||
|
|
2e1b52d7d4 | ||
|
|
09d5163396 | ||
|
|
a6a38b9a92 | ||
|
|
bd4521b240 | ||
|
|
8a7379879b | ||
|
|
b78dd6f79a | ||
|
|
4ebb5fd112 | ||
|
|
487368976f | ||
|
|
c6defceec9 | ||
|
|
3576aafffc | ||
|
|
66234249c6 | ||
|
|
8ed3b765ec | ||
|
|
fcbcc53e27 | ||
|
|
b377e1a618 | ||
|
|
a0f612ed71 | ||
|
|
6f64821584 | ||
|
|
47e97a77a8 | ||
|
|
e8d3f50bb4 | ||
|
|
716cfaf938 | ||
|
|
7cd9e5ee03 | ||
|
|
f9c9bea623 | ||
|
|
720832122c | ||
|
|
7b635acf56 | ||
|
|
6b7efe65d7 | ||
|
|
03cd4000f0 | ||
|
|
dfd6434b59 | ||
|
|
4e7480237b | ||
|
|
9bfcf07486 | ||
|
|
b1f1344057 | ||
|
|
07201c1519 | ||
|
|
455a9fdffc | ||
|
|
3f67425727 | ||
|
|
3fd9ae06db | ||
|
|
a8ce726e47 | ||
|
|
29cd1a143d | ||
|
|
6a19c6a0ce | ||
|
|
eaec515cc1 | ||
|
|
97709d3493 | ||
|
|
87ad874621 | ||
|
|
dfe95f6fc0 | ||
|
|
05feccfeb6 | ||
|
|
196cb09c66 | ||
|
|
589e198301 | ||
|
|
b2b81677e8 | ||
|
|
f032aad22e | ||
|
|
c48e5428c5 | ||
|
|
cbca940611 | ||
|
|
0da3772ae9 | ||
|
|
496ef36b11 | ||
|
|
a4a2aba56f | ||
|
|
803f1d8642 | ||
|
|
ff1c73e20d | ||
|
|
7afd37cd98 | ||
|
|
24bb364d40 | ||
|
|
5d54a238bd | ||
|
|
b50ce5fc3f | ||
|
|
624e92840c | ||
|
|
b03adc412c | ||
|
|
e794f5e916 | ||
|
|
a2551ce718 | ||
|
|
5b0a3607f6 | ||
|
|
bcf7bd17be | ||
|
|
102d8d5979 | ||
|
|
3edc232003 | ||
|
|
043dc5c51f | ||
|
|
e2d666df35 | ||
|
|
314e0a14b8 | ||
|
|
1f9cf1e4b7 | ||
|
|
12029ed06e | ||
|
|
89741b9312 | ||
|
|
ae962c2e08 | ||
|
|
4e3501972a | ||
|
|
a65ba41850 | ||
|
|
3b11c1cb6b | ||
|
|
41185eeb5c | ||
|
|
e97ff54ab2 | ||
|
|
97fae09c52 | ||
|
|
8b0f58b3c2 | ||
|
|
70eb557929 | ||
|
|
fdb3a3759b | ||
|
|
7a5e8d0e17 | ||
|
|
a0748f8139 | ||
|
|
9f65673f54 | ||
|
|
8f6a3c59d1 | ||
|
|
05210ec411 | ||
|
|
faffda8318 | ||
|
|
af3901ce01 | ||
|
|
acd538207f | ||
|
|
84f0ef9fce | ||
|
|
7ab447b21c | ||
|
|
6f8805a6b3 | ||
|
|
0171c1afef | ||
|
|
31c106f604 | ||
|
|
20d2838be4 | ||
|
|
4238a951ca | ||
|
|
b5d4bed615 | ||
|
|
3ef715ef64 | ||
|
|
aea39528ab | ||
|
|
dd498409e0 | ||
|
|
bae33101df | ||
|
|
e1972ac544 | ||
|
|
c7241ba3dc | ||
|
|
73416ed65a | ||
|
|
cd70c178f1 | ||
|
|
aa298235f3 | ||
|
|
8733fed14b | ||
|
|
169d170247 | ||
|
|
d0ee42e0de | ||
|
|
91ecc074a7 | ||
|
|
c2b381af1e | ||
|
|
eeb0a15617 | ||
|
|
6abed0dd73 | ||
|
|
1318cd706f | ||
|
|
1e8c609dec | ||
|
|
4506596379 | ||
|
|
0b2c016af2 | ||
|
|
29d946ec59 | ||
|
|
da76b422c8 | ||
|
|
a1a3b33fc6 | ||
|
|
7ecd7877d5 | ||
|
|
e5f64f4211 | ||
|
|
90a6b71b7e | ||
|
|
131d531922 | ||
|
|
5684919a9f | ||
|
|
80fc0723c8 | ||
|
|
3ae4c8981e | ||
|
|
b80c67286b | ||
|
|
0bead4135e | ||
|
|
ac6473e039 | ||
|
|
32188bdd54 | ||
|
|
b3a577e3f9 | ||
|
|
7d1774e2ed | ||
|
|
395edb62bf | ||
|
|
716b7e7b93 | ||
|
|
9aadc347d7 | ||
|
|
d2d1423c88 | ||
|
|
73cfea6053 | ||
|
|
1614166d75 | ||
|
|
3d9b122a9f | ||
|
|
1cc1d29c2f | ||
|
|
45467a3358 | ||
|
|
bdc236159b | ||
|
|
f0047ffd75 | ||
|
|
429843c358 | ||
|
|
eab4f9c739 | ||
|
|
b445857227 | ||
|
|
dc7b73fc25 | ||
|
|
d5048db2c6 | ||
|
|
0b0b7cbc67 | ||
|
|
008f132453 | ||
|
|
6d2038355f | ||
|
|
004f4deae3 | ||
|
|
9223796f02 | ||
|
|
ff43672f4e | ||
|
|
811255200e | ||
|
|
6db1b86e12 | ||
|
|
400e840c22 | ||
|
|
f03892a4bd | ||
|
|
0842a3f3e2 | ||
|
|
bd0cad723d | ||
|
|
c83c85efc3 | ||
|
|
b6b8f77213 | ||
|
|
770eb06cab | ||
|
|
180b4d6e46 | ||
|
|
5377454476 | ||
|
|
dbbf6dd566 | ||
|
|
4b9d4d0a90 | ||
|
|
90fc4466bf | ||
|
|
0d69377f8b | ||
|
|
359a55d7a5 | ||
|
|
b7804e7486 | ||
|
|
febb018036 | ||
|
|
dc0585246f | ||
|
|
22c1848f58 | ||
|
|
054ea26893 | ||
|
|
8c30b66d39 | ||
|
|
14f9fe68f7 | ||
|
|
d56ca5b841 | ||
|
|
3f97f0f212 | ||
|
|
60e0c552ab | ||
|
|
f889628677 | ||
|
|
128bd171f5 | ||
|
|
6402a346a1 | ||
|
|
0ab0f9c569 | ||
|
|
19e21e17cc | ||
|
|
7e60c66759 | ||
|
|
4f18dc339f | ||
|
|
60a2626f61 | ||
|
|
e3a6f12478 | ||
|
|
2e980b06f6 | ||
|
|
b27b5512e3 | ||
|
|
3a013d50d6 | ||
|
|
3ebfd2e9e6 | ||
|
|
f51b69445a | ||
|
|
96904d525f | ||
|
|
26fa885adb | ||
|
|
f4ca4ec476 | ||
|
|
327995526e | ||
|
|
dfbbdc7d58 | ||
|
|
35894b3d8f | ||
|
|
0f219b4cd0 | ||
|
|
bfe94786de | ||
|
|
6cab2498dc | ||
|
|
dbe81bdb6e | ||
|
|
2e2b3844fe | ||
|
|
78b77dcd82 | ||
|
|
90614adb2b | ||
|
|
019ef43fe7 | ||
|
|
cfb6f0a908 | ||
|
|
77aeda1753 | ||
|
|
2a24ec3d49 | ||
|
|
d9d09b31b4 | ||
|
|
56550a9534 | ||
|
|
f40beb8234 | ||
|
|
cb06052c86 | ||
|
|
e09265fbc5 | ||
|
|
dede93ca62 | ||
|
|
caf8bd0c0f | ||
|
|
01d89a01a5 | ||
|
|
b0da647b0e | ||
|
|
fbcd4c3133 | ||
|
|
3e41c5973b | ||
|
|
1933beed9b | ||
|
|
defb35b258 | ||
|
|
8a4024ecfc | ||
|
|
9756c9133e | ||
|
|
c07d5e2c97 | ||
|
|
00e594d4fa | ||
|
|
3bf316af2b | ||
|
|
b116d3da28 | ||
|
|
804618d699 | ||
|
|
0e9fe6a096 | ||
|
|
ded4b94e54 | ||
|
|
eb0812df16 |
@@ -1,7 +1,11 @@
|
||||
|
||||
name: Build
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches: master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -17,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:
|
||||
@@ -36,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:
|
||||
|
||||
@@ -16,24 +16,26 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11, 17]
|
||||
java_version: [11, 17, 21]
|
||||
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:
|
||||
path:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: Maven version
|
||||
run: mvn --version
|
||||
- name: Build with Maven
|
||||
run: mvn package
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-clickhouse</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-cockroach</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-db2</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-h2</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-hana</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-mariadb</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-mysql</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.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.10.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.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.10.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.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-net-postgis-types</artifactId>
|
||||
<version>16.10.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;
|
||||
|
||||
}
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-nuodb</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-oracle</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.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.10.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.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.10.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.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>16.10.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;
|
||||
|
||||
}
|
||||
@@ -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.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-postgis</name>
|
||||
<description>ebean-postgis composite</description>
|
||||
<artifactId>ebean-postgis</artifactId>
|
||||
|
||||
<properties>
|
||||
<postgis.jdbc.version>2.5.1</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.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.10.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.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>16.10.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;
|
||||
|
||||
}
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-postgres</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-sqlite</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-sqlserver</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-yugabyte</name>
|
||||
@@ -16,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<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>composites</artifactId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean (all platforms)</name>
|
||||
@@ -16,31 +17,31 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-joda-time</artifactId>
|
||||
<version>13.18.0</version>
|
||||
<version>14.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-jsonnode</artifactId>
|
||||
<version>13.18.0</version>
|
||||
<version>14.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -59,13 +60,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+4
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>composites</artifactId>
|
||||
@@ -23,6 +23,9 @@
|
||||
<module>ebean-nuodb</module>
|
||||
<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,23 @@
|
||||
# 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
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.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
|
||||
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
|
||||
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.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,201 @@
|
||||
# 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 |
|
||||
|
||||
## Migration & upgrades
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Migrate to `Database.builder()`](migrating-to-database-builder.md) | Replace legacy `new DatabaseConfig()` and `DatabaseFactory.create(...)` code with `Database.builder()` and `DatabaseBuilder.build()`. Includes common rewrites, fluent builder equivalents, and manual-review cases for semi-automated upgrades |
|
||||
|
||||
## Observability
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Ebean OpenTelemetry tracing](add-ebean-opentelemetry.md) | Add `ebean-opentelemetry`, register `GlobalOpenTelemetry` once before Ebean databases are built, and troubleshoot missing spans or double-registration errors |
|
||||
| [Ebean query metrics and naming](ebean-query-metrics.md) | How Ebean query metric names are derived from `setLabel(..)` and profile locations; secondary (lazy/query) load naming; inline SQL comments; collecting metrics at runtime; mapping to avaje-metrics tags |
|
||||
| [Ebean query plan capture](ebean-query-plan-capture.md) | Enable and configure database query plan (`EXPLAIN`) capture for slow queries; bind capture vs plan capture; periodic and on-demand collection; thresholds, load limits, EXPLAIN dialect, and listeners |
|
||||
|
||||
## 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
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.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
|
||||
- Ebean OpenTelemetry tracing: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-opentelemetry.md
|
||||
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
|
||||
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.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
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.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,158 @@
|
||||
# Guide: Add Ebean OpenTelemetry tracing
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains how to enable Ebean transaction tracing with OpenTelemetry and,
|
||||
most importantly, how to order startup so Ebean sees the intended global
|
||||
OpenTelemetry instance.
|
||||
|
||||
Use this guide when adding `ebean-opentelemetry`, diagnosing missing Ebean spans,
|
||||
or fixing `GlobalOpenTelemetry` double-registration errors.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`ebean-opentelemetry` provides an Ebean profiling handler that creates transaction
|
||||
spans as children of the current active OpenTelemetry span. It does not create
|
||||
top-level request, job, or Lambda invocation spans by itself.
|
||||
|
||||
The handler resolves its tracer from `GlobalOpenTelemetry` when the Ebean
|
||||
`Database` is configured. For that reason, the application must build and register
|
||||
the OpenTelemetry SDK before any Ebean `Database` beans are created.
|
||||
|
||||
Rules of thumb:
|
||||
|
||||
- Register the global OpenTelemetry instance once.
|
||||
- Register it before building Ebean databases.
|
||||
- Model that ordering as a real DI dependency.
|
||||
- Do not call `GlobalOpenTelemetry.set(...)` or `buildAndRegisterGlobal()` in
|
||||
multiple places.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Add the dependency
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-opentelemetry</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
The module registers the Ebean OpenTelemetry profile handler via `ServiceLoader`.
|
||||
No manual Ebean plugin registration is normally required.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Build OpenTelemetry before Ebean databases
|
||||
|
||||
Create one application-owned OpenTelemetry bean. For example, when using
|
||||
`avaje-metrics-otel`:
|
||||
|
||||
```java
|
||||
import io.avaje.config.Configuration;
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.avaje.metrics.otel.MetricsOpenTelemetry;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Factory
|
||||
class OpenTelemetryConfig {
|
||||
|
||||
@Bean
|
||||
OpenTelemetry openTelemetry(Configuration config) {
|
||||
return MetricsOpenTelemetry.builder()
|
||||
.endpoint(config.get("otel.endpoint"))
|
||||
.serviceName(config.get("otel.serviceName", "orders"))
|
||||
.deploymentEnvironmentName(config.get("app.env", "local"))
|
||||
.meterInterval(Duration.ofSeconds(30))
|
||||
.traceInterval(Duration.ofSeconds(30))
|
||||
.buildAndRegisterGlobal();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you build the SDK directly, use the same principle: create the SDK once and
|
||||
register that instance globally before any Ebean databases are built.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Make database beans depend on OpenTelemetry
|
||||
|
||||
In DI code, make the `Database` bean method accept `OpenTelemetry`. This parameter
|
||||
is intentionally present to make startup order deterministic: OpenTelemetry is
|
||||
created and registered before Ebean configures the database and profile handler.
|
||||
|
||||
```java
|
||||
import io.avaje.config.Configuration;
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.ebean.Database;
|
||||
import io.ebean.datasource.DataSourceBuilder;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
|
||||
@Factory
|
||||
class DatabaseConfig {
|
||||
|
||||
@Bean
|
||||
Database database(OpenTelemetry openTelemetry, Configuration config) {
|
||||
var dataSource = DataSourceBuilder.create()
|
||||
.url(config.get("db.url"))
|
||||
.username(config.get("db.username"))
|
||||
.password(config.get("db.password"));
|
||||
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(dataSource)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Spring, use the same dependency shape: either inject `OpenTelemetry` into the
|
||||
database `@Bean` method or use `@DependsOn` to ensure the OpenTelemetry bean is
|
||||
initialized first.
|
||||
|
||||
Do not invert the dependency by making OpenTelemetry depend on the Ebean
|
||||
`Database`. That creates a startup cycle and can still initialize Ebean before the
|
||||
global OpenTelemetry instance is ready.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - Create a parent span at the application boundary
|
||||
|
||||
Ebean transaction spans are child spans. They are only created when a recording
|
||||
OpenTelemetry span is active on the current thread.
|
||||
|
||||
Use HTTP server instrumentation, Lambda instrumentation, or an application-level
|
||||
root span around the top-level request/job boundary. Ebean will then attach
|
||||
transaction spans beneath that current span.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `GlobalOpenTelemetry.set has already been called`
|
||||
|
||||
This usually means more than one component is trying to register a global SDK, or
|
||||
some startup path touched the global before the application registered its SDK.
|
||||
|
||||
Fixes:
|
||||
|
||||
1. Keep exactly one `buildAndRegisterGlobal()` / `GlobalOpenTelemetry.set(...)`
|
||||
call in the application.
|
||||
2. Build that OpenTelemetry bean before Ebean `Database` beans.
|
||||
3. Remove duplicate OTEL setup from tests, helper factories, or secondary modules.
|
||||
|
||||
### No Ebean spans appear
|
||||
|
||||
Check:
|
||||
|
||||
1. `ebean-opentelemetry` is on the runtime classpath.
|
||||
2. OpenTelemetry is registered before Ebean databases are built.
|
||||
3. There is a current recording parent span when Ebean transactions run.
|
||||
4. Sampling is not dropping the parent trace.
|
||||
@@ -0,0 +1,295 @@
|
||||
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 3: 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)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
If the project has a dedicated config-wrapper class (a `@Component` that reads config
|
||||
keys), accept it as a parameter instead of `Configuration`.
|
||||
|
||||
> **Note:** Injecting `Configuration` requires that `avaje-config` is properly wired
|
||||
> into the DI context. If you encounter "No dependency provided for
|
||||
> io.avaje.config.Configuration" errors, use `Config.get(...)` static access instead
|
||||
> (as shown in Step 2).
|
||||
|
||||
---
|
||||
|
||||
## 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,294 @@
|
||||
# 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.5.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>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>1.8</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
The `io.avaje:junit` bundle includes JUnit Jupiter (API + engine) and AssertJ,
|
||||
avoiding the need to declare those dependencies separately.
|
||||
|
||||
---
|
||||
|
||||
## 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>12.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-test</artifactId>
|
||||
<version>12.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>12.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,445 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Add `TestEntityBuilder`** to your test configuration for rapid test data creation
|
||||
with auto-populated random values. See `testing-with-testentitybuilder.md`.
|
||||
- **Proceed to Step 3** — production database configuration
|
||||
(`add-ebean-postgres-database-config.md`). Verify this step passes with
|
||||
`mvn verify` before continuing.
|
||||
@@ -0,0 +1,262 @@
|
||||
# Guide: Ebean query metrics and naming
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains the metrics Ebean captures, how the metric **name** for a query
|
||||
is derived, and how you influence that name with `setLabel(..)` and **profile
|
||||
locations**. It also covers secondary (lazy / query) load naming, the inline SQL
|
||||
comment, collecting metrics at runtime, and how the names map to avaje-metrics tags.
|
||||
|
||||
Use this guide when you want to identify a query in metrics/telemetry, when a query
|
||||
shows up under an unexpected metric name, or when wiring Ebean metrics into a reporter.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Ebean records timing and counter metrics for the work it does. Every metric has a
|
||||
**name** whose leading segment identifies the kind of work:
|
||||
|
||||
| Prefix | What it measures | Example name |
|
||||
|---|---|---|
|
||||
| `orm.` | Entity (ORM) query | `orm.Customer.findList`, `orm.CustomerFinder.byName` |
|
||||
| `dto.` | DTO query | `dto.CustomerDto.byEmail` |
|
||||
| `sql.query.` | Raw SQL query | `sql.query.<label>` |
|
||||
| `sql.update.` / `sql.call.` | Raw SQL update / stored procedure call | `sql.update.<label>` |
|
||||
| `orm.update.` | ORM update statement | `orm.update.<label>` |
|
||||
| `iud.` | Bean insert / update / delete | `iud.Customer.insert` |
|
||||
| `txn.main` / `txn.readonly` / `txn.named.` | Transactions | `txn.main`, `txn.named.processOrders` |
|
||||
| `l2n.` | L2 cache region | `l2n.customer.hit` |
|
||||
|
||||
The rest of this guide focuses on **`orm.` query names**, which is where labels and
|
||||
profile locations apply.
|
||||
|
||||
---
|
||||
|
||||
## How an ORM query name is derived
|
||||
|
||||
An entity query name has the form `orm.<identifier>`. The `<identifier>` comes from one
|
||||
of three sources, in priority order:
|
||||
|
||||
1. **An explicit `setLabel(..)`** — prefixed with the bean type for disambiguation.
|
||||
2. **A profile location** — used as-is (it is already a unique `Class.method` identifier).
|
||||
3. **Neither** — the bean type plus the query type (e.g. `findList`).
|
||||
|
||||
| Root query source | Resulting name |
|
||||
|---|---|
|
||||
| `setLabel("custMain")` on `Customer` | `orm.Customer.custMain` |
|
||||
| Profile location `CustomerFinder.byName` | `orm.CustomerFinder.byName` |
|
||||
| Unlabelled `DB.find(Customer.class).findList()` | `orm.Customer.findList` |
|
||||
|
||||
The asymmetry is intentional: an explicit label is a short, ambiguous token (`custMain`
|
||||
could be used for any bean), so the bean type is prefixed. A profile location is already
|
||||
unique and type-independent, so it is used as-is.
|
||||
|
||||
### Step 1 - Label a query explicitly
|
||||
|
||||
```java
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setLabel("custMain")
|
||||
.findList();
|
||||
// metric name: orm.Customer.custMain
|
||||
```
|
||||
|
||||
DTO queries support `setLabel(..)` too, and follow the **same naming convention** as
|
||||
ORM queries — an explicit label is prefixed with the DTO type, a profile location is
|
||||
used as-is, and an unlabelled DTO query uses just the DTO type:
|
||||
|
||||
```java
|
||||
DB.findDto(CustomerDto.class, sql)
|
||||
.setLabel("byEmail")
|
||||
.findList();
|
||||
// metric name: dto.CustomerDto.byEmail
|
||||
// profile location only -> dto.<location> (no type prefix)
|
||||
// unlabelled -> dto.CustomerDto
|
||||
```
|
||||
|
||||
### Step 2 - Use a profile location (preferred for finders / query beans)
|
||||
|
||||
A profile location identifies a query by its **call site** (`Class.method`) instead of a
|
||||
hand-written label.
|
||||
|
||||
**The common case is automatic.** With Ebean's byte-code enhancement enabled (the normal
|
||||
setup when using query beans / finders), Ebean assigns each query a profile location
|
||||
derived from its call site — no code is required:
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.eq(Status.ACTIVE)
|
||||
.findList();
|
||||
// metric name: orm.<CallingClass>.<method> (often with a line number, see below)
|
||||
```
|
||||
|
||||
The enhancer derives the location from the calling code (the method that runs the query),
|
||||
and for many call sites it includes the **source line number** (e.g.
|
||||
`CustomerService.find:42`), so distinct call sites — even in the same method — get distinct
|
||||
names automatically.
|
||||
|
||||
**Setting one explicitly.** You can also set a profile location yourself, which is useful
|
||||
without enhancement or to control the identity:
|
||||
|
||||
```java
|
||||
ProfileLocation LOC = ProfileLocation.create();
|
||||
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setProfileLocation(LOC)
|
||||
.where().eq("status", Status.ACTIVE)
|
||||
.findList();
|
||||
// metric name: orm.<DeclaringClass>.<method>
|
||||
```
|
||||
|
||||
Factory choices:
|
||||
|
||||
- `ProfileLocation.create()` — call site as `Class.method`, **no line number**.
|
||||
- `ProfileLocation.createWithLine()` — includes the source line number
|
||||
(e.g. `CustomerService.find:42`), so two queries in the **same method** get
|
||||
**distinct** names.
|
||||
- `ProfileLocation.create("label")` — a named location (used for named transactions).
|
||||
|
||||
> Note: a location with no line number (`create()`, or a call site the enhancer emits
|
||||
> without a line) means two different queries in the same method share one name. The
|
||||
> queried entity is still distinguishable downstream via the avaje-metrics `type` tag
|
||||
> (see "Mapping to avaje-metrics tags" below). Use `createWithLine()` to separate
|
||||
> same-method call sites in the name itself.
|
||||
|
||||
---
|
||||
|
||||
## Secondary (lazy / query) load naming
|
||||
|
||||
When a query lazy-loads or `fetchQuery()`-loads an association, Ebean issues a
|
||||
**secondary** query. Its name **extends the parent query's full name** with the relative
|
||||
path and the load mode (`lazy` or `query`), joined with `.`:
|
||||
|
||||
```
|
||||
orm.<parent name without the "orm." prefix>.<path>.<loadMode>
|
||||
```
|
||||
|
||||
So a secondary load is always an exact extension of its parent metric name, which makes
|
||||
the relationship obvious in dashboards.
|
||||
|
||||
Example — root labelled `custMain` on `Customer`, chain `Customer -> orders -> details`:
|
||||
|
||||
Lazy loading:
|
||||
```
|
||||
orm.Customer.custMain
|
||||
orm.Customer.custMain.orders.lazy
|
||||
orm.Customer.custMain.orders.lazy.details.lazy
|
||||
```
|
||||
|
||||
Secondary eager `fetchQuery()` loading:
|
||||
```
|
||||
orm.Customer.custMain
|
||||
orm.Customer.custMain.orders.query
|
||||
orm.Customer.custMain.orders.query.details.query
|
||||
```
|
||||
|
||||
The same applies with a **profile-location** root (no explicit `setLabel`):
|
||||
```
|
||||
orm.CustomerFinder.byName
|
||||
orm.CustomerFinder.byName.contacts.lazy
|
||||
```
|
||||
|
||||
Unlike the root query, the secondary name is **not** bean-type prefixed by the loaded
|
||||
type — it inherits the parent's name so it relates back to where the load originated.
|
||||
|
||||
---
|
||||
|
||||
## Inline SQL comment
|
||||
|
||||
When `includeLabelInSql` is enabled (the default), Ebean prepends the query's label (or
|
||||
profile-location label) as an inline SQL comment, which is useful for matching slow
|
||||
queries in database logs back to application code:
|
||||
|
||||
```sql
|
||||
select /* CustomerFinder.byName */ t0.id, t0.name from be_customer t0 where ...
|
||||
```
|
||||
|
||||
The comment uses the explicit `setLabel(..)` if present, otherwise the profile-location
|
||||
label. Secondary queries use their full extended name
|
||||
(e.g. `/* Customer.custMain.contacts.query */`). `EXISTS` / subquery forms are not
|
||||
commented.
|
||||
|
||||
Disable it via the builder:
|
||||
|
||||
```java
|
||||
Database.builder()
|
||||
.includeLabelInSql(false)
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collecting metrics at runtime
|
||||
|
||||
Read collected metrics through `Database.metaInfo()`:
|
||||
|
||||
```java
|
||||
import io.ebean.meta.MetaQueryMetric;
|
||||
import io.ebean.meta.ServerMetrics;
|
||||
|
||||
ServerMetrics metrics = database.metaInfo().collectMetrics(); // resets counters
|
||||
|
||||
for (MetaQueryMetric q : metrics.queryMetrics()) {
|
||||
System.out.printf("%s type=%s count=%d total=%d mean=%d%n",
|
||||
q.name(), // e.g. orm.Customer.custMain
|
||||
q.type().getSimpleName(), // the queried bean/DTO type, e.g. Customer
|
||||
q.count(), q.total(), q.mean());
|
||||
}
|
||||
```
|
||||
|
||||
Key API:
|
||||
|
||||
- `database.metaInfo()` → `MetaInfoManager`.
|
||||
- `collectMetrics()` collects and **resets**; `collectMetrics(false)` collects without
|
||||
reset; `visitMetrics(visitor)` for streaming.
|
||||
- `ServerMetrics` exposes `queryMetrics()`, `timedMetrics()`, `countMetrics()`.
|
||||
- `MetaQueryMetric` exposes `name()`, `label()`, `type()` (the queried `Class<?>`),
|
||||
`sql()`, `hash()`, plus timing `count()` / `total()` / `max()` / `mean()`.
|
||||
|
||||
---
|
||||
|
||||
## Mapping to avaje-metrics tags
|
||||
|
||||
When integrating with **avaje-metrics** (`avaje-metrics-ebean`
|
||||
`DatabaseMetricSupplier`), the flat `orm.`/`dto.`/`sql.` names are translated to a tagged
|
||||
form, with the bean type carried as a `type` tag:
|
||||
|
||||
```
|
||||
ebean.query{kind=orm|dto|sql, type=<BeanSimpleName>, label=<rest of the name>}
|
||||
```
|
||||
|
||||
Because the entity is available as the `type` tag, two different-entity queries that
|
||||
share a profile-location name remain distinct series on tag-aware backends (OpenTelemetry,
|
||||
Prometheus, StatsD) without needing the bean type in the name.
|
||||
|
||||
For the integration setup, see the avaje-metrics guide
|
||||
[`add-ebean-metrics.md`](https://github.com/avaje/avaje-metrics/blob/master/docs/guides/add-ebean-metrics.md).
|
||||
|
||||
To capture the database execution plan (`EXPLAIN`) for slow queries identified by these
|
||||
metrics, see [Ebean query plan capture](ebean-query-plan-capture.md).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### A query shows up as `orm.<Bean>.findList` (no useful identity)
|
||||
|
||||
It has neither a label nor a profile location. Add `setLabel(..)` or a
|
||||
`ProfileLocation`, or apply a profile location on the finder / query bean.
|
||||
|
||||
### Two queries in one method share a metric name
|
||||
|
||||
This happens when the profile location for those call sites has no line number. With
|
||||
enhancement, many call sites already include a line number; for those that don't, use
|
||||
`ProfileLocation.createWithLine()` to separate them by line, or give each an explicit
|
||||
`setLabel(..)`. On tag-aware backends the avaje-metrics `type` tag already separates
|
||||
different entity types.
|
||||
|
||||
### A secondary (lazy / query) load isn't grouped under its parent
|
||||
|
||||
Secondary names extend the parent's full name. If the parent has no label or profile
|
||||
location, its name falls back to `orm.<Bean>.<queryType>` and the secondary extends
|
||||
that. Give the root query a label or profile location for a stable parent name.
|
||||
@@ -0,0 +1,233 @@
|
||||
# Guide: Ebean query plan capture
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains how to enable and configure **query plan capture** in Ebean — the
|
||||
mechanism that captures the database's actual execution plan (via `EXPLAIN`) for slow
|
||||
queries, so you can diagnose missing indexes and poor plans in production.
|
||||
|
||||
Use this guide when you want Ebean to record real query plans, when tuning the capture
|
||||
thresholds and load limits, or when wiring a listener to ship captured plans somewhere.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Query plan capture is a **two-phase** mechanism:
|
||||
|
||||
1. **Bind capture** — when enabled, Ebean watches query executions and, for queries
|
||||
slower than a threshold, captures the actual **bind values** that were used. This is
|
||||
cheap: it just remembers the parameters of a slow execution.
|
||||
2. **Plan capture** — using those captured bind values, Ebean runs `EXPLAIN <sql>`
|
||||
against the database to obtain the execution plan, producing `MetaQueryPlan` results
|
||||
that are handed to a `QueryPlanListener`.
|
||||
|
||||
Plan capture is split this way so the expensive `EXPLAIN` work (actual database load)
|
||||
happens periodically or on demand, against representative bind values, rather than on
|
||||
every slow query.
|
||||
|
||||
Two ways to trigger phase 2:
|
||||
|
||||
- **Automatic periodic capture** — a background timer collects plans on a schedule.
|
||||
- **On demand** — call the `MetaInfoManager` API to arm and collect plans yourself
|
||||
(this is what remote tooling such as ebean-insight uses).
|
||||
|
||||
Plan capable queries are:
|
||||
|
||||
- **ORM entity SELECT queries** (`orm.*` metrics) — captured via the per-entity `BeanDescriptor`.
|
||||
- **Native-SQL `DtoQuery`** (`dto.*` metrics) — a `DtoQuery` created from a SQL string
|
||||
(`DB.findDto(MyDto.class, "select ...")`) has its own bind capture and is `EXPLAIN`'d directly.
|
||||
- **ORM-backed `DtoQuery`** (`Query.asDto(...)`) — captured via the *underlying* ORM query plan
|
||||
(`orm.*`), not the `dto.*` plan. The `dto.*` plan itself is **not** armed in this case, so it
|
||||
does not double-count in `queryPlanInit`.
|
||||
- **Native-SQL `SqlQuery`** (`sql.query.*` metrics) — a **labelled** `SqlQuery`
|
||||
(`DB.sqlQuery("select ...").setLabel("myLabel")`) has its own bind capture and is `EXPLAIN`'d
|
||||
directly. A label is required: without `setLabel(...)` the query produces no metric and no plan.
|
||||
|
||||
Specifically **excluded** are:
|
||||
|
||||
- **Update / DML** — `orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`.
|
||||
|
||||
Bind capture is wired into the ORM query path (per-entity `BeanDescriptor`), the native-SQL DTO
|
||||
path (per-DTO `DtoBeanDescriptor`), and the native-SQL `SqlQuery` path (the relational query
|
||||
engine); the init/collect API iterates all three. DML — even though it produces timing metrics —
|
||||
never captures bind values and cannot be `EXPLAIN`'d.
|
||||
|
||||
> **Cost when disabled:** SqlQuery plan capture is fully gated on the `queryPlan.enable` master
|
||||
> switch. When capture is disabled no `SqlQuery` plans are created or cached, so labelled queries
|
||||
> incur no extra cost beyond their existing timing metric.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Enable bind capture
|
||||
|
||||
Bind capture is the master switch; nothing is captured until it is on.
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.queryPlanEnable(true) // turn on bind capture
|
||||
.queryPlanThresholdMicros(100_000) // capture binds for queries slower than 100ms
|
||||
.build();
|
||||
```
|
||||
|
||||
- `queryPlanEnable(boolean)` — enable bind capture. Default **false**.
|
||||
- `queryPlanThresholdMicros(long)` — global execution-time threshold (microseconds) a
|
||||
query must exceed before its bind values are captured. Default **`Long.MAX_VALUE`**
|
||||
(effectively off), so you must either lower it or arm specific plans by hash (Step 3).
|
||||
|
||||
Equivalent `application.properties` (avaje-config / properties):
|
||||
|
||||
```properties
|
||||
queryPlan.enable=true
|
||||
queryPlan.thresholdMicros=100000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Enable automatic periodic capture (optional)
|
||||
|
||||
To have Ebean periodically run `EXPLAIN` for armed queries and report the plans:
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.queryPlanEnable(true)
|
||||
.queryPlanThresholdMicros(100_000)
|
||||
.queryPlanCapture(true) // turn on the periodic capture timer
|
||||
.queryPlanCapturePeriodSecs(600) // every 10 minutes (default)
|
||||
.queryPlanCaptureMaxTimeMillis(10_000) // stop after 10s of capturing per cycle
|
||||
.queryPlanCaptureMaxCount(10) // at most 10 plans per cycle
|
||||
.queryPlanListener(capture -> {
|
||||
for (var plan : capture.plans()) {
|
||||
System.out.println(plan.label() + "\n" + plan.plan());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
- `queryPlanCapture(boolean)` — enable the background periodic capture. Default **false**.
|
||||
- `queryPlanCapturePeriodSecs(long)` — capture frequency in seconds. Default **600** (10 min).
|
||||
- `queryPlanCaptureMaxTimeMillis(long)` — per-cycle time budget; capture stops once
|
||||
exceeded, bounding the database load. Default **10000** (10s).
|
||||
- `queryPlanCaptureMaxCount(int)` — max plans captured per cycle. Default **10**.
|
||||
- `queryPlanListener(QueryPlanListener)` — receives each `QueryPlanCapture`. If not set,
|
||||
the default listener logs plans to the `io.ebean.QUERYPLAN` logger at `INFO`.
|
||||
|
||||
Properties form:
|
||||
|
||||
```properties
|
||||
queryPlan.enable=true
|
||||
queryPlan.thresholdMicros=100000
|
||||
queryPlan.capture=true
|
||||
queryPlan.capturePeriodSecs=600
|
||||
queryPlan.captureMaxTimeMillis=10000
|
||||
queryPlan.captureMaxCount=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Capture on demand (foreground)
|
||||
|
||||
Instead of (or in addition to) the periodic timer, drive capture through
|
||||
`database.metaInfo()`. This is useful for targeted capture and is how remote tooling
|
||||
arms specific slow queries by their plan hash.
|
||||
|
||||
```java
|
||||
import io.ebean.meta.MetaInfoManager;
|
||||
import io.ebean.meta.MetaQueryPlan;
|
||||
import io.ebean.meta.QueryPlanInit;
|
||||
import io.ebean.meta.QueryPlanRequest;
|
||||
|
||||
MetaInfoManager meta = database.metaInfo();
|
||||
|
||||
// Phase 1: arm bind capture - either all plans or specific hashes
|
||||
QueryPlanInit init = new QueryPlanInit();
|
||||
init.setAll(true); // or init.add("<planHash>", 50_000);
|
||||
init.thresholdMicros(100_000);
|
||||
List<MetaQueryPlan> armed = meta.queryPlanInit(init);
|
||||
|
||||
// ... let the application run so slow executions capture their bind values ...
|
||||
|
||||
// Phase 2: collect plans now (runs EXPLAIN)
|
||||
QueryPlanRequest request = new QueryPlanRequest();
|
||||
request.maxCount(10);
|
||||
request.maxTimeMillis(10_000);
|
||||
request.since(System.currentTimeMillis() - 300_000); // binds at least ~5 min old
|
||||
List<MetaQueryPlan> plans = meta.queryPlanCollectNow(request);
|
||||
```
|
||||
|
||||
- `QueryPlanInit` arms bind capture. `setAll(true)` arms every plan; `add(hash, micros)`
|
||||
arms a specific plan (a hash of `"all"` is treated as all).
|
||||
- `QueryPlanRequest.since(epochMillis)` ensures the captured bind values have existed for
|
||||
a while, so they better represent the slowest executions. `maxCount` / `maxTimeMillis`
|
||||
bound the work, mirroring the periodic settings.
|
||||
|
||||
`MetaQueryPlan` exposes `beanType()`, `label()`, `profileLocation()`, `sql()`, `hash()`,
|
||||
`bind()`, `plan()` (the raw EXPLAIN output), `queryTimeMicros()`, `captureCount()`,
|
||||
`captureMicros()`, and `whenCaptured()`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - EXPLAIN dialect
|
||||
|
||||
Ebean chooses the `EXPLAIN` statement per database platform:
|
||||
|
||||
| Platform | EXPLAIN used |
|
||||
|---|---|
|
||||
| PostgreSQL | `explain (analyze, costs, verbose, buffers) <sql>` |
|
||||
| YugabyteDB | `explain (analyze, buffers, dist) <sql>` |
|
||||
| Oracle | `EXPLAIN PLAN FOR <sql>` |
|
||||
| SQL Server | platform-specific logger |
|
||||
| H2 / MySQL / other | `explain <sql>` |
|
||||
|
||||
Override the prefix with `queryPlanExplain(..)` (or `queryPlan.explain`):
|
||||
|
||||
```java
|
||||
Database.builder()
|
||||
.queryPlanExplain("explain (costs, verbose)") // omit ANALYZE on Postgres
|
||||
.build();
|
||||
```
|
||||
|
||||
> **Caution (PostgreSQL / Yugabyte):** the default includes `ANALYZE`, which **actually
|
||||
> executes** the query to produce real timings. For non-idempotent or expensive queries,
|
||||
> override with a non-ANALYZE `explain` to avoid side effects and extra load.
|
||||
|
||||
---
|
||||
|
||||
## Related setting: internal plan TTL
|
||||
|
||||
`queryPlanTTLSeconds(int)` (default **300**) is a **different** concept — it is the time to
|
||||
live for Ebean's *internal* query plan (the object that knows how to execute a query, read
|
||||
the result set and collect metrics). It is not part of EXPLAIN capture, but is set through
|
||||
the same builder.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No plans are captured
|
||||
|
||||
1. `queryPlanEnable(true)` must be set — it is the master switch.
|
||||
2. `queryPlanThresholdMicros` defaults to `Long.MAX_VALUE`. Lower it, or arm specific
|
||||
plans via `QueryPlanInit`, otherwise no execution is ever "slow enough".
|
||||
3. For periodic capture, also set `queryPlanCapture(true)`.
|
||||
4. Queries must actually run slower than the threshold to have their binds captured.
|
||||
|
||||
### Plans appear but nothing is reported anywhere
|
||||
|
||||
No `queryPlanListener` is configured, so plans go to the default `io.ebean.QUERYPLAN`
|
||||
logger. Set a listener, or enable `INFO` logging for `io.ebean.QUERYPLAN`.
|
||||
|
||||
### Capture adds noticeable database load
|
||||
|
||||
`EXPLAIN ANALYZE` executes the query. Reduce `queryPlanCaptureMaxCount`, increase
|
||||
`queryPlanCapturePeriodSecs`, tighten `queryPlanCaptureMaxTimeMillis`, or override
|
||||
`queryPlanExplain` to a non-ANALYZE form.
|
||||
|
||||
### An unlabelled SqlQuery or update metric never offers plan capture
|
||||
|
||||
ORM entity SELECT queries (`orm.*`), native-SQL `DtoQuery` (`dto.*`) and native-SQL
|
||||
**labelled** `SqlQuery` (`sql.query.*`) are plan capable. ORM-backed DTO queries
|
||||
(`Query.asDto(...)`) are captured via their underlying ORM plan (`orm.*`), not the `dto.*` plan.
|
||||
An unlabelled `SqlQuery` produces no metric and no plan — add `setLabel(...)` to make it
|
||||
capturable. Write metrics (`orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`) have no bind
|
||||
capture and are intentionally excluded.
|
||||
@@ -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,242 @@
|
||||
# Guide: Migrate from `DatabaseConfig` / `DatabaseFactory` to `Database.builder()`
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide shows how to migrate legacy programmatic database creation code from:
|
||||
|
||||
- `new DatabaseConfig()`
|
||||
- `DatabaseFactory.create(...)`
|
||||
- old `setXxx(...)` builder-style configuration methods
|
||||
|
||||
…to the preferred builder-based style using:
|
||||
|
||||
- `Database.builder()`
|
||||
- fluent `DatabaseBuilder` methods such as `name(...)`, `register(...)`, and `defaultDatabase(...)`
|
||||
- `DatabaseBuilder.build()`
|
||||
|
||||
Use this guide when upgrading older Ebean setup code or when building an automated/semi-automated migration.
|
||||
|
||||
---
|
||||
|
||||
## Preferred pattern
|
||||
|
||||
Prefer code shaped like this:
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties()
|
||||
.dataSourceBuilder(dataSource)
|
||||
.register(true)
|
||||
.defaultDatabase(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
The important points are:
|
||||
|
||||
1. Start with `Database.builder()`
|
||||
2. Configure via `DatabaseBuilder`
|
||||
3. Finish with `.build()`
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Replace `new DatabaseConfig()` with `Database.builder()`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.loadFromProperties();
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
DatabaseBuilder config = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties();
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Prefer the `DatabaseBuilder` type for local variables and parameters when possible.
|
||||
- If existing code only uses standard builder methods, this change is usually mechanical.
|
||||
- If existing code later reads configuration back, use `config.settings()`.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Replace `DatabaseFactory.create(config)` with `config.build()`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.loadFromProperties();
|
||||
Database database = DatabaseFactory.create(config);
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
DatabaseBuilder config = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties();
|
||||
Database database = config.build();
|
||||
```
|
||||
|
||||
### Short form
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties()
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Replace `DatabaseFactory.create("name")`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
Database database = DatabaseFactory.create("other");
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("other")
|
||||
.loadFromProperties()
|
||||
.build();
|
||||
```
|
||||
|
||||
### Important
|
||||
|
||||
For **named databases**, set `.name("...")` before `.loadFromProperties()` so the named configuration is loaded.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Replace legacy `setXxx(...)` methods with fluent builder methods
|
||||
|
||||
`DatabaseBuilder` already exposes preferred fluent names for most configuration methods.
|
||||
Use those names when migrating older setup code.
|
||||
|
||||
| Legacy call | Preferred call |
|
||||
|---|---|
|
||||
| `setName("db")` | `name("db")` |
|
||||
| `setRegister(false)` | `register(false)` |
|
||||
| `setDefaultServer(false)` | `defaultDatabase(false)` |
|
||||
| `setContainerConfig(cfg)` | `containerConfig(cfg)` |
|
||||
| `setDbSchema("app")` | `dbSchema("app")` |
|
||||
| `setDataSourceConfig(ds)` | `dataSourceBuilder(ds)` |
|
||||
| `setReadOnlyDataSourceConfig(ro)` | `readOnlyDataSourceBuilder(ro)` |
|
||||
| `setRunMigration(true)` | `runMigration(true)` |
|
||||
| `setDisableClasspathSearch(true)` | `disableClasspathSearch(true)` |
|
||||
| `setPersistBatch(batch)` | `persistBatch(batch)` |
|
||||
|
||||
### Full example
|
||||
|
||||
#### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.setRegister(false);
|
||||
config.setDefaultServer(false);
|
||||
config.setDataSourceConfig(dataSource);
|
||||
Database database = DatabaseFactory.create(config);
|
||||
```
|
||||
|
||||
#### After
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.register(false)
|
||||
.defaultDatabase(false)
|
||||
.dataSourceBuilder(dataSource)
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Verify semantics after migration
|
||||
|
||||
The migration should preserve behavior, but verify these points:
|
||||
|
||||
- `register(true)` is still the default
|
||||
- `defaultDatabase(true)` is still the default
|
||||
- call `loadFromProperties()` if the old code loaded configuration from properties
|
||||
- for named databases, set the name before loading properties
|
||||
- explicit entity registration via `addClass(...)` / `addAll(...)` is unchanged
|
||||
- custom datasource wiring via `dataSourceBuilder(...)` and `readOnlyDataSourceBuilder(...)` is unchanged
|
||||
|
||||
---
|
||||
|
||||
## Manual-review cases
|
||||
|
||||
These cases are **not** simple search-and-replace migrations and should be reviewed manually:
|
||||
|
||||
### `DatabaseFactory.createWithContextClassLoader(...)`
|
||||
|
||||
There is no direct builder shorthand for this today. Keep this as-is for now and migrate the surrounding builder configuration first.
|
||||
|
||||
### `DatabaseFactory.initialiseContainer(...)`
|
||||
|
||||
This is a container lifecycle concern, not a normal database-builder call. Keep it as-is unless you are intentionally moving the `ContainerConfig` onto the first builder via `containerConfig(...)`.
|
||||
|
||||
### `DatabaseFactory.shutdown()`
|
||||
|
||||
This is also a lifecycle concern rather than normal builder setup. Leave it alone unless you are making a deliberate lifecycle change.
|
||||
|
||||
### Variables or method signatures typed as `DatabaseConfig`
|
||||
|
||||
If the code only uses standard builder operations, switch the type to `DatabaseBuilder`.
|
||||
If the code depends on implementation-specific `DatabaseConfig` methods, review it manually.
|
||||
|
||||
### Code that needs read access to builder settings
|
||||
|
||||
Use:
|
||||
|
||||
```java
|
||||
DatabaseBuilder builder = Database.builder();
|
||||
DatabaseBuilder.Settings settings = builder.settings();
|
||||
```
|
||||
|
||||
rather than relying on the concrete `DatabaseConfig` type only to read getters.
|
||||
|
||||
---
|
||||
|
||||
## Automation notes for AI agents and bulk refactors
|
||||
|
||||
This migration is a good candidate for semi-automated upgrading.
|
||||
|
||||
### Safe mechanical rewrites
|
||||
|
||||
These are usually safe to rewrite automatically:
|
||||
|
||||
- `new DatabaseConfig()` → `Database.builder()`
|
||||
- `DatabaseFactory.create(builder)` → `builder.build()`
|
||||
- `DatabaseFactory.create("name")` → `Database.builder().name("name").loadFromProperties().build()`
|
||||
- legacy `setXxx(...)` calls → preferred fluent builder methods
|
||||
|
||||
### Flag for manual review
|
||||
|
||||
Automatically flag, but do not blindly rewrite:
|
||||
|
||||
- `DatabaseFactory.createWithContextClassLoader(...)`
|
||||
- `DatabaseFactory.initialiseContainer(...)`
|
||||
- `DatabaseFactory.shutdown()`
|
||||
- parameters, fields, or return types declared as `DatabaseConfig`
|
||||
- any use that clearly depends on `DatabaseConfig` implementation details rather than `DatabaseBuilder`
|
||||
|
||||
---
|
||||
|
||||
## Related guides
|
||||
|
||||
- [Database configuration](add-ebean-postgres-database-config.md) — preferred modern setup style using `Database.builder()`
|
||||
- [Guide index](README.md) — full list of Ebean setup and migration guides
|
||||
@@ -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/)
|
||||
@@ -0,0 +1,333 @@
|
||||
# Immutable Bean Cache — notes on multi-level / remote caching
|
||||
|
||||
These notes capture design thoughts for a possible future multi-level immutable bean cache,
|
||||
where immutable beans may be cached remotely (for example Redis or a Postgres cache table)
|
||||
in addition to an in-JVM cache.
|
||||
|
||||
## Current important constraint
|
||||
|
||||
`AssocOneHelp.read()` now uses `ImmutableBeanCache.getIfPresent(id)` as a direct-hit fast path.
|
||||
|
||||
That means:
|
||||
|
||||
- `getIfPresent(id)` is on the **row read hot path**
|
||||
- it must remain **cheap and local**
|
||||
- it should **not** perform network I/O
|
||||
- it should **not** deserialize remote payloads
|
||||
- it should **not** trigger loading or record misses
|
||||
|
||||
## Strong recommendation
|
||||
|
||||
For any multi-level cache design:
|
||||
|
||||
- **L1 cache** = in-JVM cache of already materialized immutable beans
|
||||
- **L2 cache** = remote/shared cache of serialized immutable snapshots
|
||||
- **Loader** = Ebean query using the configured fetch group
|
||||
|
||||
With that split:
|
||||
|
||||
- `getIfPresent(id)` => **L1 only**
|
||||
- `getAll(ids)` => batch through **L1 -> L2 -> loader**
|
||||
|
||||
This preserves the `AssocOneHelp` fast path.
|
||||
|
||||
---
|
||||
|
||||
## Snapshot mindset
|
||||
|
||||
Remote cache entries should be treated as **immutable snapshots**, not just arbitrary beans.
|
||||
|
||||
A cached value is specific to:
|
||||
|
||||
- bean type
|
||||
- bean id
|
||||
- tenant (if multi-tenant)
|
||||
- fetch-group / cache identity
|
||||
- serializer/schema version
|
||||
|
||||
This matters because a `Customer` cached with:
|
||||
|
||||
- `select("name,version")`
|
||||
|
||||
is not equivalent to a `Customer` cached with:
|
||||
|
||||
- `select("name,version").fetch("billingAddress", "line1,city")`
|
||||
|
||||
## Key design recommendation
|
||||
|
||||
Remote keys should include at least:
|
||||
|
||||
- bean type
|
||||
- bean id
|
||||
- tenant id (if applicable)
|
||||
- cache/fetch-group identity
|
||||
- optionally serializer/schema version
|
||||
|
||||
Example shape:
|
||||
|
||||
- `immutable:Customer:basic:42`
|
||||
- `immutable:Customer:withAddresses:42`
|
||||
|
||||
---
|
||||
|
||||
## Recommended multi-level flow
|
||||
|
||||
### L1
|
||||
|
||||
Store actual read-only `EntityBean` instances.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- support `getIfPresent(id)`
|
||||
- avoid repeated deserialize cost
|
||||
- avoid network calls on row read path
|
||||
|
||||
### L2
|
||||
|
||||
Store serialized immutable snapshots.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- batch lookup only
|
||||
- support cross-JVM sharing
|
||||
- feed L1 with materialized immutable beans
|
||||
|
||||
### Loader
|
||||
|
||||
Use the existing query/fetch-group-based loader for misses.
|
||||
|
||||
### Suggested `getAll(ids)` flow
|
||||
|
||||
1. Check L1
|
||||
2. Batch remaining ids to L2
|
||||
3. Deserialize L2 hits into read-only beans
|
||||
4. Put those beans into L1
|
||||
5. Batch remaining misses to DB loader
|
||||
6. Freeze / ensure read-only beans
|
||||
7. Write through to L2
|
||||
8. Put into L1
|
||||
9. Negative-cache true misses if desired
|
||||
|
||||
---
|
||||
|
||||
## Invalidation is more important than serialization
|
||||
|
||||
Things to think about:
|
||||
|
||||
- update/delete invalidation across JVMs
|
||||
- local L1 invalidation when L2 entry is removed
|
||||
- ordering relative to DB commit
|
||||
- multiple cache instances for the same bean type but different fetch groups
|
||||
- tenant-scoped invalidation
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- keep current immutable-cache invalidation semantics
|
||||
- add a remote invalidation/event mechanism for L2-backed caches
|
||||
- each JVM should evict affected L1 entries when notified
|
||||
|
||||
Examples:
|
||||
|
||||
- Redis: pub/sub or streams
|
||||
- Postgres cache table: NOTIFY/listen, polling, or invalidation table/outbox pattern
|
||||
|
||||
---
|
||||
|
||||
## Serialization format considerations
|
||||
|
||||
## JSON
|
||||
|
||||
### Pros
|
||||
|
||||
- human readable / debuggable
|
||||
- easier rolling upgrades
|
||||
- field-name based, so generally more tolerant of schema evolution
|
||||
- good fit for Redis strings or Postgres JSONB
|
||||
- easier operational debugging
|
||||
|
||||
### Cons
|
||||
|
||||
- larger payloads
|
||||
- more CPU to serialize/deserialize
|
||||
- nested graphs / enums / dates / inheritance need disciplined handling
|
||||
|
||||
## Kryo / generic binary serialization
|
||||
|
||||
### Pros
|
||||
|
||||
- smaller payloads
|
||||
- often faster than JSON
|
||||
- can preserve object graphs efficiently
|
||||
|
||||
### Cons
|
||||
|
||||
- more fragile across versions and rolling deploys
|
||||
- class registration / compatibility pain
|
||||
- harder to inspect/debug
|
||||
- tighter coupling to JVM/class layout
|
||||
- riskier for long-lived shared cache entries
|
||||
|
||||
## Recommendation
|
||||
|
||||
For a first remote/shared implementation:
|
||||
|
||||
- prefer **JSON** or another self-describing structured format
|
||||
- if a binary format is later needed, prefer a stable schema-based format over generic object-graph serialization
|
||||
- **do not start with Kryo** unless short-lived entries and tight deployment coordination are acceptable
|
||||
|
||||
---
|
||||
|
||||
## What to serialize
|
||||
|
||||
Avoid thinking in terms of serializing arbitrary live entity bean graphs directly.
|
||||
|
||||
A cleaner model is:
|
||||
|
||||
- serialize a **snapshot representation**
|
||||
- deserialize into a fresh entity bean
|
||||
- mark loaded properties appropriately
|
||||
- freeze / ensure read-only state
|
||||
- store the resulting materialized bean in L1
|
||||
|
||||
This gives more control over:
|
||||
|
||||
- loaded-property semantics
|
||||
- read-only state
|
||||
- subtype handling
|
||||
- schema/version evolution
|
||||
|
||||
## Practical recommendation
|
||||
|
||||
Remote cache entries should represent exactly the configured fetch-group snapshot.
|
||||
|
||||
That means:
|
||||
|
||||
- cache what the fetch group loaded
|
||||
- include nested associations loaded by that fetch group
|
||||
- treat it as a self-contained immutable snapshot
|
||||
|
||||
This is simpler than trying to normalize the graph into many remote cache fragments and re-link it later.
|
||||
|
||||
---
|
||||
|
||||
## Redis vs Postgres cache table
|
||||
|
||||
## Redis
|
||||
|
||||
### Good for
|
||||
|
||||
- low latency
|
||||
- batch lookup via MGET / pipelining
|
||||
- TTL/eviction support
|
||||
- natural shared-cache use case
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- extra infrastructure
|
||||
- memory cost
|
||||
- invalidation/event coordination still required
|
||||
|
||||
## Postgres cache table (including unlogged-style approach)
|
||||
|
||||
### Good for
|
||||
|
||||
- simpler ops if Postgres is already present
|
||||
- easy batch lookup with `IN (...)`
|
||||
- fewer moving parts than introducing Redis
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- slower than Redis for hot shared-cache usage
|
||||
- adds pressure to Postgres
|
||||
- TTL/cleanup becomes application responsibility
|
||||
- still network/database I/O, so should remain off the `getIfPresent()` hot path
|
||||
|
||||
## Recommendation
|
||||
|
||||
- if the goal is a serious shared L2 cache, Redis is the more natural fit
|
||||
- if the goal is pragmatic shared caching with minimal extra infrastructure, Postgres can work but should still be treated as L2-only
|
||||
|
||||
---
|
||||
|
||||
## Versioning / evolution
|
||||
|
||||
Whatever serializer is used, include versioning information.
|
||||
|
||||
Useful dimensions:
|
||||
|
||||
- serializer/schema version
|
||||
- cache implementation version
|
||||
- fetch-group/cache identity version
|
||||
|
||||
This helps when:
|
||||
|
||||
- fields are added/removed
|
||||
- graph shape changes
|
||||
- fetch-group definitions evolve
|
||||
|
||||
---
|
||||
|
||||
## Compression
|
||||
|
||||
If remote snapshots become large:
|
||||
|
||||
- compress only above a size threshold
|
||||
- avoid compressing tiny payloads
|
||||
|
||||
This is especially relevant for JSON in Redis or Postgres L2.
|
||||
|
||||
---
|
||||
|
||||
## Observability
|
||||
|
||||
A multi-level cache should expose at least:
|
||||
|
||||
- L1 hit rate
|
||||
- L2 hit rate
|
||||
- DB loader rate
|
||||
- deserialize failures
|
||||
- invalidation counts
|
||||
- average payload size
|
||||
- cold-start amplification
|
||||
|
||||
Without this, it will be hard to judge whether the remote cache is helping.
|
||||
|
||||
---
|
||||
|
||||
## Overall recommended architecture
|
||||
|
||||
### Recommended model
|
||||
|
||||
- **L1**: actual read-only `EntityBean` instances
|
||||
- **L2**: serialized immutable snapshots
|
||||
- **Loader**: fetch-group-based DB query
|
||||
|
||||
### Method responsibilities
|
||||
|
||||
- `getIfPresent(id)` => **L1 only**
|
||||
- `getAll(ids)` => **L1 + L2 + DB loader** in batches
|
||||
|
||||
This aligns well with the current `AssocOneHelp` optimization and keeps the row-read path fast.
|
||||
|
||||
---
|
||||
|
||||
## Bottom line
|
||||
|
||||
If/when multi-level immutable caching is explored, the main points to preserve are:
|
||||
|
||||
1. keep `getIfPresent()` local-only
|
||||
2. do remote work only in batched `getAll()`
|
||||
3. key by type + id + tenant + fetch-group/cache identity
|
||||
4. treat remote values as immutable snapshots
|
||||
5. prefer JSON/self-describing format first
|
||||
6. be cautious with generic binary serializers like Kryo
|
||||
|
||||
---
|
||||
|
||||
## Possible follow-up
|
||||
|
||||
If this becomes active design work later, consider promoting these notes into one of:
|
||||
|
||||
- a dedicated design note under `docs/notes/`
|
||||
- a GitHub issue / discussion for design iteration
|
||||
- a lightweight ADR if this becomes a committed architectural direction
|
||||
+6
-34
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>13.21.0</version>
|
||||
<version>16.10.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean api</name>
|
||||
@@ -26,25 +26,21 @@
|
||||
<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.4</version>
|
||||
<version>4.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>persistence-api</artifactId>
|
||||
<artifactId>jakarta-persistence-api</artifactId>
|
||||
<version>${ebean-persistence-api.version}</version>
|
||||
</dependency>
|
||||
|
||||
@@ -90,30 +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 -->
|
||||
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>1.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import javax.persistence.PessimisticLockException;
|
||||
import jakarta.persistence.PessimisticLockException;
|
||||
|
||||
/**
|
||||
* Thrown when failing to acquire a pessimistic lock.
|
||||
|
||||
@@ -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,14 +1,14 @@
|
||||
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;
|
||||
import io.ebean.text.json.JsonContext;
|
||||
|
||||
import javax.persistence.OptimisticLockException;
|
||||
import javax.persistence.PersistenceException;
|
||||
import jakarta.persistence.OptimisticLockException;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -57,7 +57,7 @@ import java.util.concurrent.Callable;
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public final class DB {
|
||||
|
||||
private static final DbContext context = DbContext.getInstance();
|
||||
@@ -266,49 +266,6 @@ public final class DB {
|
||||
getDefault().register(transactionCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the current transaction.
|
||||
*/
|
||||
public static void commitTransaction() {
|
||||
getDefault().commitTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback the current transaction.
|
||||
*/
|
||||
public static void rollbackTransaction() {
|
||||
getDefault().rollbackTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the current transaction has already been committed do nothing otherwise
|
||||
* rollback the transaction.
|
||||
* <p>
|
||||
* It is preferable to use <em>try with resources</em> rather than this.
|
||||
* <p>
|
||||
* Useful to put in a finally block to ensure the transaction is ended, rather
|
||||
* than a rollbackTransaction() in each catch block.
|
||||
* <p>
|
||||
* Code example:
|
||||
*
|
||||
* <pre>{@code
|
||||
* DB.beginTransaction();
|
||||
* try {
|
||||
* // do some fetching and or persisting
|
||||
*
|
||||
* // commit at the end
|
||||
* DB.commitTransaction();
|
||||
*
|
||||
* } finally {
|
||||
* // if commit didn't occur then rollback the transaction
|
||||
* DB.endTransaction();
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
public static void endTransaction() {
|
||||
getDefault().endTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the current transaction as rollback only.
|
||||
*/
|
||||
@@ -648,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,104 @@
|
||||
package io.ebean;
|
||||
|
||||
final class DInsertOptionsBuilder implements InsertOptions.Builder {
|
||||
|
||||
private Boolean getGeneratedKeys;
|
||||
private boolean onConflictUpdate;
|
||||
private boolean onConflictNothing;
|
||||
private String constraint;
|
||||
private String uniqueColumns;
|
||||
private String updateSet;
|
||||
|
||||
@Override
|
||||
public InsertOptions.Builder onConflictNothing() {
|
||||
this.onConflictNothing = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsertOptions.Builder onConflictUpdate() {
|
||||
this.onConflictUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsertOptions.Builder constraint(String constraint) {
|
||||
this.constraint = constraint;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsertOptions.Builder uniqueColumns(String uniqueColumns) {
|
||||
this.uniqueColumns = uniqueColumns;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsertOptions.Builder updateSet(String updateSet) {
|
||||
this.updateSet = updateSet;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsertOptions.Builder getGeneratedKeys(boolean getGeneratedKeys) {
|
||||
this.getGeneratedKeys = getGeneratedKeys;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsertOptions build() {
|
||||
return new Options(constraint, uniqueColumns, updateSet, onConflictUpdate, onConflictNothing, getGeneratedKeys);
|
||||
}
|
||||
|
||||
static final class Options implements InsertOptions {
|
||||
|
||||
private static final String UPDATE = "U";
|
||||
private static final String NOTHING = "N";
|
||||
private static final String NORMAL = "_";
|
||||
private final String key;
|
||||
private final Boolean getGeneratedKeys;
|
||||
private final String constraint;
|
||||
private final String uniqueColumns;
|
||||
private final String updateSet;
|
||||
|
||||
Options(String constraint, String uniqueColumns, String updateSet, boolean onConflictUpdate, boolean onConflictNothing, Boolean getGeneratedKeys) {
|
||||
this.constraint = constraint;
|
||||
this.uniqueColumns = uniqueColumns;
|
||||
this.updateSet = updateSet;
|
||||
this.getGeneratedKeys = getGeneratedKeys;
|
||||
this.key = (onConflictUpdate ? UPDATE : onConflictNothing ? NOTHING : NORMAL)
|
||||
+ '+' + plus(constraint)
|
||||
+ '+' + plus(uniqueColumns)
|
||||
+ '+' + plus(updateSet);
|
||||
}
|
||||
|
||||
private String plus(String val) {
|
||||
return val == null ? "" : val;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String constraint() {
|
||||
return constraint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uniqueColumns() {
|
||||
return uniqueColumns;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String updateSet() {
|
||||
return updateSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getGetGeneratedKeys() {
|
||||
return getGeneratedKeys;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,9 +1,9 @@
|
||||
package io.ebean;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
/**
|
||||
* Thrown when a foreign key constraint is enforced.
|
||||
* Thrown when a foreign key constraint is enforced or a field is too large.
|
||||
*/
|
||||
public class DataIntegrityException extends PersistenceException {
|
||||
private static final long serialVersionUID = -6740171949170180970L;
|
||||
@@ -14,4 +14,11 @@ public class DataIntegrityException extends PersistenceException {
|
||||
public DataIntegrityException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create with message only.
|
||||
*/
|
||||
public DataIntegrityException(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 io.ebean.annotation.Platform;
|
||||
import io.ebean.annotation.TxIsolation;
|
||||
import io.ebean.cache.ServerCacheManager;
|
||||
@@ -11,8 +11,8 @@ import io.ebean.plugin.Property;
|
||||
import io.ebean.plugin.SpiServer;
|
||||
import io.ebean.text.json.JsonContext;
|
||||
|
||||
import javax.persistence.OptimisticLockException;
|
||||
import javax.persistence.PersistenceException;
|
||||
import jakarta.persistence.OptimisticLockException;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import javax.sql.DataSource;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -23,11 +23,18 @@ import java.util.concurrent.Callable;
|
||||
/**
|
||||
* Provides the API for fetching and saving beans to a particular database.
|
||||
*
|
||||
* <h5>Constructing a Database</h5>
|
||||
* <p>
|
||||
* Databases are typically constructed via {@link #builder()} and {@link DatabaseBuilder#build()}.
|
||||
* They can also be automatically constructed on demand using configuration information in
|
||||
* the application.properties file. The underlying implementation is provided by
|
||||
* {@link DatabaseFactory}.
|
||||
*
|
||||
* <h5>Registration with the DB singleton</h5>
|
||||
* <p>
|
||||
* When a Database instance is created it can be registered with the DB
|
||||
* singleton (see {@link DatabaseConfig#setRegister(boolean)}). The DB
|
||||
* singleton is essentially a map of Database's that have been registered
|
||||
* When a Database instance is created it can be registered with the {@link DB}
|
||||
* singleton (see {@link DatabaseBuilder#register(boolean)}). The {@link DB}
|
||||
* singleton is essentially a map of {@link Database}'s that have been registered
|
||||
* with it.
|
||||
* <p>
|
||||
* The Database can then be retrieved later via {@link DB#byName(String)}.
|
||||
@@ -35,16 +42,10 @@ import java.util.concurrent.Callable;
|
||||
* <h5>The 'default' Database</h5>
|
||||
* <p>
|
||||
* One Database can be designated as the 'default' or 'primary' Database
|
||||
* (see {@link DatabaseConfig#setDefaultServer(boolean)}). Many methods on DB
|
||||
* (see {@link DatabaseBuilder#defaultDatabase(boolean)}). Many methods on {@link DB}
|
||||
* such as {@link DB#find(Class)} etc are actually just a convenient way to
|
||||
* call methods on the 'default/primary' Database.
|
||||
*
|
||||
* <h5>Constructing a Database</h5>
|
||||
* <p>
|
||||
* Databases are constructed by the DatabaseFactory. They can be created
|
||||
* programmatically via {@link DatabaseFactory#create(DatabaseConfig)} or they
|
||||
* can be automatically constructed on demand using configuration information in
|
||||
* the application.properties file.
|
||||
*
|
||||
* <h5>Example: Get a Database</h5>
|
||||
* <pre>{@code
|
||||
@@ -80,12 +81,32 @@ import java.util.concurrent.Callable;
|
||||
* method. Example: a single thread requires more than one transaction.
|
||||
*
|
||||
* @see DB
|
||||
* @see DatabaseBuilder
|
||||
* @see DatabaseFactory
|
||||
* @see DatabaseConfig
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public interface Database {
|
||||
|
||||
/**
|
||||
* Return a new database builder.
|
||||
* <pre>{@code
|
||||
*
|
||||
* // build the 'default' database using configuration
|
||||
* // from application.properties / application.yaml
|
||||
*
|
||||
* Database db = Database.builder()
|
||||
* .name("db")
|
||||
* .loadFromProperties()
|
||||
* .build();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
static DatabaseBuilder builder() {
|
||||
return new DatabaseConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the Database instance.
|
||||
*/
|
||||
@@ -120,6 +141,7 @@ public interface Database {
|
||||
/**
|
||||
* Return the associated read only DataSource for this Database instance (can be null).
|
||||
*/
|
||||
@Nullable
|
||||
DataSource readOnlyDataSource();
|
||||
|
||||
/**
|
||||
@@ -383,7 +405,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
|
||||
@@ -649,43 +671,6 @@ public interface Database {
|
||||
*/
|
||||
void flush();
|
||||
|
||||
/**
|
||||
* Commit the current transaction.
|
||||
*/
|
||||
void commitTransaction();
|
||||
|
||||
/**
|
||||
* Rollback the current transaction.
|
||||
*/
|
||||
void rollbackTransaction();
|
||||
|
||||
/**
|
||||
* If the current transaction has already been committed do nothing otherwise
|
||||
* rollback the transaction.
|
||||
* <p>
|
||||
* Useful to put in a finally block to ensure the transaction is ended, rather
|
||||
* than a rollbackTransaction() in each catch block.
|
||||
* <p>
|
||||
* Code example:
|
||||
* <p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* database.beginTransaction();
|
||||
* try {
|
||||
* // do some fetching and or persisting ...
|
||||
*
|
||||
* // commit at the end
|
||||
* database.commitTransaction();
|
||||
*
|
||||
* } finally {
|
||||
* // if commit didn't occur then rollback the transaction
|
||||
* database.endTransaction();
|
||||
* }
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
void endTransaction();
|
||||
|
||||
/**
|
||||
* Refresh the values of a bean.
|
||||
* <p>
|
||||
@@ -792,18 +777,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>
|
||||
@@ -1204,22 +1177,53 @@ public interface Database {
|
||||
*/
|
||||
void insert(Object bean);
|
||||
|
||||
/**
|
||||
* Insert the bean with options (ON CONFLICT DO UPDATE | DO NOTHING).
|
||||
* <p>
|
||||
* Currently, this is limited to use with Postgres only,
|
||||
* <p>
|
||||
* When using this ebean will look to determine the unique columns by looking at
|
||||
* the mapping like {@code @Column(unique=true} and {@code @Index(unique=true}.
|
||||
*/
|
||||
void insert(Object bean, InsertOptions insertOptions);
|
||||
|
||||
/**
|
||||
* Insert the bean with a transaction.
|
||||
*/
|
||||
void insert(Object bean, Transaction transaction);
|
||||
|
||||
/**
|
||||
* Insert the beans with options (ON CONFLICT DO UPDATE | DO NOTHING) and transaction.
|
||||
* <p>
|
||||
* Currently, this is limited to use with Postgres only,
|
||||
*/
|
||||
void insert(Object bean, InsertOptions insertOptions, Transaction transaction);
|
||||
|
||||
/**
|
||||
* Insert a collection of beans. If there is no current transaction one is created and used to
|
||||
* insert all the beans in the collection.
|
||||
*/
|
||||
void insertAll(Collection<?> beans);
|
||||
|
||||
/**
|
||||
* Insert the beans with options - typically ON CONFLICT DO UPDATE | DO NOTHING.
|
||||
* <p>
|
||||
* Currently, this is limited to use with Postgres only,
|
||||
*/
|
||||
void insertAll(Collection<?> beans, InsertOptions options);
|
||||
|
||||
/**
|
||||
* Insert a collection of beans with an explicit transaction.
|
||||
*/
|
||||
void insertAll(Collection<?> beans, Transaction transaction);
|
||||
|
||||
/**
|
||||
* Insert the beans with options (ON CONFLICT DO UPDATE | DO NOTHING) and transaction.
|
||||
* <p>
|
||||
* Currently, this is limited to use with Postgres only,
|
||||
*/
|
||||
void insertAll(Collection<?> beans, InsertOptions options, Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute explicitly passing a transaction.
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,25 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.ebean.config.ContainerConfig;
|
||||
import io.ebean.config.DatabaseConfig;
|
||||
import io.ebean.service.SpiContainer;
|
||||
import io.ebean.service.SpiContainerFactory;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Properties;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Creates Database instances.
|
||||
* Low-level factory for creating {@link Database} instances.
|
||||
* <p>
|
||||
* This uses either DatabaseConfig or properties in the application.properties file to
|
||||
* configure and create a Database instance.
|
||||
* Most applications should prefer {@link Database#builder()} together with {@link DatabaseBuilder#build()}.
|
||||
* This factory remains for legacy creation entry points plus container lifecycle methods.
|
||||
* <p>
|
||||
* The Database instance can either be registered with the DB singleton or
|
||||
* not. The DB singleton effectively holds a map of Database by a name.
|
||||
* If the Database is registered with the DB singleton you can retrieve it
|
||||
* The Database instance can either be registered with the {@link DB} singleton or
|
||||
* not. The {@link DB} singleton effectively holds a map of {@link Database} by name.
|
||||
* If the Database is registered with the {@link DB} singleton you can retrieve it
|
||||
* later via {@link DB#byName(String)}.
|
||||
* <p>
|
||||
* One Database can be nominated as the 'default/primary' Database. Many
|
||||
* methods on the DB singleton such as {@link DB#find(Class)} are just a
|
||||
* methods on the {@link DB} singleton such as {@link DB#find(Class)} are just a
|
||||
* convenient way of using the 'default/primary' Database.
|
||||
*/
|
||||
public final class DatabaseFactory {
|
||||
@@ -40,7 +36,8 @@ public final class DatabaseFactory {
|
||||
* Initialise the container with clustering configuration.
|
||||
* <p>
|
||||
* Call this prior to creating any Database instances or alternatively set the
|
||||
* ContainerConfig on the DatabaseConfig when creating the first Database instance.
|
||||
* {@link ContainerConfig} on the first {@link DatabaseBuilder} via
|
||||
* {@link DatabaseBuilder#containerConfig(ContainerConfig)}.
|
||||
*/
|
||||
public static void initialiseContainer(ContainerConfig containerConfig) {
|
||||
lock.lock();
|
||||
@@ -52,8 +49,11 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using properties to configure the database.
|
||||
* Create using configuration loaded from properties for the given database name.
|
||||
*
|
||||
* @deprecated migrate to {@code Database.builder().name(name).loadFromProperties().build()}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static Database create(String name) {
|
||||
lock.lock();
|
||||
try {
|
||||
@@ -64,21 +64,13 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using the DatabaseConfig object to configure the database.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* DatabaseConfig config = new DatabaseConfig();
|
||||
* config.setName("db");
|
||||
* config.loadProperties();
|
||||
*
|
||||
* Database database = DatabaseFactory.create(config);
|
||||
*
|
||||
* }</pre>
|
||||
* @deprecated migrate to {@link DatabaseBuilder#build()}.
|
||||
*/
|
||||
public static Database create(DatabaseConfig config) {
|
||||
@Deprecated(forRemoval = true)
|
||||
public static Database create(DatabaseBuilder builder) {
|
||||
lock.lock();
|
||||
try {
|
||||
var config = builder.settings();
|
||||
if (config.getName() == null) {
|
||||
throw new PersistenceException("The name is null (it is required)");
|
||||
}
|
||||
@@ -100,9 +92,10 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using the DatabaseConfig additionally specifying a classLoader to use as the context class loader.
|
||||
* Create using the {@link DatabaseBuilder}, additionally specifying a classLoader to use as the
|
||||
* context class loader.
|
||||
*/
|
||||
public static Database createWithContextClassLoader(DatabaseConfig config, ClassLoader classLoader) {
|
||||
public static Database createWithContextClassLoader(DatabaseBuilder config, ClassLoader classLoader) {
|
||||
lock.lock();
|
||||
try {
|
||||
ClassLoader currentContextLoader = Thread.currentThread().getContextClassLoader();
|
||||
@@ -132,7 +125,7 @@ public final class DatabaseFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private static Database createInternal(DatabaseConfig config) {
|
||||
private static Database createInternal(DatabaseBuilder.Settings config) {
|
||||
return container(config.getContainerConfig()).createServer(config);
|
||||
}
|
||||
|
||||
@@ -146,12 +139,9 @@ public final class DatabaseFactory {
|
||||
if (container != null) {
|
||||
return container;
|
||||
}
|
||||
|
||||
if (containerConfig == null) {
|
||||
// effectively load configuration from ebean.properties
|
||||
Properties properties = DbPrimary.getProperties();
|
||||
containerConfig = new ContainerConfig();
|
||||
containerConfig.loadFromProperties(properties);
|
||||
}
|
||||
container = createContainer(containerConfig);
|
||||
return container;
|
||||
@@ -160,11 +150,11 @@ public final class DatabaseFactory {
|
||||
/**
|
||||
* Create the container instance using the configuration.
|
||||
*/
|
||||
protected static SpiContainer createContainer(ContainerConfig containerConfig) {
|
||||
Iterator<SpiContainerFactory> factories = ServiceLoader.load(SpiContainerFactory.class).iterator();
|
||||
if (factories.hasNext()) {
|
||||
return factories.next().create(containerConfig);
|
||||
private static SpiContainer createContainer(ContainerConfig 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package io.ebean;
|
||||
import io.ebean.config.BeanNotEnhancedException;
|
||||
import io.ebean.datasource.DataSourceConfigurationException;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -92,6 +92,7 @@ final class DbContext {
|
||||
/**
|
||||
* Read, create and put of Databases.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
private Database getWithCreate(String name) {
|
||||
lock.lock();
|
||||
try {
|
||||
|
||||
@@ -44,25 +44,12 @@ final class DbPrimary {
|
||||
* Return the default database name.
|
||||
*/
|
||||
static String getDefaultServerName() {
|
||||
lock.lock();
|
||||
try {
|
||||
getProperties();
|
||||
return defaultServerName;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default configuration Properties.
|
||||
*/
|
||||
static Properties getProperties() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (defaultServerName == null) {
|
||||
defaultServerName = determineDefaultServerName();
|
||||
}
|
||||
return Config.asProperties();
|
||||
return defaultServerName;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
@@ -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,8 +1,10 @@
|
||||
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;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -40,7 +42,7 @@ import java.util.stream.Stream;
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@NonNullApi
|
||||
@NullMarked
|
||||
public interface DtoQuery<T> extends CancelableQuery {
|
||||
|
||||
/**
|
||||
@@ -138,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);
|
||||
|
||||
@@ -206,15 +219,31 @@ public interface DtoQuery<T> extends CancelableQuery {
|
||||
*/
|
||||
DtoQuery<T> usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
DtoQuery<T> usingConnection(Connection connection);
|
||||
|
||||
/**
|
||||
* Ensure that the master DataSource is used if there is a read only data source
|
||||
* being used (that is using a read replica database potentially with replication lag).
|
||||
* <p>
|
||||
* When the database is configured with a read-only DataSource via
|
||||
* say {@link io.ebean.config.DatabaseConfig#setReadOnlyDataSource(DataSource)} then
|
||||
* say {@link io.ebean.DatabaseBuilder#readOnlyDataSource(DataSource)} then
|
||||
* by default when a query is run without an active transaction, it uses the read-only data
|
||||
* source. We we use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* 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() {
|
||||
|
||||
@@ -36,6 +36,11 @@ import java.util.Map;
|
||||
*/
|
||||
public interface ExpressionFactory {
|
||||
|
||||
/**
|
||||
* Return a new ExpressionList.
|
||||
*/
|
||||
<T> ExpressionList<T> expressionList();
|
||||
|
||||
/**
|
||||
* Path exists - for the given path in a JSON document.
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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 javax.persistence.NonUniqueResultException;
|
||||
import jakarta.persistence.NonUniqueResultException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.*;
|
||||
@@ -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")
|
||||
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 orderBy().
|
||||
*/
|
||||
@Deprecated
|
||||
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();
|
||||
*
|
||||
@@ -508,6 +472,8 @@ public interface ExpressionList<T> {
|
||||
ExpressionList<T> filterMany(String manyProperty);
|
||||
|
||||
/**
|
||||
* @deprecated for removal - migrate to {@link #filterManyRaw(String, String, Object...)}.
|
||||
* <p>
|
||||
* Add filter expressions to the many property.
|
||||
*
|
||||
* <pre>{@code
|
||||
@@ -524,8 +490,29 @@ public interface ExpressionList<T> {
|
||||
* @param expressions Filter expressions with and, or and ? or ?1 type bind parameters
|
||||
* @param params Bind parameters used in the expressions
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
ExpressionList<T> filterMany(String manyProperty, String expressions, Object... params);
|
||||
|
||||
/**
|
||||
* Add filter expressions for the many path. The expressions can include SQL functions if
|
||||
* desired and the property names are translated to column names.
|
||||
* <p>
|
||||
* The expressions can contain placeholders for bind values using <code>?</code> or <code>?1</code> style.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* new QCustomer()
|
||||
* .name.startsWith("Postgres")
|
||||
* .contacts.filterManyRaw("status = ? and firstName like ?", Contact.Status.NEW, "Rob%")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param rawExpressions The raw expressions which can include ? and ?1 style bind parameter placeholders
|
||||
* @param params The parameter values to bind
|
||||
*/
|
||||
ExpressionList<T> filterManyRaw(String manyProperty, String rawExpressions, Object... params);
|
||||
|
||||
/**
|
||||
* Specify specific properties to fetch on the main/root bean (aka partial
|
||||
* object).
|
||||
@@ -1086,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
|
||||
@@ -1093,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.
|
||||
*/
|
||||
@@ -1120,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.
|
||||
*/
|
||||
@@ -1684,7 +1719,7 @@ public interface ExpressionList<T> {
|
||||
* .eq("status", Customer.Status.ACTIVE)
|
||||
* .gt("id", 0)
|
||||
* .endAnd()
|
||||
* .order().asc("name")
|
||||
* .orderBy().asc("name")
|
||||
* .findList();
|
||||
* }</pre>
|
||||
*/
|
||||
@@ -1705,7 +1740,7 @@ public interface ExpressionList<T> {
|
||||
* .or()
|
||||
* .eq("status", Customer.Status.ACTIVE)
|
||||
* .isNull("anniversary")
|
||||
* .order().asc("name")
|
||||
* .orderBy().asc("name")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
@@ -1725,7 +1760,7 @@ public interface ExpressionList<T> {
|
||||
* .eq("status", Customer.Status.ACTIVE)
|
||||
* .gt("id", 0)
|
||||
* .endAnd()
|
||||
* .order().asc("name")
|
||||
* .orderBy().asc("name")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
@@ -1759,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> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Options to be used with insert such as ON CONFLICT DO UPDATE | NOTHING.
|
||||
*/
|
||||
public interface InsertOptions {
|
||||
|
||||
/**
|
||||
* Use ON CONFLICT UPDATE with automatic determination of the unique columns to conflict on.
|
||||
* <p>
|
||||
* Uses mapping to determine the unique columns - {@code @Column(unique=true)} and {@code @Index(unique=true)} .
|
||||
*/
|
||||
InsertOptions ON_CONFLICT_UPDATE = InsertOptions.builder()
|
||||
.onConflictUpdate()
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Use ON CONFLICT DO NOTHING with automatic determination of the unique columns to conflict on.
|
||||
* <p>
|
||||
* Uses mapping to determine the unique columns - {@code @Column(unique=true)} and {@code @Index(unique=true)} .
|
||||
*/
|
||||
InsertOptions ON_CONFLICT_NOTHING = InsertOptions.builder()
|
||||
.onConflictNothing()
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Return a builder for InsertOptions.
|
||||
*/
|
||||
static Builder builder() {
|
||||
return new DInsertOptionsBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the constraint name that is used for ON CONFLICT.
|
||||
*/
|
||||
@Nullable
|
||||
String constraint();
|
||||
|
||||
/**
|
||||
* Return the unique columns that is used for ON CONFLICT.
|
||||
* <p>
|
||||
* When not explicitly set will use mapping like {@code @Column(unique=true)} to determine the
|
||||
* non-unique columns.
|
||||
*/
|
||||
@Nullable
|
||||
String uniqueColumns();
|
||||
|
||||
/**
|
||||
* Return the ON CONFLICT UPDATE SET clause.
|
||||
* <p>
|
||||
* When not set will use the non-unique columns.
|
||||
*/
|
||||
@Nullable
|
||||
String updateSet();
|
||||
|
||||
/**
|
||||
* Return if GetGeneratedKeys should be used to fetch the generated keys after insert.
|
||||
*/
|
||||
@Nullable
|
||||
Boolean getGetGeneratedKeys();
|
||||
|
||||
/**
|
||||
* Return the key for these build options.
|
||||
*/
|
||||
String key();
|
||||
|
||||
/**
|
||||
* The builder for InsertOptions.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Use a ON CONFLICT UPDATE automatically determining the unique columns.
|
||||
*/
|
||||
Builder onConflictUpdate();
|
||||
|
||||
/**
|
||||
* Use a ON CONFLICT DO NOTHING automatically determining the unique columns.
|
||||
*/
|
||||
Builder onConflictNothing();
|
||||
|
||||
/**
|
||||
* Specify an explicit conflict constraint name.
|
||||
* <p>
|
||||
* When this is used then unique columns will not be used.
|
||||
*/
|
||||
Builder constraint(String constraint);
|
||||
|
||||
/**
|
||||
* Specify the unique columns for the conflict target.
|
||||
* <p>
|
||||
* When not specified and constraint is also not specified then
|
||||
* it will automatically determine the unique columns
|
||||
* based on mapping like {@code @Column(unique=true)} and
|
||||
* {@code @Index(unique=true)} .
|
||||
*/
|
||||
Builder uniqueColumns(String uniqueColumns);
|
||||
|
||||
/**
|
||||
* Specify the ON CONFLICT DO UPDATE SET clause.
|
||||
* <p>
|
||||
* When not specified ebean will include all the non-unique columns.
|
||||
*/
|
||||
Builder updateSet(String updateSet);
|
||||
|
||||
/**
|
||||
* Specify if GetGeneratedKeys should be used to return generated keys.
|
||||
*/
|
||||
Builder getGeneratedKeys(boolean getGeneratedKeys);
|
||||
|
||||
/**
|
||||
* Build and return the insert options.
|
||||
*/
|
||||
InsertOptions build();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -24,7 +25,9 @@ public final class Lists {
|
||||
*/
|
||||
public static <T> List<List<T>> partition(List<T> source, int max) {
|
||||
final int totalCount = source.size();
|
||||
if (totalCount <= max) {
|
||||
if (totalCount == 0) {
|
||||
return Collections.emptyList();
|
||||
} else if (totalCount <= max) {
|
||||
return List.of(source);
|
||||
}
|
||||
final int numOfPartitions = (totalCount + max - 1) / max; // round up
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
/**
|
||||
* Captures and wraps IOException's occurring during ElasticSearch processing etc.
|
||||
|
||||
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user