mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-10-21 11:09:17 +00:00
Merge pull request #3859 from ebean-orm/feature/findOneOrThrow
enh: Add findOneOrThrow() with decent derived message for EntityNotFo…
This commit is contained in:
@@ -3,6 +3,7 @@ package io.ebean;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.util.Collection;
|
||||
@@ -10,6 +11,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
@@ -109,6 +111,22 @@ public interface DtoQuery<T> extends CancelableQuery {
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing a
|
||||
* {@link jakarta.persistence.EntityNotFoundException} if there is no matching row.
|
||||
*/
|
||||
default T findOneOrThrow() {
|
||||
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException("Not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing the exception produced
|
||||
* by the given supplier if there is no matching row.
|
||||
*/
|
||||
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
||||
return findOneOrEmpty().orElseThrow(exceptionSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind all the parameters using index positions.
|
||||
* <p>
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.sql.Timestamp;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* List of Expressions that make up a where or having clause.
|
||||
@@ -421,6 +422,26 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing a {@link jakarta.persistence.EntityNotFoundException}
|
||||
* if there is no matching bean.
|
||||
*
|
||||
* @see Query#findOneOrThrow()
|
||||
*/
|
||||
default T findOneOrThrow() {
|
||||
return query().findOneOrThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing the exception produced by the
|
||||
* given supplier if there is no matching bean.
|
||||
*
|
||||
* @see Query#findOneOrThrow(Supplier)
|
||||
*/
|
||||
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
||||
return query().findOneOrThrow(exceptionSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute find row count query in a background thread.
|
||||
* <p>
|
||||
|
||||
@@ -3,9 +3,11 @@ package io.ebean;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
@@ -73,6 +75,26 @@ public interface MappedQuery<D> {
|
||||
*/
|
||||
Optional<D> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single mapped DTO or throwing a
|
||||
* {@link jakarta.persistence.EntityNotFoundException} if there is no matching row.
|
||||
* <p>
|
||||
* The exception message reflects the underlying entity type and its id or single
|
||||
* equality predicate (a likely natural/unique key) when the query is that simple,
|
||||
* otherwise a generic "not found" message.
|
||||
*/
|
||||
default D findOneOrThrow() {
|
||||
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException("Not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query returning a single mapped DTO or throwing the exception produced
|
||||
* by the given supplier if there is no matching row.
|
||||
*/
|
||||
default D findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
||||
return findOneOrEmpty().orElseThrow(exceptionSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Timestamp;
|
||||
@@ -12,6 +13,7 @@ import java.util.Set;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
@@ -723,6 +725,37 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing a {@link jakarta.persistence.EntityNotFoundException}
|
||||
* if there is no matching bean.
|
||||
* <p>
|
||||
* This is a convenience alternative to:
|
||||
* <pre>{@code
|
||||
* query.findOneOrEmpty()
|
||||
* .orElseThrow(() -> new EntityNotFoundException(...));
|
||||
* }</pre>
|
||||
* <p>
|
||||
* The exception message is a best effort - it uses the id when this is effectively a
|
||||
* find-by-id query, or the single equality predicate when the query is filtered by what
|
||||
* looks like a natural/unique key, otherwise a generic "not found" message.
|
||||
*/
|
||||
default T findOneOrThrow() {
|
||||
return findOneOrEmpty().orElseThrow(() ->
|
||||
new EntityNotFoundException(getBeanType().getSimpleName() + " not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing the exception produced by the
|
||||
* given supplier if there is no matching bean.
|
||||
* <pre>{@code
|
||||
* Customer customer = query
|
||||
* .findOneOrThrow(() -> new NotFoundException("Customer not found for id: " + id));
|
||||
* }</pre>
|
||||
*/
|
||||
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
||||
return findOneOrEmpty().orElseThrow(exceptionSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query returning the list of objects.
|
||||
* <p>
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.ebean;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import javax.sql.DataSource;
|
||||
import java.io.Serializable;
|
||||
import java.sql.Connection;
|
||||
@@ -11,6 +12,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Query object for performing native SQL queries that return SqlRow or directly read
|
||||
@@ -144,6 +146,22 @@ public interface SqlQuery extends Serializable, CancelableQuery {
|
||||
*/
|
||||
Optional<SqlRow> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single row or throwing a
|
||||
* {@link jakarta.persistence.EntityNotFoundException} if there is no matching row.
|
||||
*/
|
||||
default SqlRow findOneOrThrow() {
|
||||
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException("Not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query returning a single row or throwing the exception produced
|
||||
* by the given supplier if there is no matching row.
|
||||
*/
|
||||
default SqlRow findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
||||
return findOneOrEmpty().orElseThrow(exceptionSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set one of more positioned parameters.
|
||||
* <p>
|
||||
@@ -391,6 +409,22 @@ public interface SqlQuery extends Serializable, CancelableQuery {
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Return the single value or throw a {@link jakarta.persistence.EntityNotFoundException}
|
||||
* if there is no matching row.
|
||||
*/
|
||||
default T findOneOrThrow() {
|
||||
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException("Not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the single value or throw the exception produced by the given supplier
|
||||
* if there is no matching row.
|
||||
*/
|
||||
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
||||
return findOneOrEmpty().orElseThrow(exceptionSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of values.
|
||||
*/
|
||||
|
||||
+18
@@ -1335,6 +1335,24 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a "propertyName: value" description when this expression list is a single
|
||||
* simple equality predicate (a candidate natural/unique key), otherwise null.
|
||||
* <p>
|
||||
* Used to build a decent default message for {@code findOneOrThrow()} when the query
|
||||
* isn't a simple find-by-id.
|
||||
*/
|
||||
@Nullable
|
||||
public String singleEqDescription() {
|
||||
if (list.size() == 1 && list.get(0) instanceof SimpleExpression) {
|
||||
SimpleExpression simple = (SimpleExpression) list.get(0);
|
||||
if (simple.isOpEquals()) {
|
||||
return simple.getPropName() + ": " + simple.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> clear() {
|
||||
list.clear();
|
||||
|
||||
@@ -90,6 +90,11 @@ public final class DefaultMappedQuery<T, D> implements MappedQuery<D> {
|
||||
return Optional.ofNullable(findOne());
|
||||
}
|
||||
|
||||
@Override
|
||||
public D findOneOrThrow() {
|
||||
return mapper().map(query.findOneOrThrow());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PagedList<D> findPagedList() {
|
||||
DtoMapper<T, D> m = mapper();
|
||||
|
||||
@@ -23,6 +23,7 @@ import io.ebeaninternal.server.query.NativeSqlQueryPlanKey;
|
||||
import io.ebeaninternal.server.rawsql.SpiRawSql;
|
||||
import io.ebeaninternal.server.transaction.ExternalJdbcTransaction;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Timestamp;
|
||||
@@ -1656,6 +1657,30 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
|
||||
return server.findOneOrEmpty(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final T findOneOrThrow() {
|
||||
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException(notFoundMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a decent default "not found" message using the id (if this is effectively a
|
||||
* find-by-id query) or, failing that, a single simple equality predicate (a likely
|
||||
* natural/unique key). Falls back to a generic message for anything more complex.
|
||||
*/
|
||||
private String notFoundMessage() {
|
||||
String type = beanDescriptor.type().getSimpleName();
|
||||
if (isFindById()) {
|
||||
return type + " not found for id: " + id;
|
||||
}
|
||||
if (whereExpressions != null) {
|
||||
String desc = whereExpressions.singleEqDescription();
|
||||
if (desc != null) {
|
||||
return type + " not found for " + desc;
|
||||
}
|
||||
}
|
||||
return type + " not found";
|
||||
}
|
||||
|
||||
@Override
|
||||
public final FutureIds<T> findFutureIds() {
|
||||
return server.findFutureIds(this);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.tests.query;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.model.basic.Customer;
|
||||
import org.tests.model.basic.ResetBasicData;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@code findOneOrThrow()} - a convenience alternative to
|
||||
* {@code findOneOrEmpty().orElseThrow(...)} that produces a decent default message
|
||||
* when the query is effectively a find-by-id or single natural/unique key lookup.
|
||||
*/
|
||||
class TestFindOneOrThrow extends BaseTestCase {
|
||||
|
||||
@Test
|
||||
void findOneOrThrow_whenFound_returnsBean() {
|
||||
ResetBasicData.reset();
|
||||
Customer existing = DB.find(Customer.class).setMaxRows(1).findList().get(0);
|
||||
|
||||
Customer found = DB.find(Customer.class).setId(existing.getId()).findOneOrThrow();
|
||||
|
||||
assertThat(found.getId()).isEqualTo(existing.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOneOrThrow_whenNotFoundById_messageIncludesId() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
assertThatThrownBy(() -> DB.find(Customer.class).setId(999999).findOneOrThrow())
|
||||
.isInstanceOf(EntityNotFoundException.class)
|
||||
.hasMessage("Customer not found for id: 999999");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOneOrThrow_whenNotFoundBySingleEqPredicate_messageIncludesPredicate() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
assertThatThrownBy(() -> DB.find(Customer.class)
|
||||
.where().eq("name", "NonExistentCustomerXYZ")
|
||||
.findOneOrThrow())
|
||||
.isInstanceOf(EntityNotFoundException.class)
|
||||
.hasMessage("Customer not found for name: NonExistentCustomerXYZ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOneOrThrow_whenNotFoundByMultiplePredicates_fallsBackToGenericMessage() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
assertThatThrownBy(() -> DB.find(Customer.class)
|
||||
.where().eq("name", "NonExistentCustomerXYZ").eq("id", 999999)
|
||||
.findOneOrThrow())
|
||||
.isInstanceOf(EntityNotFoundException.class)
|
||||
.hasMessage("Customer not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOneOrThrow_withSupplier_usesSuppliedException() {
|
||||
ResetBasicData.reset();
|
||||
|
||||
assertThatThrownBy(() -> DB.find(Customer.class).setId(999999)
|
||||
.findOneOrThrow(() -> new IllegalStateException("custom message")))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("custom message");
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import io.ebean.LazyInitialisationException;
|
||||
import io.ebean.PagedList;
|
||||
import io.ebean.Transaction;
|
||||
import io.ebean.test.LoggedSql;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.dtomapping.model.Address;
|
||||
@@ -83,6 +84,29 @@ class TestQueryMapTo {
|
||||
assertThat(dto).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapTo_findOneOrThrow_whenFound_expectPopulatedDto() {
|
||||
Customer customer = new Customer("Gamma");
|
||||
customer.save();
|
||||
|
||||
CustomerDto dto = DB.find(Customer.class)
|
||||
.where().idEq(customer.getId())
|
||||
.mapTo(CustomerDto.class)
|
||||
.findOneOrThrow();
|
||||
|
||||
assertThat(dto.getName()).isEqualTo("Gamma");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapTo_findOneOrThrow_whenNoMatch_expectEntityNotFoundExceptionWithEntityTypeAndId() {
|
||||
assertThatThrownBy(() -> DB.find(Customer.class)
|
||||
.setId(-1L)
|
||||
.mapTo(CustomerDto.class)
|
||||
.findOneOrThrow())
|
||||
.isInstanceOf(EntityNotFoundException.class)
|
||||
.hasMessage("Customer not found for id: -1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapTo_whenPairNotRegistered_expectPersistenceException() {
|
||||
assertThatThrownBy(() -> DB.find(Contact.class).mapTo(CustomerDto.class).findList())
|
||||
|
||||
Reference in New Issue
Block a user