Ok so I get this weird issue that I can't fix.
I have 3 entities ( i will write things that only matters imo)
#Data
#Entity // all # are in javax
#Table(name = "a", schema = "pl")
#SequenceGenerator(...)
public class A extends BaseEntity {
#OneToMany(mappedBy = "pk.a")
private Set<ABRel> Bs = new HashSet<ABRel>();
}
#Getter
#Setter
#Entity
#Table(name = "a_b_rel", schema = "pl")
public class ABRel implements IEntity {
#EmbeddedId
private OfferOrderProjectRelId pk;
public OfferOrderProjectRel(B b, A a) {
if (a == null || b == null) {
throw new IllegalArgumentException("B orA equals null");
}
B.addA(this); // this methods just adds ABRel to sets in A and B
A.addB(this);
pk = new ABRelId(b, a);
}
}
#Getter
#Setter
#Embeddable
#EqualsAndHashCode
public class OfferOrderProjectRelId implements Serializable {
#ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.REFRESH })
#JoinColumn(name = "b_id")
private B b;
#ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.REFRESH })
#JoinColumn(name = "a_id")
private A a;
public ABRelId(B b, A a) {
setB(b);
setA(a);
}
}
#Data
#Entity (it has javax import)
#Table(name = "b", schema = "pl")
#SequenceGenerator(...)
public class B extends BaseEntity {
#OneToMany(mappedBy = "pk.b")
private Set<ABRel> As = new HashSet<ABRel>();
#NotBlank
#Length(max = 10000)
#Column(name = "type", length = 10000, nullable = false)
private String type;
}
ABRel and ABRelId have private contructor (ABRel() and ABRelId()) but not sure if it matters. Entities are working just fine, so I don't think somethink is wrong with them but meaby I am wrong.
So I'm tryin to add criteria by B.type for my filters. Criteria are made "in" (not sure how to say it :) ) A.class. So here's criteria that I'm tryin to add in my dao ( I can add this to criteria not detached one if someone ask):
DetachedCriteria idCriteria = DetachedCriteria.forClass(A.class, "a");
idCriteria.createAlias("Bs", "btype", JoinType.LEFT_OUTER_JOIN, Restrictions.eq("Bs.pk.B.type", "someType"));
Criteria criteria = getSession().createCriteria(A.class, "a");
criteria.add(Subqueries.propertyIn("id", idCriteria));
What I am tryin to achieve is to get all ABRels that have some specified B.type, then I will have to count it somehow, but this is not my issue atm. I have to use criteria, can't use any HQL. I also read that hibernate has some kind of bug with creating alias beetwen entity and its embedded so I can't make it too (probably thats why I am having to much trouble with it). So any ideas? I'm running out of option so any help would be great!
I almost forget, I'm getting this error
org.hibernate.HibernateException: Unknown entity: null at
org.hibernate.loader.criteria.CriteriaQueryTranslator.getPropertyMapping(CriteriaQueryTranslator.java:638)
at
org.hibernate.loader.criteria.CriteriaQueryTranslator.getType(CriteriaQueryTranslator.java:587)
at
org.hibernate.loader.criteria.CriteriaQueryTranslator.getTypeUsingProjection(CriteriaQueryTranslator.java:569)
at
org.hibernate.loader.criteria.CriteriaQueryTranslator.getTypedValue(CriteriaQueryTranslator.java:627)
at
org.hibernate.criterion.SimpleExpression.getTypedValues(SimpleExpression.java:100)
at
org.hibernate.loader.criteria.CriteriaQueryTranslator.getQueryParameters(CriteriaQueryTranslator.java:335)
at
org.hibernate.criterion.SubqueryExpression.createAndSetInnerQuery(SubqueryExpression.java:151)
at
org.hibernate.criterion.SubqueryExpression.toSqlString(SubqueryExpression.java:68)
UPDATE:
I have added sth like this
criteria.createAlias("As", "oorel", JoinType.LEFT_OUTER_JOIN);
criteria.createAlias("oorel.pk.b", "order", JoinType.LEFT_OUTER_JOIN, Restrictions.eq("type", "order"));
And now I'm getting new error(it's in my native language so i will try to translate it) its postgres and hibernate exception :
Column index out of range: 1, number of columns: 0
Sorry for my bad english and thank you in advance.
This is unfortunetly a hibernate bug which havent been fixed yet. This error appears because hibernate is not able to create alias via entity that contains composite key.
Related
I try to migrate a SpringBoot application to SpringBoot 3. Sprinboot 3 use Hibernate 6.
My application refuse to start because of the following error
Caused by: java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" because the return value of "java.util.Map.get(Object)" is null
at org.hibernate.envers.configuration.internal.metadata.AuditMetadataGenerator.addJoins(AuditMetadataGenerator.java:206)
at org.hibernate.envers.configuration.internal.metadata.AuditMetadataGenerator.generateSecondPass(AuditMetadataGenerator.java:409)
at org.hibernate.envers.configuration.internal.EntitiesConfigurator.configure(EntitiesConfigurator.java:86)
at org.hibernate.envers.boot.internal.EnversServiceImpl.initialize(EnversServiceImpl.java:129)
at org.hibernate.envers.boot.internal.AdditionalJaxbMappingProducerImpl.produceAdditionalMappings(AdditionalJaxbMappingProducerImpl.java:92)
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:329)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1350)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1421)
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:66)
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:376)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:352)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1797)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1747)
... 110 common frames omitted
After digging in the Envers code it appear that the problem is located in the org.hibernate.envers.configuration.internal.metadata.AuditMetadaGenerator class.
At line 337, there is a condition that let a audited class be referenced during the first pass of envers .
if ( entity.isJoinAware() ) {
final JoinAwarePersistentEntity joinAwareEntity = (JoinAwarePersistentEntity) entity;
createJoins( persistentClass, joinAwareEntity, auditingData );
addJoins( persistentClass, propertyMapper, auditingData, persistentClass.getEntityName(), mappingData, true );
}
private void createJoins(PersistentClass persistentClass, JoinAwarePersistentEntity entity, ClassAuditingData auditingData) {
final Iterator<org.hibernate.mapping.Join> joins = persistentClass.getJoinIterator();
final Map<org.hibernate.mapping.Join, Join> joinElements = new HashMap<>();
entityJoins.put( persistentClass.getEntityName(), joinElements );
....
This is this list that is called during the second pass line 206.
while ( joins.hasNext() ) {
final org.hibernate.mapping.Join join = joins.next();
final Join entityJoin = entityJoins.get( entityName ).get( join );
Here entityJoins.get(entityName) return null for one of my entity.
This entity is correctly annotated with #Audited and extend from another Entity like this:
#Entity
#Table(name = "a")
#Audited
#DiscriminatorValue("DISCRIMINATOR")
public class A extends B {
//...
}
#Entity
#Table(name = "b")
#Inheritance(strategy = InheritanceType.JOINED)
#Audited
public abstract class B {
//...
}
In my comprehension, specifying the Inheritance with a value of InheritanceType.JOINED make envers create a JoinedSubclassPersistentEntity that itself inherit from PersistentEntity.
This PersistentEntity has a method :
public boolean isJoinAware() {
return false;
}
that is overriden by it's children like RootPersistentEntity by :
#Override
public boolean isJoinAware() {
return true;
}
This override is not made by JoinedSubclassPersistentEntity which is the class generated when using Joined inheritence strategy.
This cause my entity to not be add to the first pass but still processed by the second pass.
So the question is ? Is it a bug in Envers ? Can i used Joined inheritence strategy with a #Audited class ?
It was working well in hibernate 5.6.
[Edit]:
I managed to reproduce the error with a simple test class :
#Entity
#Audited
#DiscriminatorValue("OPTION")
public class Child extends Parent{
private String propA;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "recursive_children_child", joinColumns = {#JoinColumn(name = "child_id", nullable = false, updatable = false)}, inverseJoinColumns = {#JoinColumn(name = "recursive_id", nullable = false, updatable = false)})
#NotAudited
#OrderBy("propA DESC")
private List<Child> children = new ArrayList<>();
#ManyToOne(fetch = FetchType.LAZY)
#JoinTable(name = "recursive_children_child", joinColumns = {#JoinColumn(name = "recursive_id")}, inverseJoinColumns = {#JoinColumn(name = "child_id")})
#NotAudited
private Child recursiveChild;
public Child(String propA) {
this.propA = propA;
}
public Child() {
}
public String getPropA() {
return propA;
}
public void setPropA(String propA) {
this.propA = propA;
}
}
The problem seems to be with the recursive relationship in the Child class. Ember try to audit the relationship despite the presence of #Audited annotation.
Link to a project that reproduce the bug => https://github.com/scandinave/envers6-migration-bug
I finally found the problem by reading the Jakarta Persistence Spec.
The JoinTable annotation is used in the mapping of entity associations. A JoinTable annotation is
specified on the owning side of the association
In my project there is a #JoinTable annotation on both side of the association. This was working before Hibernate 6, but not now. I just had to remove the #JoinTable not needed to solve the error.
Question
If I have declared my (composite) primary key using #IdClass, how do I write my #Query to be able to issue a DELETE query using a Collection<MyIdClass> ?
Secondary question
Will the CASCADE actually trigger the deletion of the associated AnotherEntity despite using #Query?
Current model
#Entity
#Table(name = "myentity")
#JsonIgnoreProperties(ignoreUnknown = true)
#Data
#NoArgsConstructor
#AllArgsConstructor
#Builder
#IdClass(MyIdClass.class)
public class MyEntity {
#Id
#Column(updatable = false)
private String foo;
#Id
#Column(updatable = false)
private String bar;
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "my_foreign_key", referencedColumnName = "external_pk")
private AnotherEntity anotherEntity;
}
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class MyIdClass implements Serializable {
private String foo;
private String bar;
}
#Entity
#Table(name = "anotherentity")
#JsonIgnoreProperties(ignoreUnknown = true)
#Data
#SuperBuilder
#NoArgsConstructor
#AllArgsConstructor
public class AnotherEntity {
#Id
#Column(name = "external_pk", nullable = false, updatable = false)
private String externalPk;
}
What I've read
A few resources:
https://www.baeldung.com/spring-data-jpa-query
https://www.baeldung.com/spring-data-jpa-delete
https://stackoverflow.com/a/36765129/9768291
And I also found this SO question which seemed very close to what I'm looking for, but unfortunately there are no answers.
Goal
Something similar to:
#Repository
public interface MyCRUDRepository extends CrudRepository<MyEntity, MyIdClass> {
#Modifying
#Query("DELETE FROM myentity m WHERE m IN ?1") // how do I write this?
void deleteAllWithIds(Collection<MyIdClass> ids);
}
Ultimately, I want to do this to batch my DELETE requests to increase the performance.
Pitfalls I'm trying to avoid
I know there is a deleteAll(Iterable<? extends MyEntity>) but then I need to actually have those entities to begin with, which would require extra calls to the DB.
There is also deleteById(MyIdClass), but that actually always issues a findById before sending a single DELETE statement as a transaction: not good for the performance!
Potentially irrelevant precision
I'm not sure if that can help, but my JPA provider is EclipseLink. My understanding is that there are properties for batching requests, and that's ultimately what I'm aiming to use.
However, I'm not entirely sure what are the internal requirements for that batching to happen. For example, if I did a deleteById in a for-loop, would the alternating SELECT and DELETE statements prevent the batching from happening? The documentation is quite scarce about that.
If you're positive IdClass is a better choice than EmbeddedId in your situation, you could add an extra mapping to MyEntity :
#Embedded
#AttributeOverrides({
#AttributeOverride(name = "foo",
column = #Column(name = "foo", insertable = false, updatable = false)),
#AttributeOverride(name = "bar",
column = #Column(name = "bar", insertable = false, updatable = false))})
private MyIdClass id;
and use it in you repository:
#Modifying
#Query("DELETE FROM MyEntity me WHERE me.id in (:ids)")
void deleteByIdIn(#Param("ids") Collection<MyIdClass> ids);
This will generate a single query: delete from myentity where bar=? and foo=? [or bar=? and foo=?]..., resulting in this test to pass (with following table records insert into myentity(foo,bar) values ('foo1', 'bar1'),('foo2', 'bar2'),('foo3', 'bar3'),('foo4', 'bar4');):
#Test
#Transactional
void deleteByInWithQuery_multipleIds_allDeleted() {
assertEquals(4, ((Collection<MyEntity>) myEntityRepository.findAll()).size());
MyIdClass id1 = new MyIdClass("foo1", "bar1");
MyIdClass id2 = new MyIdClass("foo2", "bar2");
assertDoesNotThrow(() -> myEntityRepository.deleteByIdIn(List.of(id1, id2)));
assertEquals(2, ((Collection<MyEntity>) myEntityRepository.findAll()).size());
}
I think you are looking for something that will generate a query like this
delete from myentity where MyIdClass in (? , ? , ?)
You can try from this post, it may help you.
This answer provided great insight, but it seems like the approach only works for Hibernate. EclipseLink, which is the JPA Provider that I'm forced to use, would keep throwing an error at me, for the same code.
The only working solution I found is the following hack:
JPA Query for Spring #Repository
#Repository
public interface MyCRUDRepository extends CrudRepository<MyEntity, MyIdClass> {
#Modifying
#Query("DELETE FROM myentity m WHERE CONCAT(m.foo, '~', m.bar) IN :ids")
void deleteAllWithConcatenatedIds(#Param("ids") Collection<String> ids);
}
Associated index for the DB (Postgres)
DROP INDEX IF EXISTS concatenated_pk_index;
CREATE UNIQUE INDEX concatenated_pk_index ON myentity USING btree (( foo || '~' || bar ));
Explanation
Since EclipseLink refuses to properly treat my #IdClass, I had to adapt the service to concatenate the composite key into a single String. Then, in Postgres, you can actually create an index on that concatenation of different composite key columns.
Labeling the index as UNIQUE will greatly improve the performance of that query, but should only be done if you are sure that the concatenation will be unique (in my case it is since I'm using all the columns of the composite key).
The calling service then only has to do something like String.join("~", dto.getFoo(), dto.getBar()) and to collect all of those into the list that will be passed to the repository.
My application operates in a weird way. In fact when debugging I can clearly see that my objects get persisted on the DB but when in running mode, JPA does not seem to persist them. The following is a code snippet from my source code :
#Entity
#Table(name = "a", schema="myschema")
public class A implements Serializable{
#Id
#Column(name = "id", unique = true, nullable = false)
#NotNull(message = "id can not be null")
private UUID id = UUID.randomUUID();
#JsonIgnore
// #ManyToMany(fetch = FetchType.EAGER)
#ManyToMany
#JoinTable(name = "a_b", joinColumns = { #JoinColumn(name = "a_id") },
inverseJoinColumns = { #JoinColumn(name = "b_id") }
)
private List<b> blist = new ArrayList<>();
//omitted source code
}
#Entity
#Table(name = "b", schema="myschema")
public class B implements Serializable{
#Id
#Column(name = "id", unique = true, nullable = false)
#NotNull(message = "id can not be null")
private Integer id;
#ManyToMany(mappedBy = "blist")
#JsonIgnore
private List<A> alist = new ArrayList<>();
//omitted source code
}
#Service
public class MyService{
//omitted source code
public Optional<A> createCopy(A source, int bId) {
B b = bRepository.findById(bId);
A copy_ = this.copy(source);
A target = aRepository.save(copy_);
b.getAlist().add(target);
bRepository.save(b);
return Optional.of(target);
}
private A copy(A source){
A target = new A();
//copy one to one from source to target
target.setB(source.getB());
return target;
}
}
When debugging I can see that after making a call to MyService#createCopy() method, a new record is persisted in the DB within the table a_b. However when I simply run the server and then proceeds with a call to MyService#createCopy(), no additional record in a_b gets persisted.
Anyone ever encountered such an odd behavior before? And if yes how can one solve it please?
It's look like you have not set your new list again to b objet in your service, That's why you are not able to get records in within the table a_b. Change your service as below.
#Service
public class MyService{
//omitted source code
public Optional<A> createCopy(A source, int bId) {
B b = bRepository.findById(bId);
A copy_ = this.copy(source);
A target = aRepository.save(copy_);
List<A> alist= b.getAlist();// Here b.getAlist() will return a independent list and adding any item within this list will not affect the list inside the object b.
alist.add(target);
b.setAlist(alist);// setting the new list to object b.
bRepository.save(b);
return Optional.of(target);
}
Try it once and let me know if it works.
I managed to fix the problem as following (Thanks to #Ajit hint):
private A copy(A source){
A target = new A();
//copy one to one from source to target
//instead of target.setB(source.getB()); I did this
List<B> targetBlist = target.getBlist();
source.getBlist().forEach(bObj -> {
Optional<B> optB = bRepository.findById(bObj.getId());
if(optB.isPresent())
targetBlist.add(optB.get());
});
return target;
}
Now that my problem is fixed I still cannot figure out what was the actual issue. It smells like this has to do something with lazy loading. In fact the only case where the problem does not occur is when I do inspect the source#bList before proceeding to the copy(). If anyone could clarify this to me I will be thankful.
I am using Hibernate 5.1.2
I have run into an unexpected problem that I can't seem to work around. Here's the summary of my data model:
dfip_project_version is my superclass table, and dfip_appln_proj_version is my subclass table. dfip_application contains a list of dfip_appln_proj_versions.
I have mapped this as follows:
#Table(name = "DFIP_PROJECT_VERSION")
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
public abstract class AbstractProjectVersion {
#Id #GeneratedValue
#Column(name = "PROJECT_VERSION_OID")
Long oid;
#Column(name = "PROJ_VSN_EFF_FROM_DTM")
Timestamp effFromDtm;
#Column(name = "PROJ_VSN_EFF_TO_DTM")
Timestamp effToDtm;
#Column(name = "PROJECT_VERSION_TYPE")
#Type(type = "project_version_type")
ProjectVersionType projectVersionType;
}
#Table(name = "DFIP_APPLN_PROJ_VERSION")
#Entity
class ApplicationProjectVersion extends AbstractProjectVersion {
#OneToOne
#JoinColumn(name = "APPLICATION_OID", nullable = false)
Application application;
public ApplicationProjectVersion() {
projectVersionType = ProjectVersionType.APPLICATION;
}
}
#Table(name = "DFIP_APPLICATION")
#Entity
class Application {
#Id #GeneratedValue
#Column(name = "APPLICATION_OID")
Long oid;
#OneToMany(mappedBy="application", orphanRemoval = true, fetch = FetchType.EAGER)
#Fetch(FetchMode.SELECT)
#Where(clause = "PROJ_VSN_EFF_TO_DTM is null")
List<ApplicationProjectVersion> applicationVersions = [];
}
I am using the #Where annotation so that only the current ApplicationProjectVersion is retrieved with the Application.
The problem with this is that Hibernate assumes that the column I am referencing is in the dfip_appl_proj_version table, when it's actually on the super-class table (dfip_project_version).
Here's what I tried so far to work around this limitation:
Attempt 1
I tried putting the #Where annotation onto the AbstractProjectVersion super-class, like so:
#Table(name = "DFIP_PROJECT_VERSION")
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Where(clause = "PROJ_VSN_EFF_TO_DTM is null")
public abstract class AbstractProjectVersion {
...etc...
}
This did nothing, as the WHERE clause does not seem to be noticed when retrieving the Application.
Attempt 2
I made the applicationVersions list on Application LAZY, and tried to map latestVersion manually like this:
#Table(name = "DFIP_APPLICATION")
#Entity
class Application {
#Id #GeneratedValue
#Column(name = "APPLICATION_OID")
Long oid;
#OneToMany(mappedBy="application", orphanRemoval = true, fetch = FetchType.LAZY)
#Fetch(FetchMode.SELECT)
List<ApplicationProjectVersion> applicationVersions = [];
#ManyToOne
#JoinColumnsOrFormulas([
#JoinColumnOrFormula(formula = #JoinFormula(value = "(APPLICATION_OID)", referencedColumnName="APPLICATION_OID")),
#JoinColumnOrFormula(formula = #JoinFormula(value = "(select apv.PROJECT_VERSION_OID from DFIP_PROJECT_VERSION pv, DFIP_APPLN_PROJ_VERSION apv where apv.PROJECT_VERSION_OID = pv.PROJECT_VERSION_OID and apv.APPLICATION_OID = APPLICATION_OID and pv.PROJ_VSN_EFF_TO_DTM is null)", referencedColumnName="PROJECT_VERSION_OID")),
])
ApplicationProjectVersion latestVersion;
}
This caused Hibernate to generate the following SQL (snippet):
from DFIP_APPLICATION this_
left outer join DFIP_APPLN_PROJ_VERSION applicatio2_
on (this_.APPLICATION_OID)=applicatio2_.APPLICATION_OID and
(select apv.PROJECT_VERSION_OID from DFIP_PROJECT_VERSION pv, DFIP_APPLN_PROJ_VERSION apv
where apv.PROJECT_VERSION_OID = pv.PROJECT_VERSION_OID and apv.APPLICATION_OID = this_.APPLICATION_OID
and pv.PROJ_VSN_EFF_TO_DTM is null)=applicatio2_.PROJECT_VERSION_OID
which resulted in ORA-01799: a column may not be outer-joined to a subquery.
If I can't specify a sub-query in my join formula, then I cannot join to the super-class manually...
Attempt 3
I noticed that usage of #JoinFormula makes Hibernate notice my #Where annotation on the super-class. So I tried the following:
#Table(name = "DFIP_PROJECT_VERSION")
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Where(clause = "PROJ_VSN_EFF_TO_DTM is null")
public abstract class AbstractProjectVersion {
...etc...
}
#Table(name = "DFIP_APPLICATION")
#Entity
class Application {
#Id #GeneratedValue
#Column(name = "APPLICATION_OID")
Long oid;
#OneToMany(mappedBy="application", orphanRemoval = true, fetch = FetchType.LAZY)
#Fetch(FetchMode.SELECT)
List<ApplicationProjectVersion> applicationVersions = [];
#ManyToOne
#JoinFormula(value = "(APPLICATION_OID)", referencedColumnName="APPLICATION_OID")
ApplicationProjectVersion latestVersion;
}
This generated the following SQL (snippet):
from DFIP_APPLICATION this_
left outer join DFIP_APPLN_PROJ_VERSION applicatio2_
on (this_.APPLICATION_OID)=applicatio2_.APPLICATION_OID and ( applicatio2_1_.PROJ_VSN_EFF_TO_DTM is null)
left outer join DFIP_PROJECT_VERSION applicatio2_1_ on applicatio2_.PROJECT_VERSION_OID=applicatio2_1_.PROJECT_VERSION_OID
This is almost correct! Unfortunately it is not valid SQL, since applicatio2_1_ is used before it is declared on the next line :(.
Now I am out of ideas, so any help would be appreciated. Is there a way to specify a WHERE clause that will bring in only the current ProjectVersion, without getting rid of my inheritance structure?
Related Hibernate issue ticket
I have a solution to this problem. I must admit, it ended up being a little more cumbersome than what I hoped for, but it does work quite well. I waited a couple of months before posting, to make sure that there are no issues and so far, I have not experienced any problems.
My entities are still mapped exactly as described in the question, but instead of using the problematic #Where annotation, I had to use #Filter annotation instead:
public class Application {
#OneToMany(mappedBy="application", orphanRemoval = true, fetch = FetchType.EAGER)
#Cascade([SAVE_UPDATE, DELETE, MERGE])
#Fetch(FetchMode.SELECT)
// Normally we'd just use the #Where(clause = "PROJ_VSN_EFF_TO_DTM is null"), but that doesn't work with collections of
// entities that use inheritance, as we have here.
//
// Hibernate thinks that PROJ_VSN_EFF_TO_DTM is a column on DFIP_APPLN_PROJ_VERSION table, but it is actually on the "superclass"
// table (DFIP_PROJECT_VERSION).
//
// B/c of this, we have to do the same thing with a Filter, which is defined on AbstractProjectVersion.
// NOTE: This filter must be explicitly enabled, which is currently achieved by HibernateForceFiltersAspect
//
#Filter(name="currentProjectVersionOnly",
condition = "{pvAlias}.PROJ_VSN_EFF_TO_DTM is null",
deduceAliasInjectionPoints=false,
aliases=[ #SqlFragmentAlias(alias = "pvAlias", table = "DFIP_PROJECT_VERSION") ]
)
List<ApplicationProjectVersion> projectVersions = [];
}
Since we are using a Filter, we must also define it:
// NOTE: This filter needs to be explicitly turned on with session.enableFilter("currentProjectVersionOnly");
// This is currently achieved with HibernateForceFiltersAspect
#FilterDef(name="currentProjectVersionOnly")
#Table(name = "DFIP_PROJECT_VERSION")
#Inheritance(strategy = InheritanceType.JOINED)
public abstract class AbstractProjectVersion {
}
And of course, we must enable it, since Hibernate does not have a setting to automatically turn on all filters.
To do this I created a system-wide Aspect, whose job is to enable specified filters before every call to any DAO:
/**
* Enables provided Hibernate filters every time a Hibernate session is openned.
*
* Must be enabled and configured explicitly from Spring XML config (i.e. no auto-scan here)
*
* #author Val Blant
*/
#Aspect
public class HibernateForceFiltersAspect {
List<String> filtersToEnable = [];
#PostConstruct
public void checkConfig() throws Exception {
if ( filtersToEnable.isEmpty() ) {
throw new IllegalArgumentException("Missing required property 'filtersToEnable'");
}
}
/**
* This advice gets executed before all method calls into DAOs that extend from <code>HibernateDao</code>
*
* #param jp
*/
#Before("#target(org.springframework.stereotype.Repository) && execution(* ca.gc.agr.common.dao.hibernate.HibernateDao+.*(..))")
public void enableAllFilters(JoinPoint jp) {
Session session = ((HibernateDao)jp?.getTarget())?.getSession();
if ( session != null ) {
filtersToEnable.each { session.enableFilter(it) } // Enable all specified Hibernate filters
}
}
}
And the corresponding Spring configuration:
<!-- This aspect is used to force-enable specified Hibernate filters for all method calls on DAOs that extend HibernateDao -->
<bean class="ca.gc.agr.common.dao.hibernate.HibernateForceFiltersAspect">
<property name="filtersToEnable">
<list>
<value>currentProjectVersionOnly</value> <!-- Defined in AbstractProjectVersion -->
</list>
</property>
</bean>
And there you have it - polymorphic #Where clause :).
since you are looking for #Where with inheritance, I assume you are trying to apply some SQL logic globally, maybe hibernate interceptor or SQL inspector would be a better fit for this type of requirement
I'm currently experiencing problems with my OneToMany/ManyToOne-Mapping. The mapping looks like this:
public class A implements Serializable {
#EmbeddedId
private AId id;
// Other stuff...
}
#Embeddable
public class AId implements Serializable {
#ManyToOne
#JoinColumn(name = "B_ID", nullable = false)
private B b;
// Other stuff...
}
public class B implements Serializable {
#OneToMany(mappedBy = "id.b")
private List<A> as;
// Other stuff...
}
If I try to access object B by using object A everything works just fine, but the inverse direction doesn't work at all. The relationship is always null.
A objectA = findAById(id);
B objectB = objectA.getB(); // OK
// But... for example
objectB.getAs(); // returns null
I wrote a small query to get all the As for an object B using its primary key:
SELECT as FROM B b, IN(b.as) as WHERE b.id = :id
This works perfectly, I get the expected result.
I checked what is persisted in the DB, too, and it's all right. Has anybody a clue why that relationship only works in one direction?
Regards,
Alex
that's because by default #onetomany has lazy fetch. You can fix that using this
fetch = FetchType.EAGER
public class B implements Serializable {
#OneToMany(mappedBy = "id.b", fetch = FetchType.EAGER)
private List<A> as;
// Other stuff...
}