Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/dto-mapping-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,8 @@ the chain.

### mapTo(Dto.class) runtime wiring (implemented)

`query.mapTo(dtoType)` returns a `MappedQuery<D>` (`findList()`/`findOne()`/`findOneOrEmpty()`).
`query.mapTo(dtoType)` returns a `MappedQuery<D>` (`findList()`/`findOne()`/`findOneOrEmpty()`/
`findPagedList()`/`usingMaster(boolean)`/`usingTransaction(Transaction)`/`usingConnection(Connection)`).
On first use it resolves the generated `DtoMapper<S, D>` for the query's `(getBeanType(), dtoType)`
pair via a `DtoMapperManager` (a `ServiceLoader`-backed aggregator over all generated
`DtoMapperRegister`s, analogous to `DtoBeanManager`), then:
Expand All @@ -592,6 +593,12 @@ pair via a `DtoMapperManager` (a `ServiceLoader`-backed aggregator over all gene
An unregistered `(source, dtoType)` pair throws a `PersistenceException` with a suggested
`@DtoMapping` fix, at first use (i.e. `findList()`/`findOne()`), not at `mapTo(dtoType)` call time.

`MappedQuery<D>.usingMaster(boolean)`, `.usingTransaction(Transaction)`, and `.usingConnection(Connection)`
all delegate directly to the underlying entity query, mirroring `Query`/`QueryBuilder`. This lets a
caller retry against the master data source after a read-replica failure by calling
`usingMaster(true)` on the *same* `MappedQuery` instance and re-invoking a find method - there's no
need to rebuild the query and call `.mapTo(...)` again.

## Still open / to revisit during implementation

- Whether `.fetch(...)` calls can still be layered on top of a `mapTo(Dto.class)` query for explicit
Expand Down
18 changes: 18 additions & 0 deletions ebean-api/src/main/java/io/ebean/MappedQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.sql.Connection;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -48,4 +49,21 @@ public interface MappedQuery<D> {
* Execute the query returning an optional mapped DTO.
*/
Optional<D> findOneOrEmpty();

/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
* data source can be used if defined.
*/
MappedQuery<D> usingMaster(boolean useMaster);

/**
* Use the explicit transaction to execute the query.
*/
MappedQuery<D> usingTransaction(Transaction transaction);

/**
* Execute the query using the given connection.
*/
MappedQuery<D> usingConnection(Connection connection);

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import io.ebean.DtoMapper;
import io.ebean.MappedQuery;
import io.ebean.PagedList;
import io.ebean.Transaction;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQuery;

import java.sql.Connection;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -91,4 +93,22 @@ public PagedList<D> findPagedList() {
DtoMapper<T, D> m = mapper();
return new MappedPagedList<>(query.findPagedList(), m);
}

@Override
public MappedQuery<D> usingMaster(boolean useMaster) {
query.usingMaster(useMaster);
return this;
}

@Override
public MappedQuery<D> usingTransaction(Transaction transaction) {
query.usingTransaction(transaction);
return this;
}

@Override
public MappedQuery<D> usingConnection(Connection connection) {
query.usingConnection(connection);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.ebean.DB;
import io.ebean.LazyInitialisationException;
import io.ebean.PagedList;
import io.ebean.Transaction;
import io.ebean.test.LoggedSql;
import jakarta.persistence.PersistenceException;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -141,6 +142,61 @@ void mapTo_whenSelectAlreadySet_expectManualFetchSpecPreserved() {
assertThat(sql.get(0)).doesNotContain("o_address");
}

@Test
void mapTo_usingMaster_expectFluentReturnAndNoError() {
Customer customer = new Customer("MasterCo");
customer.save();

// usingMaster() can be called on the MappedQuery itself - e.g. on retry after a read-replica
// failure - without needing to rebuild the underlying query and call mapTo() again
var mapped = DB.find(Customer.class)
.where().idEq(customer.getId())
.mapTo(CustomerDto.class);

assertThat(mapped.usingMaster(false)).isSameAs(mapped);
assertThat(mapped.usingMaster(true)).isSameAs(mapped);

List<CustomerDto> dtos = mapped.findList();
assertThat(dtos).hasSize(1);
assertThat(dtos.get(0).getName()).isEqualTo("MasterCo");
}

@Test
void mapTo_usingTransaction_expectQueryRunsOnExplicitTransaction() {
Customer customer = new Customer("TxnCo");
customer.save();

try (Transaction txn = DB.beginTransaction()) {
List<CustomerDto> dtos = DB.find(Customer.class)
.where().idEq(customer.getId())
.mapTo(CustomerDto.class)
.usingTransaction(txn)
.findList();

assertThat(dtos).hasSize(1);
assertThat(dtos.get(0).getName()).isEqualTo("TxnCo");
txn.commit();
}
}

@Test
void mapTo_usingConnection_expectFluentReturn() {
Customer customer = new Customer("ConnCo");
customer.save();

try (Transaction txn = DB.beginTransaction()) {
var mapped = DB.find(Customer.class)
.where().idEq(customer.getId())
.mapTo(CustomerDto.class);

assertThat(mapped.usingConnection(txn.connection())).isSameAs(mapped);

List<CustomerDto> dtos = mapped.findList();
assertThat(dtos).hasSize(1);
assertThat(dtos.get(0).getName()).isEqualTo("ConnCo");
}
}

@Test
void mapTo_withFilterMany_expectFilteredContactsInDtoGraph() {
Customer customer = new Customer("FilterManyCo");
Expand Down
Loading