Like other ORM frameworks, a query in Cayenne reads the database. But unlike other ORM frameworks, Cayenne only writes changes to the database upon a commit. This means that a query will not see changes made in the same transaction:
ObjectContext ctx = runtime.newContext();
// Searches for an item with id 1.
Item item = Cayenne.objectForPK(ctx, Item.class, 1);
System.out.println("" + item.getValue()); // output is 42.
// Changes the value but doesn't commit.
item.setValue(84);
// Searches in the database with new attribute value. Item is not found.
Item copy1 = ObjectSelect.query(Item.class)
.where(Item.VALUE.eq(84)).selectOne(ctx);
if (copy1 == null) System.out.println("not found"); // output: not found.
// Searches in the database with old attribute value. Item is found.
Item copy2 = ObjectSelect.query(Item.class)
.where(Item.VALUE.eq(42)).selectOne(ctx);
// But item has the new attribute values cached in the ObjectContext.
System.out.println("" + copy2.getValue()); // output is 84.
A possible but cumbersome solution would be to always query the database and the ObjectContext:
Item copy = ObjectSelect.query(Item.class)
.where(Item.VALUE.eq(84)).selectOne(ctx);
if (copy == null) copy = (Item) Item.VALUE.eq(84)
.first(new ArrayList<>(ctx.uncommittedObjects()));
System.out.println("" + copy.getValue()); // output: "84".
What is the best practice to find an object within the same transaction that may have been modified or newly created?
Edit: After the hint given by Andrus in the answer below, the solution looks like:
try {
runtime.performInTransaction(() -> {
ObjectContext ctx = runtime.newContext();
Item item = Cayenne.objectForPK(ctx, Item.class, 31);
item.setValue(84);
ctx.commitChanges();
Item copy = ObjectSelect.query(Item.class)
.where(Item.VALUE.eq(84)).selectOne(ctx);
System.out.println("" + copy.getValue()); // output: 84.
ctx.rollbackChanges();
return 0;
});
} catch (Exception e) {
// rollback causes CayenneRuntimeException
System.out.println(e.toString());
}
If the exception caused by the rollback is undesirable, you can use the following helper function:
public <T> performInTransaction(TransactionalOperation<T> op) {
Transaction t = runtime.getInjector()
.getInstance(TransactionFactory.class)
.createTransaction();
BaseTransaction.bindThreadTransaction(t);
try {
TransactionDescriptor td = TransactionDescriptor.builder()
.propagation(TransactionPropagation.MANDATORY).build();
T result = runtime.performInTransaction(op, td);
BaseTransaction.bindThreadTransaction(null);
if (t.isRollbackOnly()) t.rollback(); else t.commit();
return result;
} catch (Exception e) {
t.rollback();
throw e;
}
}
Cayenne only writes changes to the database upon a commit.
Correct, but bear in mind that Cayenne does flush and commit steps as a single operation by default to make things easier and avoid explicit transaction management in 99% of cases. But it allows you to split the two if you need to.
This means that a query will not see changes made in the same transaction
The issue is that your query will not be in the same transaction by default. Here is how you combine multiple commits and queries in one transaction:
runtime.performInTransaction(() -> {
...
ctx.commitChanges();
...
ObjectSelect.query(Item.class).selectOne(ctx);
...
ctx.commitChanges();
...
});