В итоге я нашел решение. Я задокументировал это на внутренней вики-странице моего проекта, но скопирую все это сюда:
Проблема
Построители позволяют нам быстро создавать сложные графы объектов без указания деталей для всех созданных объектов. Преимущество этого подхода опубликовано в другом месте и выходит за рамки этого ответа.
Один из недостатков этих классов, созданных Builder, заключается в том, что мы создаем большие, «отсоединенные» ( см. Определение Hibernate ) графы объектов, которые необходимо повторно присоединить к сеансу Hibernate, как только мы сообщим хранилище для сохранения.
Если мы смоделировали наши отношения правильно, большинство отношений ManyToOne не будут автоматически сохранены, и сохранение объекта с зависимостями ManyToOne приведет к следующему сообщению об ошибке:
org.hibernate.TransientObjectException: объект ссылается на несохраненный временный экземпляр - сохраните временный экземпляр перед сбросом
Внедрение ManyToOneDependencySaver помогает решить следующие временные проблемы экземпляров:
public interface ManyToOneDependencySaver<E, T extends GenericRepository<?, ?>> {
T saveDependencies(E entity);
ManyToOneDependencySaver<E, T> withRepository(T repository);
}
Допустим, у нас есть следующая объектная модель:
http://s15.postimage.org/3s1oxboor/Object_Model.png
Допустим, мы используем построитель в тесте для создания экземпляра SomeSimpleEntity, который в конечном итоге необходимо сохранить (интеграционный тест):
@Test
public void shouldSaveEntityAndAllManyToOnesWhenPropertiesAreDummiedUp() {
SomeSimpleEntity someSimpleEntity = new SomeSimpleEntityBuilder()
.withFieldsDummiedUp()
.build();
this.idObjectRepository.saveOrUpdate(someSimpleEntity);
Assert.assertNotNull("SimpleEntity's ID should not be null: ",
someSimpleEntity.getId());
Assert.assertNotNull(
"SimpleEntity.RequiredFirstManyToManyEntity's ID should not be null: ",
someSimpleEntity.getRequiredFirstManyToManyEntity().getId());
Assert.assertNotNull(
"SimpleEntity.RequiredSecondManyToManyEntity's ID should not be null: ",
someSimpleEntity.getRequiredSecondManyToManyEntity().getId());
Assert.assertNotNull(
"SimpleEntity.RequiredFirstManyToManyEntity.RequiredSecondManyToManyEntity's ID should not be null: ",
someSimpleEntity.getRequiredFirstManyToManyEntity()
.getRequiredSecondManyToManyEntity().getId());
}
Это будет сбой this.idObjectRepository.saveOrUpdate (someSimpleEntity); , поскольку SomeSimpleEntity имеет обязательное поле типа FirstManyToManyEntity, а экземпляр, созданный для FirstManyToManyEntity, еще не является персистентной сущностью. Наши каскады не настроены на увеличение графика .
Ранее чтобы обойти эту проблему нам нужно сделать это :
@Test
public void shouldSaveEntityAndAllManyToOnesWhenPropertiesAreDummiedUp() {
FirstManyToOneEntity firstManyToOneEntity = new FirstManyToOneEntity()
.withFieldsDummiedUp()
.build();
this.idObjectRepository.saveOrUpdate(firstManyToOneEntity); //Save object SomeSimpleEntity depends on
SomeSimpleEntity someSimpleEntity = new SomeSimpleEntityBuilder()
.withFieldsDummiedUp()
.withRequiredFirstManyToOneEntity(firstManyToOneEntity)
.build();
this.idObjectRepository.saveOrUpdate(someSimpleEntity); //Now save the SomeSimpleEntity
Assert.assertNotNull("SimpleEntity's ID should not be null: ",
someSimpleEntity.getId());
Assert.assertNotNull(
"SimpleEntity.RequiredFirstManyToOneEntity's ID should not be null: ",
someSimpleEntity.getRequiredFirstManyToManyEntity().getId());
}
Это работает, но наш хороший свободный интерфейс сломал , и мы указываем объекты, которые не являются интересными частями теста. Это без необходимости связывает этот тест SomeSimpleEntity с FirstManyToOneEntity .
Используя ManyToOneDependencySaver, мы можем избежать этого:
public class ManyToOneDependencySaver_saveDependenciesTests
extends
BaseRepositoryTest {
@Autowired
private IDObjectRepository idObjectRepository;
@Autowired
private ManyToOneDependencySaver<IDObject, IDObjectRepository> manyToOneDependencySaver;
@Test
public void shouldSaveEntityAndAllManyToOnesWhenPropertiesAreDummiedUp() {
SomeSimpleEntity someSimpleEntity = new SomeSimpleEntityBuilder()
.withFieldsDummiedUp()
.build();
this.manyToOneDependencySaver.withRepository(this.idObjectRepository)
.saveDependencies(someSimpleEntity)
.saveOrUpdate(someSimpleEntity);
Assert.assertNotNull("SomeSimpleEntity's ID should not be null: ",
someSimpleEntity.getId());
Assert.assertNotNull(
"SomeSimpleEntity.RequiredFirstManyToOneEntity's ID should not be null: ",
someSimpleEntity.getRequiredFirstManyToManyEntity().getId());
}
}
В этом примере это может показаться несущественным, но некоторые из наших объектов имеют глубокие графы, которые могут создавать тонны зависимостей. Использование ManyToOneDependencySaver значительно уменьшает размер вашего теста и повышает его читабельность.
Реализация ManyToOneDependencySaver?
@Repository
public class HibernateManyToOneDependencySaver<E, T extends GenericRepository<E, ?>>
implements ManyToOneDependencySaver<E, T> {
private static final Log LOG = LogFactory
.getLog(HibernateManyToOneDependencySaver.class);
protected T repository;
protected HibernateTemplate hibernateTemplate;
@Autowired
public HibernateManyToOneDependencySaver(
final HibernateTemplate hibernateTemplate) {
super();
this.hibernateTemplate = hibernateTemplate;
}
@Override
public T saveDependencies(final E entity) {
HibernateManyToOneDependencySaver.LOG.info(String.format(
"Gathering and saving Many-to-One dependencies for entity: %s",
entity));
this.saveManyToOneRelationshipsInDependencyOrderBeforeSavingThisEntity(entity);
return this.repository;
}
@Override
public ManyToOneDependencySaver<E, T> withRepository(final T aRepository) {
this.repository = aRepository;
return this;
}
private void saveManyToOneRelationshipsInDependencyOrderBeforeSavingThisEntity(
final E entity) {
SessionFactory sessionFactory = this.hibernateTemplate
.getSessionFactory();
@SuppressWarnings("unchecked")
Map<String, ClassMetadata> classMetaData = sessionFactory
.getAllClassMetadata();
HibernateManyToOneDependencySaver.LOG
.debug("Gathering dependencies...");
Stack<?> entities = ManyToOneGatherer.gather(
classMetaData, entity);
while (!entities.empty()) {
@SuppressWarnings("unchecked")
E manyToOneEntity = (E) entities.pop();
HibernateManyToOneDependencySaver.LOG.debug(String.format(
"Saving Many-to-One dependency: %s",
manyToOneEntity));
this.repository.saveOrUpdate(manyToOneEntity);
this.repository.flush();
}
}
}
public class ManyToOneGatherer {
private static final Log LOG = LogFactory
.getLog(HibernateManyToOneDependencySaver.class);
public static <T> Stack<T> gather(
final Map<String, ClassMetadata> classMetaData,
final T entity) {
ManyToOneGatherer.LOG.info(String.format(
"Gathering ManyToOne entities for entity: %s...", entity));
Stack<T> gatheredManyToOneEntities = new Stack<T>();
ClassMetadata metaData = classMetaData.get(entity
.getClass().getName());
EntityMetamodel entityMetaModel = ManyToOneGatherer
.getEntityMetaModel(metaData);
StandardProperty[] properties = entityMetaModel.getProperties();
for (StandardProperty standardProperty : properties) {
Type type = standardProperty.getType();
ManyToOneGatherer.LOG.trace(String.format(
"Examining property %s...", standardProperty.getName()));
if (type instanceof ManyToOneType) {
ManyToOneGatherer.LOG.debug(String.format(
"Property %s IS a ManyToOne",
standardProperty.getName()));
DirectPropertyAccessor propertyAccessor = new DirectPropertyAccessor();
@SuppressWarnings("unchecked")
T manyToOneEntity = (T) propertyAccessor.getGetter(
entity.getClass(), standardProperty.getName()).get(
entity);
ManyToOneGatherer.LOG.debug(String.format(
"Pushing ManyToOne property '%s' of value %s",
standardProperty.getName(), manyToOneEntity));
gatheredManyToOneEntities.push(manyToOneEntity);
ManyToOneGatherer.LOG.debug(String.format(
"Gathering and adding ManyToOnes for property %s...",
standardProperty.getName()));
ManyToOneGatherer.pushAll(ManyToOneGatherer.gather(
classMetaData, manyToOneEntity),
gatheredManyToOneEntities);
}
else {
ManyToOneGatherer.LOG.trace(String.format(
"Property %s IS NOT a ManyToOne",
standardProperty.getName()));
}
}
return gatheredManyToOneEntities;
}
private static EntityMetamodel getEntityMetaModel(
final ClassMetadata metaData) {
EntityMetamodel entityMetaModel = null;
if (metaData instanceof JoinedSubclassEntityPersister) {
JoinedSubclassEntityPersister joinedSubclassEntityPersister = (JoinedSubclassEntityPersister) metaData;
entityMetaModel = joinedSubclassEntityPersister
.getEntityMetamodel();
}
if (metaData instanceof SingleTableEntityPersister) {
SingleTableEntityPersister singleTableEntityPersister = (SingleTableEntityPersister) metaData;
entityMetaModel = singleTableEntityPersister
.getEntityMetamodel();
}
return entityMetaModel;
}
private static <T> void pushAll(final Stack<T> itemsToPush,
final Stack<T> stackToPushOnto) {
while (!itemsToPush.empty()) {
stackToPushOnto.push(itemsToPush.pop());
}
}
}
Надеюсь, это поможет кому-то еще!