mirror of
https://github.com/google/nomulus.git
synced 2025-06-19 19:00:46 +02:00
Always detach entities during load (#1116)
* Always detach entities during load The mutations on non-transient fields that we do in some of the PostLoad methods have been causing the objects to be marked as "dirty", and hibernate has been quietly persisting them during transaction commit. By detaching the entities on load, we avoid any possibility of this, which works in our case because we treat all of our model objects as immutable during normal use. There is another mixed blessing to this: lazy loading won't work on these objects once they are detached from a session, meaning that all fields must be lazy loaded up front. This is unfortunate in that we don't always need those lazy-loaded fields and there is a performance cost to loading them, but it is also useful in that objects will now be complete when used outseide of the transaction that loaded them (prior to this, an attempt to access a lazy-loaded field after its transaction closed would have caused an error at runtime). * Changes requested in review * A few improvements to test logic * Deal with premature detachment of mutated objects * Add unit tests, use a more specific exception * Changes for review - Deal with DomainDeleteFlow, which appears to be the only case in the codebase where we're doing a load-after-save. - Display the object that is being loaded after save in the exception message. - Add a TODO for figuring out why Eager loads aren't working as expected. * Move the recurring billing event into a parameter * Changes for review and rebase error fix * Remove initialization of list entries Remove initialization of list entries that we want to be lazy loaded (premium, reserved, and claims lists). * Post-rebase cleanups
This commit is contained in:
parent
5e8726b92b
commit
eb1aeab223
13 changed files with 325 additions and 104 deletions
|
@ -251,7 +251,9 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||||
buildDomainHistory(newDomain, registry, now, durationUntilDelete, inAddGracePeriod);
|
buildDomainHistory(newDomain, registry, now, durationUntilDelete, inAddGracePeriod);
|
||||||
updateForeignKeyIndexDeletionTime(newDomain);
|
updateForeignKeyIndexDeletionTime(newDomain);
|
||||||
handlePendingTransferOnDelete(existingDomain, newDomain, now, domainHistory);
|
handlePendingTransferOnDelete(existingDomain, newDomain, now, domainHistory);
|
||||||
// Close the autorenew billing event and poll message. This may delete the poll message.
|
// Close the autorenew billing event and poll message. This may delete the poll message. Store
|
||||||
|
// the updated recurring billing event, we'll need it later and can't reload it.
|
||||||
|
BillingEvent.Recurring recurringBillingEvent =
|
||||||
updateAutorenewRecurrenceEndTime(existingDomain, now);
|
updateAutorenewRecurrenceEndTime(existingDomain, now);
|
||||||
// If there's a pending transfer, the gaining client's autorenew billing
|
// If there's a pending transfer, the gaining client's autorenew billing
|
||||||
// event and poll message will already have been deleted in
|
// event and poll message will already have been deleted in
|
||||||
|
@ -275,7 +277,8 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||||
newDomain.getDeletionTime().isAfter(now)
|
newDomain.getDeletionTime().isAfter(now)
|
||||||
? SUCCESS_WITH_ACTION_PENDING
|
? SUCCESS_WITH_ACTION_PENDING
|
||||||
: SUCCESS)
|
: SUCCESS)
|
||||||
.setResponseExtensions(getResponseExtensions(existingDomain, now))
|
.setResponseExtensions(
|
||||||
|
getResponseExtensions(recurringBillingEvent, existingDomain, now))
|
||||||
.build());
|
.build());
|
||||||
persistEntityChanges(entityChanges);
|
persistEntityChanges(entityChanges);
|
||||||
return responseBuilder
|
return responseBuilder
|
||||||
|
@ -377,7 +380,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private ImmutableList<FeeTransformResponseExtension> getResponseExtensions(
|
private ImmutableList<FeeTransformResponseExtension> getResponseExtensions(
|
||||||
DomainBase existingDomain, DateTime now) {
|
BillingEvent.Recurring recurringBillingEvent, DomainBase existingDomain, DateTime now) {
|
||||||
FeeTransformResponseExtension.Builder feeResponseBuilder = getDeleteResponseBuilder();
|
FeeTransformResponseExtension.Builder feeResponseBuilder = getDeleteResponseBuilder();
|
||||||
if (feeResponseBuilder == null) {
|
if (feeResponseBuilder == null) {
|
||||||
return ImmutableList.of();
|
return ImmutableList.of();
|
||||||
|
@ -385,7 +388,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||||
ImmutableList.Builder<Credit> creditsBuilder = new ImmutableList.Builder<>();
|
ImmutableList.Builder<Credit> creditsBuilder = new ImmutableList.Builder<>();
|
||||||
for (GracePeriod gracePeriod : existingDomain.getGracePeriods()) {
|
for (GracePeriod gracePeriod : existingDomain.getGracePeriods()) {
|
||||||
if (gracePeriod.hasBillingEvent()) {
|
if (gracePeriod.hasBillingEvent()) {
|
||||||
Money cost = getGracePeriodCost(gracePeriod, now);
|
Money cost = getGracePeriodCost(recurringBillingEvent, gracePeriod, now);
|
||||||
creditsBuilder.add(Credit.create(
|
creditsBuilder.add(Credit.create(
|
||||||
cost.negated().getAmount(), FeeType.CREDIT, gracePeriod.getType().getXmlName()));
|
cost.negated().getAmount(), FeeType.CREDIT, gracePeriod.getType().getXmlName()));
|
||||||
feeResponseBuilder.setCurrency(checkNotNull(cost.getCurrencyUnit()));
|
feeResponseBuilder.setCurrency(checkNotNull(cost.getCurrencyUnit()));
|
||||||
|
@ -398,12 +401,12 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||||
return ImmutableList.of(feeResponseBuilder.setCredits(credits).build());
|
return ImmutableList.of(feeResponseBuilder.setCredits(credits).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Money getGracePeriodCost(GracePeriod gracePeriod, DateTime now) {
|
private Money getGracePeriodCost(
|
||||||
|
BillingEvent.Recurring recurringBillingEvent, GracePeriod gracePeriod, DateTime now) {
|
||||||
if (gracePeriod.getType() == GracePeriodStatus.AUTO_RENEW) {
|
if (gracePeriod.getType() == GracePeriodStatus.AUTO_RENEW) {
|
||||||
|
// If we updated the autorenew billing event, reuse it.
|
||||||
DateTime autoRenewTime =
|
DateTime autoRenewTime =
|
||||||
tm().loadByKey(checkNotNull(gracePeriod.getRecurringBillingEvent()))
|
recurringBillingEvent.getRecurrenceTimeOfYear().getLastInstanceBeforeOrAt(now);
|
||||||
.getRecurrenceTimeOfYear()
|
|
||||||
.getLastInstanceBeforeOrAt(now);
|
|
||||||
return getDomainRenewCost(targetId, autoRenewTime, 1);
|
return getDomainRenewCost(targetId, autoRenewTime, 1);
|
||||||
}
|
}
|
||||||
return tm().loadByKey(checkNotNull(gracePeriod.getOneTimeBillingEvent())).getCost();
|
return tm().loadByKey(checkNotNull(gracePeriod.getOneTimeBillingEvent())).getCost();
|
||||||
|
|
|
@ -517,8 +517,10 @@ public class DomainFlowUtils {
|
||||||
* <p>This may end up deleting the poll message (if closing the message interval) or recreating it
|
* <p>This may end up deleting the poll message (if closing the message interval) or recreating it
|
||||||
* (if opening the message interval). This may cause an autorenew billing event to have an end
|
* (if opening the message interval). This may cause an autorenew billing event to have an end
|
||||||
* time earlier than its event time (i.e. if it's being ended before it was ever triggered).
|
* time earlier than its event time (i.e. if it's being ended before it was ever triggered).
|
||||||
|
*
|
||||||
|
* <p>Returns the new autorenew recurring billing event.
|
||||||
*/
|
*/
|
||||||
public static void updateAutorenewRecurrenceEndTime(DomainBase domain, DateTime newEndTime) {
|
public static Recurring updateAutorenewRecurrenceEndTime(DomainBase domain, DateTime newEndTime) {
|
||||||
Optional<PollMessage.Autorenew> autorenewPollMessage =
|
Optional<PollMessage.Autorenew> autorenewPollMessage =
|
||||||
tm().loadByKeyIfPresent(domain.getAutorenewPollMessage());
|
tm().loadByKeyIfPresent(domain.getAutorenewPollMessage());
|
||||||
|
|
||||||
|
@ -545,8 +547,13 @@ public class DomainFlowUtils {
|
||||||
tm().put(updatedAutorenewPollMessage);
|
tm().put(updatedAutorenewPollMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
Recurring recurring = tm().loadByKey(domain.getAutorenewBillingEvent());
|
Recurring recurring =
|
||||||
tm().put(recurring.asBuilder().setRecurrenceEndTime(newEndTime).build());
|
tm().loadByKey(domain.getAutorenewBillingEvent())
|
||||||
|
.asBuilder()
|
||||||
|
.setRecurrenceEndTime(newEndTime)
|
||||||
|
.build();
|
||||||
|
tm().put(recurring);
|
||||||
|
return recurring;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -36,7 +36,9 @@ import javax.persistence.Index;
|
||||||
import javax.persistence.JoinColumn;
|
import javax.persistence.JoinColumn;
|
||||||
import javax.persistence.JoinTable;
|
import javax.persistence.JoinTable;
|
||||||
import javax.persistence.OneToMany;
|
import javax.persistence.OneToMany;
|
||||||
|
import javax.persistence.PostLoad;
|
||||||
import javax.persistence.Table;
|
import javax.persistence.Table;
|
||||||
|
import org.hibernate.Hibernate;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -78,6 +80,8 @@ public class DomainBase extends DomainContent
|
||||||
return super.getRepoId();
|
return super.getRepoId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It seems like this should be FetchType.EAGER, but for some reason when we do that we get a lazy
|
||||||
|
// load error during the load of a domain.
|
||||||
@ElementCollection
|
@ElementCollection
|
||||||
@JoinTable(
|
@JoinTable(
|
||||||
name = "DomainHost",
|
name = "DomainHost",
|
||||||
|
@ -139,6 +143,16 @@ public class DomainBase extends DomainContent
|
||||||
return dsData;
|
return dsData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Post-load method to eager load the collections. */
|
||||||
|
@PostLoad
|
||||||
|
@Override
|
||||||
|
protected void postLoad() {
|
||||||
|
super.postLoad();
|
||||||
|
// TODO(b/188044616): Determine why Eager loading doesn't work here.
|
||||||
|
Hibernate.initialize(dsData);
|
||||||
|
Hibernate.initialize(gracePeriods);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VKey<DomainBase> createVKey() {
|
public VKey<DomainBase> createVKey() {
|
||||||
return VKey.create(DomainBase.class, getRepoId(), Key.create(this));
|
return VKey.create(DomainBase.class, getRepoId(), Key.create(this));
|
||||||
|
|
|
@ -338,8 +338,7 @@ public class DomainContent extends EppResource
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostLoad
|
@PostLoad
|
||||||
@SuppressWarnings("UnusedMethod")
|
protected void postLoad() {
|
||||||
private void postLoad() {
|
|
||||||
// Reconstitute the contact list.
|
// Reconstitute the contact list.
|
||||||
ImmutableSet.Builder<DesignatedContact> contactsBuilder = new ImmutableSet.Builder<>();
|
ImmutableSet.Builder<DesignatedContact> contactsBuilder = new ImmutableSet.Builder<>();
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ import javax.persistence.JoinTable;
|
||||||
import javax.persistence.OneToMany;
|
import javax.persistence.OneToMany;
|
||||||
import javax.persistence.PostLoad;
|
import javax.persistence.PostLoad;
|
||||||
import javax.persistence.Table;
|
import javax.persistence.Table;
|
||||||
|
import org.hibernate.Hibernate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A persisted history entry representing an EPP modification to a domain.
|
* A persisted history entry representing an EPP modification to a domain.
|
||||||
|
@ -261,6 +262,12 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(b/188044616): Determine why Eager loading doesn't work here.
|
||||||
|
Hibernate.initialize(domainTransactionRecords);
|
||||||
|
Hibernate.initialize(nsHosts);
|
||||||
|
Hibernate.initialize(dsDataHistories);
|
||||||
|
Hibernate.initialize(gracePeriodHistories);
|
||||||
}
|
}
|
||||||
|
|
||||||
// In Datastore, save as a HistoryEntry object regardless of this object's type
|
// In Datastore, save as a HistoryEntry object regardless of this object's type
|
||||||
|
|
|
@ -297,8 +297,9 @@ public class DatastoreTransactionManager implements TransactionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(Object entity) {
|
public <T> T delete(T entity) {
|
||||||
syncIfTransactionless(getOfy().delete().entity(toDatastoreEntity(entity)));
|
syncIfTransactionless(getOfy().delete().entity(toDatastoreEntity(entity)));
|
||||||
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -44,13 +44,17 @@ import google.registry.util.Clock;
|
||||||
import google.registry.util.Retrier;
|
import google.registry.util.Retrier;
|
||||||
import google.registry.util.SystemSleeper;
|
import google.registry.util.SystemSleeper;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.EntityManagerFactory;
|
import javax.persistence.EntityManagerFactory;
|
||||||
import javax.persistence.EntityTransaction;
|
import javax.persistence.EntityTransaction;
|
||||||
|
@ -269,8 +273,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
assertInTransaction();
|
assertInTransaction();
|
||||||
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
|
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
|
||||||
Object toPersist = toSqlEntity(entity);
|
Object toPersist = toSqlEntity(entity);
|
||||||
getEntityManager().persist(toPersist);
|
transactionInfo.get().insertObject(toPersist);
|
||||||
transactionInfo.get().addUpdate(toPersist);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -299,8 +302,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
assertInTransaction();
|
assertInTransaction();
|
||||||
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
|
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
|
||||||
Object toPersist = toSqlEntity(entity);
|
Object toPersist = toSqlEntity(entity);
|
||||||
getEntityManager().merge(toPersist);
|
transactionInfo.get().updateObject(toPersist);
|
||||||
transactionInfo.get().addUpdate(toPersist);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -339,8 +341,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
checkArgument(exists(entity), "Given entity does not exist");
|
checkArgument(exists(entity), "Given entity does not exist");
|
||||||
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
|
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
|
||||||
Object toPersist = toSqlEntity(entity);
|
Object toPersist = toSqlEntity(entity);
|
||||||
getEntityManager().merge(toPersist);
|
transactionInfo.get().updateObject(toPersist);
|
||||||
transactionInfo.get().addUpdate(toPersist);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -397,7 +398,8 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
public <T> Optional<T> loadByKeyIfPresent(VKey<T> key) {
|
public <T> Optional<T> loadByKeyIfPresent(VKey<T> key) {
|
||||||
checkArgumentNotNull(key, "key must be specified");
|
checkArgumentNotNull(key, "key must be specified");
|
||||||
assertInTransaction();
|
assertInTransaction();
|
||||||
return Optional.ofNullable(getEntityManager().find(key.getKind(), key.getSqlKey()));
|
return Optional.ofNullable(getEntityManager().find(key.getKind(), key.getSqlKey()))
|
||||||
|
.map(this::detach);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -411,7 +413,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
.map(
|
.map(
|
||||||
key ->
|
key ->
|
||||||
new SimpleEntry<VKey<? extends T>, T>(
|
new SimpleEntry<VKey<? extends T>, T>(
|
||||||
key, getEntityManager().find(key.getKind(), key.getSqlKey())))
|
key, detach(getEntityManager().find(key.getKind(), key.getSqlKey()))))
|
||||||
.filter(entry -> entry.getValue() != null)
|
.filter(entry -> entry.getValue() != null)
|
||||||
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
|
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
}
|
}
|
||||||
|
@ -433,7 +435,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
throw new NoSuchElementException(key.toString());
|
throw new NoSuchElementException(key.toString());
|
||||||
}
|
}
|
||||||
return result;
|
return detach(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -472,12 +474,11 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
public <T> ImmutableList<T> loadAllOf(Class<T> clazz) {
|
public <T> ImmutableList<T> loadAllOf(Class<T> clazz) {
|
||||||
checkArgumentNotNull(clazz, "clazz must be specified");
|
checkArgumentNotNull(clazz, "clazz must be specified");
|
||||||
assertInTransaction();
|
assertInTransaction();
|
||||||
return ImmutableList.copyOf(
|
return getEntityManager()
|
||||||
getEntityManager()
|
.createQuery(String.format("FROM %s", getEntityType(clazz).getName()), clazz)
|
||||||
.createQuery(
|
.getResultStream()
|
||||||
String.format("SELECT entity FROM %s entity", getEntityType(clazz).getName()),
|
.map(this::detach)
|
||||||
clazz)
|
.collect(toImmutableList());
|
||||||
.getResultList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -524,18 +525,21 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(Object entity) {
|
public <T> T delete(T entity) {
|
||||||
checkArgumentNotNull(entity, "entity must be specified");
|
checkArgumentNotNull(entity, "entity must be specified");
|
||||||
if (isEntityOfIgnoredClass(entity)) {
|
if (isEntityOfIgnoredClass(entity)) {
|
||||||
return;
|
return entity;
|
||||||
}
|
}
|
||||||
assertInTransaction();
|
assertInTransaction();
|
||||||
entity = toSqlEntity(entity);
|
entity = toSqlEntity(entity);
|
||||||
Object managedEntity = entity;
|
T managedEntity = entity;
|
||||||
if (!getEntityManager().contains(entity)) {
|
if (!getEntityManager().contains(entity)) {
|
||||||
|
// We don't add the entity to "objectsToSave": once deleted, the object should never be
|
||||||
|
// returned as a result of the query or lookup.
|
||||||
managedEntity = getEntityManager().merge(entity);
|
managedEntity = getEntityManager().merge(entity);
|
||||||
}
|
}
|
||||||
getEntityManager().remove(managedEntity);
|
getEntityManager().remove(managedEntity);
|
||||||
|
return managedEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -555,7 +559,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> QueryComposer<T> createQueryComposer(Class<T> entity) {
|
public <T> QueryComposer<T> createQueryComposer(Class<T> entity) {
|
||||||
return new JpaQueryComposerImpl<T>(entity, getEntityManager());
|
return new JpaQueryComposerImpl<T>(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -658,6 +662,21 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Detach the entity, suitable for use in Optional.map(). */
|
||||||
|
@Nullable
|
||||||
|
private <T> T detach(@Nullable T entity) {
|
||||||
|
if (entity != null) {
|
||||||
|
|
||||||
|
// If the entity was previously persisted or merged, we have to throw an exception.
|
||||||
|
if (transactionInfo.get().willSave(entity)) {
|
||||||
|
throw new IllegalStateException("Inserted/updated object reloaded: " + entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntityManager().detach(entity);
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
private static class TransactionInfo {
|
private static class TransactionInfo {
|
||||||
EntityManager entityManager;
|
EntityManager entityManager;
|
||||||
boolean inTransaction = false;
|
boolean inTransaction = false;
|
||||||
|
@ -666,6 +685,12 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
// Serializable representation of the transaction to be persisted in the Transaction table.
|
// Serializable representation of the transaction to be persisted in the Transaction table.
|
||||||
Transaction.Builder contentsBuilder;
|
Transaction.Builder contentsBuilder;
|
||||||
|
|
||||||
|
// The set of entity objects that have been either persisted (via insert()) or merged (via
|
||||||
|
// put()/update()). If the entity manager returns these as a result of a find() or query
|
||||||
|
// operation, we can not detach them -- detaching removes them from the transaction and causes
|
||||||
|
// them to not be saved to the database -- so we throw an exception instead.
|
||||||
|
Set<Object> objectsToSave = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
|
||||||
|
|
||||||
/** Start a new transaction. */
|
/** Start a new transaction. */
|
||||||
private void start(Clock clock) {
|
private void start(Clock clock) {
|
||||||
checkArgumentNotNull(clock);
|
checkArgumentNotNull(clock);
|
||||||
|
@ -680,6 +705,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
inTransaction = false;
|
inTransaction = false;
|
||||||
transactionTime = null;
|
transactionTime = null;
|
||||||
contentsBuilder = null;
|
contentsBuilder = null;
|
||||||
|
objectsToSave = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
|
||||||
if (entityManager != null) {
|
if (entityManager != null) {
|
||||||
// Close this EntityManager just let the connection pool be able to reuse it, it doesn't
|
// Close this EntityManager just let the connection pool be able to reuse it, it doesn't
|
||||||
// close the underlying database connection.
|
// close the underlying database connection.
|
||||||
|
@ -708,25 +734,40 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Does the full "update" on an object including all internal housekeeping. */
|
||||||
|
private void updateObject(Object object) {
|
||||||
|
Object merged = entityManager.merge(object);
|
||||||
|
objectsToSave.add(merged);
|
||||||
|
addUpdate(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class JpaQueryComposerImpl<T> extends QueryComposer<T> {
|
/** Does the full "insert" on a new object including all internal housekeeping. */
|
||||||
|
private void insertObject(Object object) {
|
||||||
|
entityManager.persist(object);
|
||||||
|
objectsToSave.add(object);
|
||||||
|
addUpdate(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the object has been persisted/merged and will be saved on commit. */
|
||||||
|
private boolean willSave(Object object) {
|
||||||
|
return objectsToSave.contains(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class JpaQueryComposerImpl<T> extends QueryComposer<T> {
|
||||||
|
|
||||||
private static final int DEFAULT_FETCH_SIZE = 1000;
|
private static final int DEFAULT_FETCH_SIZE = 1000;
|
||||||
|
|
||||||
EntityManager em;
|
|
||||||
|
|
||||||
private int fetchSize = DEFAULT_FETCH_SIZE;
|
private int fetchSize = DEFAULT_FETCH_SIZE;
|
||||||
|
|
||||||
private boolean autoDetachOnLoad = true;
|
JpaQueryComposerImpl(Class<T> entityClass) {
|
||||||
|
|
||||||
JpaQueryComposerImpl(Class<T> entityClass, EntityManager em) {
|
|
||||||
super(entityClass);
|
super(entityClass);
|
||||||
this.em = em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TypedQuery<T> buildQuery() {
|
private TypedQuery<T> buildQuery() {
|
||||||
CriteriaQueryBuilder<T> queryBuilder = CriteriaQueryBuilder.create(em, entityClass);
|
CriteriaQueryBuilder<T> queryBuilder =
|
||||||
|
CriteriaQueryBuilder.create(getEntityManager(), entityClass);
|
||||||
return addCriteria(queryBuilder);
|
return addCriteria(queryBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -739,13 +780,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
queryBuilder.orderByAsc(orderBy);
|
queryBuilder.orderByAsc(orderBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
return em.createQuery(queryBuilder.build());
|
return getEntityManager().createQuery(queryBuilder.build());
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public QueryComposer<T> withAutoDetachOnLoad(boolean autoDetachOnLoad) {
|
|
||||||
this.autoDetachOnLoad = autoDetachOnLoad;
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -758,12 +793,12 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
@Override
|
@Override
|
||||||
public Optional<T> first() {
|
public Optional<T> first() {
|
||||||
List<T> results = buildQuery().setMaxResults(1).getResultList();
|
List<T> results = buildQuery().setMaxResults(1).getResultList();
|
||||||
return results.size() > 0 ? Optional.of(maybeDetachEntity(results.get(0))) : Optional.empty();
|
return results.size() > 0 ? Optional.of(detach(results.get(0))) : Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public T getSingleResult() {
|
public T getSingleResult() {
|
||||||
return maybeDetachEntity(buildQuery().getSingleResult());
|
return detach(buildQuery().getSingleResult());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -777,27 +812,21 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||||
} else {
|
} else {
|
||||||
logger.atWarning().log("Query implemention does not support result streaming.");
|
logger.atWarning().log("Query implemention does not support result streaming.");
|
||||||
}
|
}
|
||||||
return query.getResultStream().map(this::maybeDetachEntity);
|
return query.getResultStream().map(JpaTransactionManagerImpl.this::detach);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long count() {
|
public long count() {
|
||||||
CriteriaQueryBuilder<Long> queryBuilder = CriteriaQueryBuilder.createCount(em, entityClass);
|
CriteriaQueryBuilder<Long> queryBuilder =
|
||||||
|
CriteriaQueryBuilder.createCount(getEntityManager(), entityClass);
|
||||||
return addCriteria(queryBuilder).getSingleResult();
|
return addCriteria(queryBuilder).getSingleResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ImmutableList<T> list() {
|
public ImmutableList<T> list() {
|
||||||
return buildQuery().getResultList().stream()
|
return buildQuery().getResultList().stream()
|
||||||
.map(this::maybeDetachEntity)
|
.map(JpaTransactionManagerImpl.this::detach)
|
||||||
.collect(ImmutableList.toImmutableList());
|
.collect(ImmutableList.toImmutableList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private T maybeDetachEntity(T entity) {
|
|
||||||
if (autoDetachOnLoad) {
|
|
||||||
em.detach(entity);
|
|
||||||
}
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -262,8 +262,14 @@ public interface TransactionManager {
|
||||||
/** Deletes the set of entities by their key id. */
|
/** Deletes the set of entities by their key id. */
|
||||||
void delete(Iterable<? extends VKey<?>> keys);
|
void delete(Iterable<? extends VKey<?>> keys);
|
||||||
|
|
||||||
/** Deletes the given entity from the database. */
|
/**
|
||||||
void delete(Object entity);
|
* Deletes the given entity from the database.
|
||||||
|
*
|
||||||
|
* <p>This returns the deleted entity, which may not necessarily be the same as the original
|
||||||
|
* entity passed in, as it may be a) converted to a different type of object more appropriate to
|
||||||
|
* the database type or b) merged with an object managed by the database entity manager.
|
||||||
|
*/
|
||||||
|
<T> T delete(T entity);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the entity by its id without writing commit logs if the underlying database is
|
* Deletes the entity by its id without writing commit logs if the underlying database is
|
||||||
|
|
|
@ -58,7 +58,7 @@ class EntityCallbacksListenerTest {
|
||||||
.transact(
|
.transact(
|
||||||
() -> {
|
() -> {
|
||||||
TestEntity merged = jpaTm().getEntityManager().merge(testUpdate);
|
TestEntity merged = jpaTm().getEntityManager().merge(testUpdate);
|
||||||
merged.foo++;
|
merged.nonTransientField++;
|
||||||
jpaTm().getEntityManager().flush();
|
jpaTm().getEntityManager().flush();
|
||||||
return merged;
|
return merged;
|
||||||
});
|
});
|
||||||
|
@ -74,8 +74,7 @@ class EntityCallbacksListenerTest {
|
||||||
.transact(
|
.transact(
|
||||||
() -> {
|
() -> {
|
||||||
TestEntity removed = jpaTm().loadByKey(VKey.createSql(TestEntity.class, "id"));
|
TestEntity removed = jpaTm().loadByKey(VKey.createSql(TestEntity.class, "id"));
|
||||||
jpaTm().delete(removed);
|
return jpaTm().delete(removed);
|
||||||
return removed;
|
|
||||||
});
|
});
|
||||||
checkAll(testRemove, 0, 0, 1, 1);
|
checkAll(testRemove, 0, 0, 1, 1);
|
||||||
}
|
}
|
||||||
|
@ -99,6 +98,24 @@ class EntityCallbacksListenerTest {
|
||||||
assertThat(hasMethodAnnotatedWithEmbedded(ViolationEntity.class)).isTrue();
|
assertThat(hasMethodAnnotatedWithEmbedded(ViolationEntity.class)).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void verifyCallbacksNotCalledOnCommit() {
|
||||||
|
TestEntity testEntity = new TestEntity();
|
||||||
|
jpaTm().transact(() -> jpaTm().insert(testEntity));
|
||||||
|
|
||||||
|
TestEntity testLoad =
|
||||||
|
jpaTm().transact(() -> jpaTm().loadByKey(VKey.createSql(TestEntity.class, "id")));
|
||||||
|
assertThat(testLoad.entityPreUpdate).isEqualTo(0);
|
||||||
|
|
||||||
|
testLoad = jpaTm().transact(() -> jpaTm().loadByKey(VKey.createSql(TestEntity.class, "id")));
|
||||||
|
|
||||||
|
// Verify that post-load happened but pre-update didn't.
|
||||||
|
assertThat(testLoad.entityPostLoad).isEqualTo(1);
|
||||||
|
assertThat(testLoad.entityPreUpdate).isEqualTo(0);
|
||||||
|
// since we didn't save the non-transient field, should only be 1
|
||||||
|
assertThat(testLoad.nonTransientField).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean hasMethodAnnotatedWithEmbedded(Class<?> entityType) {
|
private static boolean hasMethodAnnotatedWithEmbedded(Class<?> entityType) {
|
||||||
boolean result = false;
|
boolean result = false;
|
||||||
Class<?> parentType = entityType.getSuperclass();
|
Class<?> parentType = entityType.getSuperclass();
|
||||||
|
@ -152,15 +169,22 @@ class EntityCallbacksListenerTest {
|
||||||
@Entity(name = "TestEntity")
|
@Entity(name = "TestEntity")
|
||||||
private static class TestEntity extends ParentEntity {
|
private static class TestEntity extends ParentEntity {
|
||||||
@Id String name = "id";
|
@Id String name = "id";
|
||||||
int foo = 0;
|
int nonTransientField = 0;
|
||||||
|
|
||||||
@Transient int entityPostLoad = 0;
|
@Transient int entityPostLoad = 0;
|
||||||
|
@Transient int entityPreUpdate = 0;
|
||||||
|
|
||||||
@Embedded EntityEmbedded entityEmbedded = new EntityEmbedded();
|
@Embedded EntityEmbedded entityEmbedded = new EntityEmbedded();
|
||||||
|
|
||||||
@PostLoad
|
@PostLoad
|
||||||
void entityPostLoad() {
|
void entityPostLoad() {
|
||||||
entityPostLoad++;
|
entityPostLoad++;
|
||||||
|
nonTransientField++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
void entityPreUpdate() {
|
||||||
|
entityPreUpdate++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,10 @@
|
||||||
|
|
||||||
package google.registry.persistence.transaction;
|
package google.registry.persistence.transaction;
|
||||||
|
|
||||||
|
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||||
|
import static google.registry.testing.DatabaseHelper.assertDetached;
|
||||||
import static google.registry.testing.TestDataHelper.fileClassPath;
|
import static google.registry.testing.TestDataHelper.fileClassPath;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
@ -25,9 +27,11 @@ import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
import google.registry.model.ImmutableObject;
|
import google.registry.model.ImmutableObject;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestExtension;
|
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestExtension;
|
||||||
|
import google.registry.testing.DatabaseHelper;
|
||||||
import google.registry.testing.FakeClock;
|
import google.registry.testing.FakeClock;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
@ -384,7 +388,7 @@ class JpaTransactionManagerImplTest {
|
||||||
void load_succeeds() {
|
void load_succeeds() {
|
||||||
assertThat(jpaTm().transact(() -> jpaTm().exists(theEntity))).isFalse();
|
assertThat(jpaTm().transact(() -> jpaTm().exists(theEntity))).isFalse();
|
||||||
jpaTm().transact(() -> jpaTm().insert(theEntity));
|
jpaTm().transact(() -> jpaTm().insert(theEntity));
|
||||||
TestEntity persisted = jpaTm().transact(() -> jpaTm().loadByKey(theEntityKey));
|
TestEntity persisted = jpaTm().transact(() -> assertDetached(jpaTm().loadByKey(theEntityKey)));
|
||||||
assertThat(persisted.name).isEqualTo("theEntity");
|
assertThat(persisted.name).isEqualTo("theEntity");
|
||||||
assertThat(persisted.data).isEqualTo("foo");
|
assertThat(persisted.data).isEqualTo("foo");
|
||||||
}
|
}
|
||||||
|
@ -397,11 +401,20 @@ class JpaTransactionManagerImplTest {
|
||||||
() -> jpaTm().transact(() -> jpaTm().loadByKey(theEntityKey)));
|
() -> jpaTm().transact(() -> jpaTm().loadByKey(theEntityKey)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadByEntity_succeeds() {
|
||||||
|
jpaTm().transact(() -> jpaTm().insert(theEntity));
|
||||||
|
TestEntity persisted = jpaTm().transact(() -> assertDetached(jpaTm().loadByEntity(theEntity)));
|
||||||
|
assertThat(persisted.name).isEqualTo("theEntity");
|
||||||
|
assertThat(persisted.data).isEqualTo("foo");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void maybeLoad_succeeds() {
|
void maybeLoad_succeeds() {
|
||||||
assertThat(jpaTm().transact(() -> jpaTm().exists(theEntity))).isFalse();
|
assertThat(jpaTm().transact(() -> jpaTm().exists(theEntity))).isFalse();
|
||||||
jpaTm().transact(() -> jpaTm().insert(theEntity));
|
jpaTm().transact(() -> jpaTm().insert(theEntity));
|
||||||
TestEntity persisted = jpaTm().transact(() -> jpaTm().loadByKeyIfPresent(theEntityKey).get());
|
TestEntity persisted =
|
||||||
|
jpaTm().transact(() -> assertDetached(jpaTm().loadByKeyIfPresent(theEntityKey).get()));
|
||||||
assertThat(persisted.name).isEqualTo("theEntity");
|
assertThat(persisted.name).isEqualTo("theEntity");
|
||||||
assertThat(persisted.data).isEqualTo("foo");
|
assertThat(persisted.data).isEqualTo("foo");
|
||||||
}
|
}
|
||||||
|
@ -417,17 +430,81 @@ class JpaTransactionManagerImplTest {
|
||||||
void loadCompoundIdEntity_succeeds() {
|
void loadCompoundIdEntity_succeeds() {
|
||||||
assertThat(jpaTm().transact(() -> jpaTm().exists(compoundIdEntity))).isFalse();
|
assertThat(jpaTm().transact(() -> jpaTm().exists(compoundIdEntity))).isFalse();
|
||||||
jpaTm().transact(() -> jpaTm().insert(compoundIdEntity));
|
jpaTm().transact(() -> jpaTm().insert(compoundIdEntity));
|
||||||
TestCompoundIdEntity persisted = jpaTm().transact(() -> jpaTm().loadByKey(compoundIdEntityKey));
|
TestCompoundIdEntity persisted =
|
||||||
|
jpaTm().transact(() -> assertDetached(jpaTm().loadByKey(compoundIdEntityKey)));
|
||||||
assertThat(persisted.name).isEqualTo("compoundIdEntity");
|
assertThat(persisted.name).isEqualTo("compoundIdEntity");
|
||||||
assertThat(persisted.age).isEqualTo(10);
|
assertThat(persisted.age).isEqualTo(10);
|
||||||
assertThat(persisted.data).isEqualTo("foo");
|
assertThat(persisted.data).isEqualTo("foo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadByKeysIfPresent() {
|
||||||
|
jpaTm().transact(() -> jpaTm().insert(theEntity));
|
||||||
|
jpaTm()
|
||||||
|
.transact(
|
||||||
|
() -> {
|
||||||
|
ImmutableMap<VKey<? extends TestEntity>, TestEntity> results =
|
||||||
|
jpaTm()
|
||||||
|
.loadByKeysIfPresent(
|
||||||
|
ImmutableList.of(
|
||||||
|
theEntityKey, VKey.createSql(TestEntity.class, "does-not-exist")));
|
||||||
|
|
||||||
|
assertThat(results).containsExactly(theEntityKey, theEntity);
|
||||||
|
assertDetached(results.get(theEntityKey));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadByKeys_succeeds() {
|
||||||
|
jpaTm().transact(() -> jpaTm().insert(theEntity));
|
||||||
|
jpaTm()
|
||||||
|
.transact(
|
||||||
|
() -> {
|
||||||
|
ImmutableMap<VKey<? extends TestEntity>, TestEntity> results =
|
||||||
|
jpaTm().loadByKeysIfPresent(ImmutableList.of(theEntityKey));
|
||||||
|
assertThat(results).containsExactly(theEntityKey, theEntity);
|
||||||
|
assertDetached(results.get(theEntityKey));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadByEntitiesIfPresent_succeeds() {
|
||||||
|
jpaTm().transact(() -> jpaTm().insert(theEntity));
|
||||||
|
jpaTm()
|
||||||
|
.transact(
|
||||||
|
() -> {
|
||||||
|
ImmutableList<TestEntity> results =
|
||||||
|
jpaTm()
|
||||||
|
.loadByEntitiesIfPresent(
|
||||||
|
ImmutableList.of(theEntity, new TestEntity("does-not-exist", "bar")));
|
||||||
|
assertThat(results).containsExactly(theEntity);
|
||||||
|
assertDetached(results.get(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadByEntities_succeeds() {
|
||||||
|
jpaTm().transact(() -> jpaTm().insert(theEntity));
|
||||||
|
jpaTm()
|
||||||
|
.transact(
|
||||||
|
() -> {
|
||||||
|
ImmutableList<TestEntity> results =
|
||||||
|
jpaTm().loadByEntities(ImmutableList.of(theEntity));
|
||||||
|
assertThat(results).containsExactly(theEntity);
|
||||||
|
assertDetached(results.get(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadAll_succeeds() {
|
void loadAll_succeeds() {
|
||||||
jpaTm().transact(() -> jpaTm().insertAll(moreEntities));
|
jpaTm().transact(() -> jpaTm().insertAll(moreEntities));
|
||||||
ImmutableList<TestEntity> persisted =
|
ImmutableList<TestEntity> persisted =
|
||||||
jpaTm().transact(() -> jpaTm().loadAllOf(TestEntity.class));
|
jpaTm()
|
||||||
|
.transact(
|
||||||
|
() ->
|
||||||
|
jpaTm().loadAllOf(TestEntity.class).stream()
|
||||||
|
.map(DatabaseHelper::assertDetached)
|
||||||
|
.collect(toImmutableList()));
|
||||||
assertThat(persisted).containsExactlyElementsIn(moreEntities);
|
assertThat(persisted).containsExactlyElementsIn(moreEntities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -462,6 +539,55 @@ class JpaTransactionManagerImplTest {
|
||||||
() -> jpaTm().transact(() -> jpaTm().assertDelete(theEntityKey)));
|
() -> jpaTm().transact(() -> jpaTm().assertDelete(theEntityKey)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadAfterInsert_fails() {
|
||||||
|
assertThat(
|
||||||
|
assertThrows(
|
||||||
|
IllegalStateException.class,
|
||||||
|
() ->
|
||||||
|
jpaTm()
|
||||||
|
.transact(
|
||||||
|
() -> {
|
||||||
|
jpaTm().insert(theEntity);
|
||||||
|
jpaTm().loadByKey(theEntityKey);
|
||||||
|
})))
|
||||||
|
.hasMessageThat()
|
||||||
|
.contains("Inserted/updated object reloaded: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadAfterUpdate_fails() {
|
||||||
|
jpaTm().transact(() -> jpaTm().insert(theEntity));
|
||||||
|
assertThat(
|
||||||
|
assertThrows(
|
||||||
|
IllegalStateException.class,
|
||||||
|
() ->
|
||||||
|
jpaTm()
|
||||||
|
.transact(
|
||||||
|
() -> {
|
||||||
|
jpaTm().update(theEntity);
|
||||||
|
jpaTm().loadByKey(theEntityKey);
|
||||||
|
})))
|
||||||
|
.hasMessageThat()
|
||||||
|
.contains("Inserted/updated object reloaded: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadAfterPut_fails() {
|
||||||
|
assertThat(
|
||||||
|
assertThrows(
|
||||||
|
IllegalStateException.class,
|
||||||
|
() ->
|
||||||
|
jpaTm()
|
||||||
|
.transact(
|
||||||
|
() -> {
|
||||||
|
jpaTm().put(theEntity);
|
||||||
|
jpaTm().loadByKey(theEntityKey);
|
||||||
|
})))
|
||||||
|
.hasMessageThat()
|
||||||
|
.contains("Inserted/updated object reloaded: ");
|
||||||
|
}
|
||||||
|
|
||||||
private void insertPerson(int age) {
|
private void insertPerson(int age) {
|
||||||
jpaTm()
|
jpaTm()
|
||||||
.getEntityManager()
|
.getEntityManager()
|
||||||
|
|
|
@ -26,6 +26,7 @@ import com.googlecode.objectify.annotation.Id;
|
||||||
import com.googlecode.objectify.annotation.Index;
|
import com.googlecode.objectify.annotation.Index;
|
||||||
import google.registry.model.ImmutableObject;
|
import google.registry.model.ImmutableObject;
|
||||||
import google.registry.testing.AppEngineExtension;
|
import google.registry.testing.AppEngineExtension;
|
||||||
|
import google.registry.testing.DatabaseHelper;
|
||||||
import google.registry.testing.DualDatabaseTest;
|
import google.registry.testing.DualDatabaseTest;
|
||||||
import google.registry.testing.FakeClock;
|
import google.registry.testing.FakeClock;
|
||||||
import google.registry.testing.TestOfyAndSql;
|
import google.registry.testing.TestOfyAndSql;
|
||||||
|
@ -70,20 +71,13 @@ public class QueryComposerTest {
|
||||||
|
|
||||||
@TestOfyAndSql
|
@TestOfyAndSql
|
||||||
public void testFirstQueries() {
|
public void testFirstQueries() {
|
||||||
assertThat(
|
|
||||||
transactIfJpaTm(
|
|
||||||
() ->
|
|
||||||
tm().createQueryComposer(TestEntity.class)
|
|
||||||
.where("name", Comparator.EQ, "bravo")
|
|
||||||
.first()
|
|
||||||
.get()))
|
|
||||||
.isEqualTo(bravo);
|
|
||||||
assertThat(
|
assertThat(
|
||||||
transactIfJpaTm(
|
transactIfJpaTm(
|
||||||
() ->
|
() ->
|
||||||
tm().createQueryComposer(TestEntity.class)
|
tm().createQueryComposer(TestEntity.class)
|
||||||
.where("name", Comparator.GT, "bravo")
|
.where("name", Comparator.GT, "bravo")
|
||||||
.first()
|
.first()
|
||||||
|
.map(DatabaseHelper::assertDetached)
|
||||||
.get()))
|
.get()))
|
||||||
.isEqualTo(charlie);
|
.isEqualTo(charlie);
|
||||||
assertThat(
|
assertThat(
|
||||||
|
@ -92,6 +86,7 @@ public class QueryComposerTest {
|
||||||
tm().createQueryComposer(TestEntity.class)
|
tm().createQueryComposer(TestEntity.class)
|
||||||
.where("name", Comparator.GTE, "charlie")
|
.where("name", Comparator.GTE, "charlie")
|
||||||
.first()
|
.first()
|
||||||
|
.map(DatabaseHelper::assertDetached)
|
||||||
.get()))
|
.get()))
|
||||||
.isEqualTo(charlie);
|
.isEqualTo(charlie);
|
||||||
assertThat(
|
assertThat(
|
||||||
|
@ -100,6 +95,7 @@ public class QueryComposerTest {
|
||||||
tm().createQueryComposer(TestEntity.class)
|
tm().createQueryComposer(TestEntity.class)
|
||||||
.where("name", Comparator.LT, "bravo")
|
.where("name", Comparator.LT, "bravo")
|
||||||
.first()
|
.first()
|
||||||
|
.map(DatabaseHelper::assertDetached)
|
||||||
.get()))
|
.get()))
|
||||||
.isEqualTo(alpha);
|
.isEqualTo(alpha);
|
||||||
assertThat(
|
assertThat(
|
||||||
|
@ -108,6 +104,7 @@ public class QueryComposerTest {
|
||||||
tm().createQueryComposer(TestEntity.class)
|
tm().createQueryComposer(TestEntity.class)
|
||||||
.where("name", Comparator.LTE, "alpha")
|
.where("name", Comparator.LTE, "alpha")
|
||||||
.first()
|
.first()
|
||||||
|
.map(DatabaseHelper::assertDetached)
|
||||||
.get()))
|
.get()))
|
||||||
.isEqualTo(alpha);
|
.isEqualTo(alpha);
|
||||||
}
|
}
|
||||||
|
@ -128,9 +125,10 @@ public class QueryComposerTest {
|
||||||
assertThat(
|
assertThat(
|
||||||
transactIfJpaTm(
|
transactIfJpaTm(
|
||||||
() ->
|
() ->
|
||||||
|
DatabaseHelper.assertDetached(
|
||||||
tm().createQueryComposer(TestEntity.class)
|
tm().createQueryComposer(TestEntity.class)
|
||||||
.where("name", Comparator.EQ, "alpha")
|
.where("name", Comparator.EQ, "alpha")
|
||||||
.getSingleResult()))
|
.getSingleResult())))
|
||||||
.isEqualTo(alpha);
|
.isEqualTo(alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +174,7 @@ public class QueryComposerTest {
|
||||||
.createQueryComposer(TestEntity.class)
|
.createQueryComposer(TestEntity.class)
|
||||||
.where("name", Comparator.GT, "alpha")
|
.where("name", Comparator.GT, "alpha")
|
||||||
.stream()
|
.stream()
|
||||||
|
.map(DatabaseHelper::assertDetached)
|
||||||
.collect(toImmutableList())))
|
.collect(toImmutableList())))
|
||||||
.containsExactly(bravo, charlie);
|
.containsExactly(bravo, charlie);
|
||||||
assertThat(
|
assertThat(
|
||||||
|
@ -185,6 +184,7 @@ public class QueryComposerTest {
|
||||||
.createQueryComposer(TestEntity.class)
|
.createQueryComposer(TestEntity.class)
|
||||||
.where("name", Comparator.GTE, "bravo")
|
.where("name", Comparator.GTE, "bravo")
|
||||||
.stream()
|
.stream()
|
||||||
|
.map(DatabaseHelper::assertDetached)
|
||||||
.collect(toImmutableList())))
|
.collect(toImmutableList())))
|
||||||
.containsExactly(bravo, charlie);
|
.containsExactly(bravo, charlie);
|
||||||
assertThat(
|
assertThat(
|
||||||
|
@ -194,6 +194,7 @@ public class QueryComposerTest {
|
||||||
.createQueryComposer(TestEntity.class)
|
.createQueryComposer(TestEntity.class)
|
||||||
.where("name", Comparator.LT, "charlie")
|
.where("name", Comparator.LT, "charlie")
|
||||||
.stream()
|
.stream()
|
||||||
|
.map(DatabaseHelper::assertDetached)
|
||||||
.collect(toImmutableList())))
|
.collect(toImmutableList())))
|
||||||
.containsExactly(alpha, bravo);
|
.containsExactly(alpha, bravo);
|
||||||
assertThat(
|
assertThat(
|
||||||
|
@ -203,6 +204,7 @@ public class QueryComposerTest {
|
||||||
.createQueryComposer(TestEntity.class)
|
.createQueryComposer(TestEntity.class)
|
||||||
.where("name", Comparator.LTE, "bravo")
|
.where("name", Comparator.LTE, "bravo")
|
||||||
.stream()
|
.stream()
|
||||||
|
.map(DatabaseHelper::assertDetached)
|
||||||
.collect(toImmutableList())))
|
.collect(toImmutableList())))
|
||||||
.containsExactly(alpha, bravo);
|
.containsExactly(alpha, bravo);
|
||||||
}
|
}
|
||||||
|
@ -226,6 +228,7 @@ public class QueryComposerTest {
|
||||||
tm().createQueryComposer(TestEntity.class)
|
tm().createQueryComposer(TestEntity.class)
|
||||||
.where("val", Comparator.EQ, 2)
|
.where("val", Comparator.EQ, 2)
|
||||||
.first()
|
.first()
|
||||||
|
.map(DatabaseHelper::assertDetached)
|
||||||
.get()))
|
.get()))
|
||||||
.isEqualTo(bravo);
|
.isEqualTo(bravo);
|
||||||
}
|
}
|
||||||
|
@ -240,6 +243,7 @@ public class QueryComposerTest {
|
||||||
.where("val", Comparator.GT, 1)
|
.where("val", Comparator.GT, 1)
|
||||||
.orderBy("val")
|
.orderBy("val")
|
||||||
.stream()
|
.stream()
|
||||||
|
.map(DatabaseHelper::assertDetached)
|
||||||
.collect(toImmutableList())))
|
.collect(toImmutableList())))
|
||||||
.containsExactly(bravo, alpha);
|
.containsExactly(bravo, alpha);
|
||||||
}
|
}
|
||||||
|
|
|
@ -395,6 +395,17 @@ public class TransactionManagerTest {
|
||||||
.isEqualTo("Expected at most one entity of type TestEntity, found at least two");
|
.isEqualTo("Expected at most one entity of type TestEntity, found at least two");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestOfyAndSql
|
||||||
|
void mutatedObjectNotPersisted() {
|
||||||
|
tm().transact(() -> tm().insert(theEntity));
|
||||||
|
tm().transact(
|
||||||
|
() -> {
|
||||||
|
TestEntity e = tm().loadByKey(theEntity.key());
|
||||||
|
e.data = "some other data!";
|
||||||
|
});
|
||||||
|
assertThat(tm().transact(() -> tm().loadByKey(theEntity.key())).data).isEqualTo("foo");
|
||||||
|
}
|
||||||
|
|
||||||
private static void assertEntityExists(TestEntity entity) {
|
private static void assertEntityExists(TestEntity entity) {
|
||||||
assertThat(tm().transact(() -> tm().exists(entity))).isTrue();
|
assertThat(tm().transact(() -> tm().exists(entity))).isTrue();
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,6 @@ import google.registry.model.transfer.TransferStatus;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
import google.registry.schema.tld.PremiumListDao;
|
import google.registry.schema.tld.PremiumListDao;
|
||||||
import google.registry.tmch.LordnTaskUtils;
|
import google.registry.tmch.LordnTaskUtils;
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
@ -119,7 +118,6 @@ import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import org.hibernate.Hibernate;
|
|
||||||
import org.joda.money.CurrencyUnit;
|
import org.joda.money.CurrencyUnit;
|
||||||
import org.joda.money.Money;
|
import org.joda.money.Money;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
@ -1065,9 +1063,6 @@ public class DatabaseHelper {
|
||||||
// Force the session to be cleared so that when we read it back, we read from Datastore
|
// Force the session to be cleared so that when we read it back, we read from Datastore
|
||||||
// and not from the transaction's session cache.
|
// and not from the transaction's session cache.
|
||||||
tm().clearSessionCache();
|
tm().clearSessionCache();
|
||||||
for (R resource : resources) {
|
|
||||||
transactIfJpaTm(() -> tm().loadByEntity(resource));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1284,9 +1279,6 @@ public class DatabaseHelper {
|
||||||
ImmutableList.Builder<Object> result = new ImmutableList.Builder<>();
|
ImmutableList.Builder<Object> result = new ImmutableList.Builder<>();
|
||||||
for (Class<?> entityClass : entityClasses) {
|
for (Class<?> entityClass : entityClasses) {
|
||||||
for (Object object : jpaTm().loadAllOf(entityClass)) {
|
for (Object object : jpaTm().loadAllOf(entityClass)) {
|
||||||
// Initialize and detach the objects so they can be used for comparison later
|
|
||||||
initializeHibernateObject(object);
|
|
||||||
jpaTm().getEntityManager().detach(object);
|
|
||||||
result.add(object);
|
result.add(object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1295,20 +1287,6 @@ public class DatabaseHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes all fields in a Hibernate proxy object so it can be used outside of a transaction.
|
|
||||||
*/
|
|
||||||
private static void initializeHibernateObject(Object object) {
|
|
||||||
for (Field field : object.getClass().getDeclaredFields()) {
|
|
||||||
field.setAccessible(true);
|
|
||||||
try {
|
|
||||||
Hibernate.initialize(field.get(object));
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
// shouldn't happen since we set the field to be accessible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads (i.e. reloads) the specified entity from the DB.
|
* Loads (i.e. reloads) the specified entity from the DB.
|
||||||
*
|
*
|
||||||
|
@ -1373,5 +1351,17 @@ public class DatabaseHelper {
|
||||||
return transactIfJpaTm(() -> tm().loadByKeysIfPresent(keys));
|
return transactIfJpaTm(() -> tm().loadByKeysIfPresent(keys));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In JPA mode, assert that the given entity is detached from the current entity manager.
|
||||||
|
*
|
||||||
|
* <p>Returns the original entity object.
|
||||||
|
*/
|
||||||
|
public static <T> T assertDetached(T entity) {
|
||||||
|
if (!tm().isOfy()) {
|
||||||
|
assertThat(jpaTm().getEntityManager().contains(entity)).isFalse();
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
private DatabaseHelper() {}
|
private DatabaseHelper() {}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue