-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathllms-write-transactions.txt
More file actions
457 lines (305 loc) · 12.6 KB
/
llms-write-transactions.txt
File metadata and controls
457 lines (305 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# Ebean ORM Bundle — Writes & Transactions (Flattened)
> Flattened bundle. Content from source markdown guides is inlined below.
---
## Source: `persisting-and-transactions-with-ebean.md`
# 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 `DB.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 | `DB.insert(bean)` | New-create flow, seed data, fixture setup |
| Save a bean that may be new or existing | `DB.save(bean)` | Common default when bean state determines insert vs update |
| Update a bean that is definitely existing | `DB.update(bean)` | Existing row should be updated only |
| Delete one bean | `DB.delete(bean)` | Remove a loaded entity bean |
| Update many rows without loading beans | `DB.update(...)` or `query.asUpdate()` | Set-based write, not per-row business logic |
| Delete many rows without loading beans | bulk update/delete API or `DB.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
Some codebases use `DB.save(bean)` and others use model instance methods such as
`bean.save()`. Unless the project already standardizes on model instance methods,
default to `DB.*(...)` style because it works regardless of whether entities
extend `Model`.
---
## 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");
DB.insert(customer);
```
### Example - update an existing bean
```java
Customer customer = new QCustomer()
.id.equalTo(customerId)
.findOne();
customer.setStatus(Customer.Status.ACTIVE);
DB.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
DB.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);
DB.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);
DB.save(order);
Shipment shipment = new Shipment(order, Instant.now());
DB.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 = DB.beginTransaction()) {
Order order = new QOrder()
.id.equalTo(orderId)
.findOne();
order.cancel();
DB.save(order);
AuditLog auditLog = new AuditLog("order-cancelled", orderId);
DB.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 `DB.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 `DB.update(...)`
```java
int rows = DB.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) {
DB.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)