开发者

Can you pass substitution variables to Hibernate mapping files?

I had asked this question on the Hibernate forums a while back, but as of yet have not received a reply: https://forum.hibernate.org/viewtopic.php?f=1&t=1008487

I am trying to figure out if there's some way to specify variables or properties during Hibernate configuration in order to modify the details of my mapping files.

For instance, say I have the following:

<hibernate-mapping default-access="field">

  <id name="${standardIdName}" column="id" type="long">
     <generator class="sequence">
        <param name="sequence">warning_seq</param>
     </generator>
  </id>

  <version name="version" column="version" />

I would like to have the value for id's name attribute set to ${standardIdName} and then pass in that ${standardIdName} during application startup. This is very similar to what Spring allows you to do via PropertyConfigurer.

The real use case driving this is that I would like to be able to have all of my relationships cascade during certain phases of testing.

If I am testing Child class and it needs to have a Parent class reference, I can use the test data builder pattern and a single hibernate save call to persist my object graph:

public class Parent {
    ...
}

public class Child {
    private Parent parent; 
    ...
}

My test does something like:

Child child = new ChildBuilder().build() //Builds child class, a "dummy" Parent class and gives the Child that "Dummy" Parent class.
childRepository.save(child);

In order for that second line to work, saving both the Child and the Parent, I need my mapping file to be something like this, where the Parent creation will be cascaded:

<hibernate-mapping>
    <class name="Child" table="child">
        <key column="id" />

        <many-to-one name="Parent" column="parent_id开发者_如何学JAVA" not-null="true"
            class="parent" cascade="all"/>
    </class>
</hibernate-mapping>

But I really shouldn't have Child cascading up to Parent in production. My proposed solution would be something like:

<hibernate-mapping>
    <class name="Child" table="child">
        <key column="id" />

        <many-to-one name="parent" column="parent_id" not-null="true"
            class="parent" cascade="${cascadeType}"/>
    </class>
</hibernate-mapping>

Where cascadeType is passed in during Hibernate configuration.

Is there any functionality like this in Hibernate?

Thanks!

Leo


I could be wrong, but as far as I remember Hibernate doesn't have support for placeholder like Spring's PropertyConfigurer. I think I understand what you are trying to accomplish here, but if you are changing your HBM files for the sake of testing purposes, how can you ensure what you have changed reflects the production scenario? If you get your cascade set up wrongly in testing, then you are essentially writing testcases to test the wrong thing. I'm sure you have a valid reason to do so, but from what you explained here, it sounds like a risk to me. When I write testcases, they test the actual production code. If you want to reset your database or manipulate it for testing purpose, I would recommend you to use DBUnit or something equivalent rather than futzing around with your HBM files.


I ended up figuring out a solution. I documented that on my project's internal wiki page, but will copy it all here:

The Problem

Builders allow us to create complex object graphs quickly, without specifying the details for all the objects created. The benefit of this approach is blogged elsewhere and outside the scope of this answer.

One of the drawbacks of these Builder-built classes is that we are creating large, "detached" (see Hibernate's definition) object graphs that need to be re-attached to a Hibernate session once we tell a repository to save.

If we've modeled our relationships correctly, most ManyToOne relationships will not be automatically persisted and saving an entity with ManyToOne dependencies will present you with the following error message:

org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing

The introduction of a ManyToOneDependencySaver helps resolve these transient instance problems:

public interface ManyToOneDependencySaver<E, T extends GenericRepository<?, ?>> {

    T saveDependencies(E entity);

    ManyToOneDependencySaver<E, T> withRepository(T repository);

}

Let's say we have the following object model:

http://s15.postimage.org/3s1oxboor/Object_Model.png

And lets say we're using a builder in a test to create an instance of SomeSimpleEntity that we ultimately need to save (integration test):

@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 is going to fail on this.idObjectRepository.saveOrUpdate(someSimpleEntity); as SomeSimpleEntity has a required field of type FirstManyToManyEntity and the instance the builder created for FirstManyToManyEntity is not yet a persisted entity. Our cascades are not set to go up the graph.

Previously to get past this problem we would need to do this:

@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());

}

This works, but our nice fluent interface has broken and we're specifying objects that aren't interesting parts of the test. This uncessarily couples this SomeSimpleEntity test to FirstManyToOneEntity.

Using the ManyToOneDependencySaver, we can avoid this:

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());

    }
}

This may seem like no big deal in this example, but some of our objects have deep graphs that can create tons of dependencies. Using the ManyToOneDependencySaver drastically reduces the size of your test and increases its readability.

The implementation of the 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());
        }
    }
}

Hopefully that helps someone else out!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜