问题
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