How to avoid initializing a Hibernate proxy when only the entity id is needed

只愿长相守 提交于 2020-06-25 03:59:25

问题


For a @ManyToOne relation in JPA entity I'm only interested in the actual id reference, not to fetch the whole model associated with the relation.

Take for example these Kotlin JPA entities:

@Entity
class Continent(
        @Id
        var id: String,
        var code: String,
        var name: String
) : Comparable<Continent> {

    companion object {
        private val COMPARATOR = compareBy<Continent> { it.id }
    }

    override fun compareTo(other: Continent): Int {
        return COMPARATOR.compare(this, other)
    }
}

@Entity
class Country(
        @Id
        var id: String,
        var alpha2Code: String,
        var alpha3Code: String,
        var name: String,
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "continent_id")
        var continent: Continent

) : Comparable<Country> {

    companion object {
        private val COMPARATOR = compareBy<Country> { it.id }
    }

    override fun compareTo(other: Country): Int {
        return COMPARATOR.compare(this, other)
    }
}

Now when I access country.continent.id from my Kotlin code the full Continent is actually queried from the database. This is overkill as I'm only interested in the Continent.id.

I've tried adding @Access(AccessType.PROPERTY) like:

@Entity
class Continent(
        @Id
        @Access(AccessType.PROPERTY)
        var id: String,

but it doesn't make a difference. The whole Continent is still queried from the database.

I tried the @Access(AccessType.PROPERTY) as it was mentioned in other posts (like Hibernate one-to-one: getId() without fetching entire object), but I already noticed mixed feedback about this.

I'm using Hibernate 5.3.7.Final with Kotlin 1.3.0.

I wonder if 1) the @Access(AccessType.PROPERTY) approach is correct and 2) should this also be working with Kotlin? Maybe the way Kotlin generated the Java code is causing an issue?

UPDATE

I created a simple test project proving the continent is being queried. https://github.com/marceloverdijk/hibernate-proxy-id

The project contains a simple test retrieving country.continent.id and SQL logging is enabled. From logging can be seen the continent is queried.

UPDATE 2

I've created https://youtrack.jetbrains.net/issue/KT-28525 for this.


回答1:


This behavior is defined by the JPA spec which requires an association to be fetched upon accessing any property, even the identifier.

Traditionally, Hibernate does not initialize an entity proxy when accessing its identifier, but this behavior was not consistent with the JPA spec, hence the need for explicitly disabling this JPA compliance strategy.

In fact, I created these two test cases in Hibernate ORM and everything works as expected:

  • ManyToOneLazyLoadingByIdJpaComplianceTest
  • ManyToOneLazyLoadingByIdTest

By default, the Proxy is not initialized when only the id is accessed.

This is the test:

Continent continent = doInJPA( this::entityManagerFactory, entityManager -> {
    Country country = entityManager.find( Country.class, 1L );

    country.getContinent().getId();

    return country.getContinent();
} );

assertEquals( 1L, (long) continent.getId());

assertProxyState( continent );

By default, this is the expected behavior:

protected void assertProxyState(Continent continent) {
    try {
        continent.getName();

        fail( "Should throw LazyInitializationException!" );
    }
    catch (LazyInitializationException expected) {

    }
}

However, if we switch to JPA compatibility moe:

<property name="hibernate.jpa.compliance.proxy" value="false"/>

This is what we get:

protected void assertProxyState(Continent continent) {
    assertEquals( "Europe", continent.getName() );
}

Hence, everything works as expected.

The problem comes from Kotlin or Spring Data JPA. You need to further investigate it and see why the Proxy gets initialized.

Most likely it's because of a toString or compare implementation added to the Continent entity.




回答2:


I had a similar problem. Turns out all @Entity annotated classes must be "open". Otherwise, Hibernate won't be able to create proxy subclasses, and therefore won't be able to lazy-load your entities.




回答3:


As Adrien Dos Reis mentioned, Hibernate requires all classes annotated with @Entity must be open (ie, not final). Instead of having all the tedious work of manually making classes and properties open, just add the all-open plugin to kotlin-maven-plugin as follows:

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>

    <configuration>
        <args>
            <arg>-Xjsr305=strict</arg>
        </args>

        <compilerPlugins>
            <plugin>all-open</plugin>
            <plugin>jpa</plugin>
        </compilerPlugins>

        <pluginOptions>
            <option>all-open:annotation=javax.persistence.Entity</option>
        </pluginOptions>
    </configuration>

    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-allopen</artifactId>
            <version>${kotlin.version}</version>
        </dependency>

        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-noarg</artifactId>
            <version>${kotlin.version}</version>
        </dependency>
    </dependencies>
</plugin>

Notice that option all-open:annotation=javax.persistence.Entity has been added to all-open plugin. This causes all classes annotated with @Entity to be open by default. For detailed information, refer to https://kotlinlang.org/docs/reference/compiler-plugins.html#all-open-compiler-plugin



来源:https://stackoverflow.com/questions/53466913/how-to-avoid-initializing-a-hibernate-proxy-when-only-the-entity-id-is-needed

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!