JPA Criteria api join through embedded ID - java

I have the following entities:
#Entity
#Table(name = "place_revision")
public class PoiRevision {
#OneToMany(mappedBy = "pk.revision", cascade = {CascadeType.ALL})
private Collection<PoiRevisionCategory> categoryMapping;
// ...
}
#Entity
#Table(name = "place_revision__category")
#AssociationOverrides({
#AssociationOverride(name = "pk.revision",
joinColumns = #JoinColumn(name = "place_revision_id")),
#AssociationOverride(name = "pk.category",
joinColumns = #JoinColumn(name = "category_id"))
})
public class PoiRevisionCategory {
#EmbeddedId
private PoiRevisionCategoryId pk = new PoiRevisionCategoryId();
// ...
}
#Embeddable
public class PoiRevisionCategoryId implements Serializable {
#ManyToOne
private PoiRevision revision;
#ManyToOne
private Category category;
// ...
}
#Entity
#Table(name = "category")
public class Category {
#ManyToMany(targetEntity = Section.class, cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
#JoinTable(
name = "category__section",
joinColumns = #JoinColumn(name = "category_id"),
inverseJoinColumns = #JoinColumn(name = "section_id")
)
private Collection<Section> sections;
// ...
}
And want to select PoiRevisions that have Categories that have some Sections.
I'm using Spring-data Specification to query the database for these entities.
My intent is to write something like:
Specification<PoiRevision> spec = new Specification<PoiRevision>() {
#Override
public Predicate toPredicate(Root<PoiRevision> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> conditions = new ArrayList<>(CONDITION_COUNT);
CollectionJoin<PoiRevision, PoiRevisionCategory> mapping = root.join(PoiRevision_.categoryMapping);
// BROKEN here as we cannot use nested path for joins
Join<PoiRevisionCategory, Category> categories = mapping.join("pk.category");
conditions.add(categories.get("sections").in(sections));
// ...
return cb.and(conditions.toArray(new Predicate[] {}));
}
};
But we cannot use nested path for such joins as JPA provider (Hibernate, in my case) looks only for direct properties of PoiRevisionCategory class. And we cannot "join" embedded Id to our result set because it's not a manageable entity.
I'm really stuck with this issue which seems to be far from complicated when translated into SQL yet it has some complexity on the ORM-side.
Any idea is much appreciated.

After switching completely to metamodel API it became clearer and I was actually able to join embedded entity just like I tried and failed with string api.
So the correct way is just to join like one would normally do
Join<PoiRevisionCategory, PoiRevisionCategoryId> pk = mapping.join(PoiRevisionCategory_.pk);
Join<PoiRevisionCategoryId, Category> cats = pk.join(PoiRevisionCategoryId_.category);
CollectionJoin<Category, Section> sec = cats.join(Category_.sections);
conditions.add(sec.get(Section_.id).in(sections));
And it does the thing just fine!
What a relief.

Related

Invalid Identifier in Hibernate Filter defaultCondition when trying to access a field of one of it's subfields

I am working on setting up some entities for a project that I am working on. My issue is that I am getting an invalid identifier error on my hibernate filters. Below is a simplified example:
#Data
#Entity
#Table(name = "###")
public class C1 {
#OneToMany(mappedBy = "###")
#Filter(name = "C2.set1Filter")
private Set<C2> set1;
#OneToMany(mappedBy = "###")
#Filter(name = "C2.set2Filter")
private Set<C2> set2;
#OneToMany(mappedBy = "###")
#Filter(name = "C2.set3Filter")
private Set<C2> set3;
}
#Data
#Entity
#Table(name = "###")
#FilterDefs({
#FilterDef(
name = "set1Filter"
defaultCondition = "c3.value = 'One'"
),
#FilterDef(
name = "set2Filter"
defaultCondition = "c3.value = 'Two'"
),
#FilterDef(
name = "set3Filter"
defaultCondition = "c3.value = 'Three'"
)
})
public class C2 {
#ManyToOne
#JoinColumn(name = "ID")
C3 c3;
}
#Data
#Entity
#Table(name = "###")
public class C3 {
#Id
#Column(name = "ID")
private Long id;
#Column(name = "VALUE")
private String value;
}
When the filters are off, they don't run, and therefore I don't get an error. However, when they are on, I am getting an error saying that c3.value is an invalid identifier.
I am obviously doing something wrong here. Is it possible to get the above to work?
As it stated in the documentation:
The #Filter condition uses a SQL condition and not a JPQL filtering predicate.
So, you should use actual tables and columns names.
#Filter(
name="setFilter",
condition="{c3}.VALUE = :val",
aliases = {
#SqlFragmentAlias( alias = "c3", table= "C3_TABLE_NAME")
}
)
As it stated here:
When using the #Filter annotation and working with entities that are mapped onto multiple database tables, you will need to use the #SqlFragmentAlias annotation if the #Filter defines a condition that uses predicates across multiple tables.

Hibernate filter does not apply for join entity

I am trying to understand hibernate filters, i thought that the filter is applied even if the query is not started from the filtered entity and can be applied if i just join to it.
My entities:
#Table(name = "SPM_SECTION", schema = "TEST")
#GenericGenerator(name = "MODSEC_ID.GEN", strategy = "uuid2")
public class ModuleSection implements Serializable {
private AcademicClass academicClass;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "CLASS_ID")
public AcademicClass getAcademicClass() {
return academicClass;
}
public void setAcademicClass(AcademicClass academicClass) {
this.academicClass = academicClass;
}
}
#Entity
#GenericGenerator(name = "AC_CLASS_ID.GEN", strategy = "uuid2")
#Where(clause="1=1")
#FilterDef(name= Resources.SECURITY_FILTER_NAME, parameters={#ParamDef(name=Resources.SECURITY_FILTER_PARAMETER, type="string")})
#Filter(name=Resources.SECURITY_FILTER_NAME, condition = "DISCRIMINATOR_ID in (:"+Resources.SECURITY_FILTER_PARAMETER+")")
#Table(name = "ACADEMIC_CLASS", schema = "TEST", uniqueConstraints = #UniqueConstraint(columnNames = {"OUS_ID", "YEAR_ID",
"PERIOD_ID", "SHIFT_ID", "SEMESTER", "CODE" }))
public class AcademicClass implements java.io.Serializable {
//I tried by having the association here, i also tried without it.
private Set<ModuleSection> sections = new HashSet<>(0);
#OneToMany(fetch = FetchType.LAZY, mappedBy = "academicClass")
public Set<ModuleSection> getSections() {
return this.sections;
}
public void setSections(Set<ModuleSection> sections) {
this.sections = sections;
}
}
The filter is enabled through an interceptor and the parameter list is fetched from the database for security.
When i execute a query like this:
em.createQuery("select acc from AcademicClass acc ...........", AcademicClass.class)
.getResultList();
the filter is applied. But i also want the filter to be applied when my query starts from ModuleSection:
em.createQuery("select ms from ModuleSection ms join ms.academicClass acc", AcademicClass.class)
.getResultList();
In above query the filter is not applied.
The academicClass in ModuleSection entity is nullable but i also have other entities not null where the above case does not work.
I also tried to apply the #Filter or #FilterJoinTable in module section property with no luck:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "CLASS_ID")
#Filter(name=Resources.SECURITY_FILTER_NAME, condition = "DISCRIMINATOR_ID in (:"+Resources.SECURITY_FILTER_PARAMETER+")")
#FilterJoinTable(name=Resources.SECURITY_FILTER_NAME, condition = "DISCRIMINATOR_ID in (:"+Resources.SECURITY_FILTER_PARAMETER+")")
public AcademicClass getAcademicClass() {
return academicClass;
}
My questions:
Are filters meant to filter only the entity in the from clause? does the filter apply in join entities?
If I want to implement the above should I also add a DISCRIMINATOR_ID in ModuleSection and add the filter to that entity starting the query from there?
There is a silence about it in the hibernate documentation, but it looks like this is true that #Filter is applied only to the from clause.
Assuming that we have the following mapping:
#Entity
#Table(name = "ACADEMIC_CLASS")
#FilterDef(
name="isAccessible",
parameters = #ParamDef(
name="sec",
type="string"
)
)
#Filter(
name="isAccessible",
condition="{acClass}.discriminator_id in (:sec)",
aliases = {
#SqlFragmentAlias(alias = "acClass", table= "TEST_SCHEMA.ACADEMIC_CLASS")
}
)
public class AcademicClass
{
#Id
#Column(name = "class_id")
private Long id;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "academicClass")
private Set<ModuleSection> sections;
// getters/setters
}
#Entity
#Table(name = "SPM_SECTION")
public class ModuleSection
{
#Id
#Column(name = "sec_id")
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "sec_class_id")
private AcademicClass academicClass;
// getters/setters
}
When we run the following query:
session
.enableFilter("isAccessible")
.setParameter("sec", "A1");
List<AcademicClass> classes = session.createQuery(
"select ac from ModuleSection ms join ms.academicClass ac",
AcademicClass.class
).getResultList();
The filter is not applied. It should happen in the JoinSequence.toJoinFragment. The filterCondition is empty in this case.
But for the rewritten in the following way query:
List<AcademicClass> classes = session.createQuery(
"select ac from ModuleSection ms, AcademicClass ac where ms.academicClass = ac",
AcademicClass.class
).getResultList();
We will have:
and as result the following query will be generated:
Hibernate:
/* select
ac
from
ModuleSection ms,
AcademicClass ac
where
ms.academicClass = ac
*/
select
academiccl1_.class_id as class_id1_0_
from TEST_SCHEMA.SPM_SECTION modulesect0_ cross
join TEST_SCHEMA.ACADEMIC_CLASS academiccl1_
where academiccl1_.discriminator_id in (?)
and modulesect0_.sec_class_id=academiccl1_.class_id
So, as a workaround you can rewrite your query in this way.
The #FilterJoinTable annotation can be used only if you have a link table between the parent entity and the child table.

Spring jpa fetch eager not working on findById

I m using spring and hibernate for my java object.
I have an entity like this :
#Entity
#Table
public class Function implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
/** Code value. */
private String code;
}
And another entity that reference the first one like
#Entity
#Table(name = "role", uniqueConstraints = { #UniqueConstraint(columnNames = { "id" }), #UniqueConstraint(columnNames = { "code" }) })
public class RoleDef extends CodeLabelEntity{
...some other primitive type...
/** List of functions */
#OneToMany(fetch = FetchType.EAGER)
#JoinTable(name = "role_function",
joinColumns = #JoinColumn(name = "role_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "function_code", referencedColumnName = "code"))
private Set<Function> functions;
}
I m using repository like this :
public interface RoleDefRepository extends JpaRepository<RoleDef, Long>, QuerydslPredicateExecutor<RoleDef> {
}
when i m doing in my roleDefService :
roleDefRepository.findAll();
I can iterate over RoleDef and functions beacause EAGER do the job
But when i'm trying to do:
RoleDef roleDef = roleDefRepository.findById(id).orElse(null);
My functions Set inside the roledef is empty. findById is not overided, it's the default method like findAll.
If you see something that i ommit to delcare...
I m using last version of spring/hibernate
Thanks
I resolved my problem by doing this in my repository :
#Query("select r from RoleDef r join fetch r.functions where r.id = ?1")
Optional<RoleDef> findByIdWithFunctionTree(Long id);
But if someone can find the real problem because i think EAGER on the relation or accessing by a get when transaction still open will do the same as the fetch...

Hibernate deletes occurring out of order

I have the following classes:
Category
#Entity
#Table(name = "category", schema = "customer")
public class Category extends CustomerOwned {
...
}
CatalogueItem
#Entity
#Table(name = "catalogue_item", schema = "customer")
#EntityListeners(value = { CatalogueItemStatusListener.class })
public class CatalogueItem extends CatalogueOwned implements Statusable<CatalogueItem, CatalogueItemStatus> {
...
#OneToMany(mappedBy = "catalogueItem", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<CatalogueItemCategory> categories;
...
}
CatalogueItemCategory
#Entity
#Table(name = "catalogue_item_category", schema = "customer")
public class CatalogueItemCategory extends CatalogueItemOwned implements CategoryOwner {
...
#ManyToOne
#JoinColumn(name = "category_id", nullable = false, insertable = true, updatable = false)
private Category category;
...
}
We perform very few hard deletes in our application, but Categorys are hard deleted. CatalogueItemCategorys are also hard deleted.
I'm creating a service to allow someone to delete a Category and at the same time I check all Category dependencies and delete those too, so I'm doing this:
#Transactional
public DependentItemsResultResource delete(String categoryId, boolean force) {
List<CatalogueItem> catalogueItems = catalogueItemRepository.getByCategory(getCustomerStringId(), categoryId);
DependentItemsResultResource resultResource = new DependentItemsResultResource();
resultResource.setDependentCatalogueItems(new SearchResponse().from(catalogueItems, CatalogueItemSummaryResource::new));
if (!catalogueItems.isEmpty()) {
if (!force) {
resultResource.setDeleted(false);
return resultResource;
}
catalogueItems.forEach(item -> {
item.getCategories().removeIf(catalogueItemCategory -> categoryId.equals(catalogueItemCategory.getCategory().getStringId()));
sendEvent(prepareEvent(new CatalogueItemUpdatedEvent(), item.getCatalogue().getStringId(), item.getStringId()));
});
}
Category category = getCategory(categoryId);
resultResource.setDeleted(true);
categoryRepository.delete(category);
CategoryDeletedEvent event = new CategoryDeletedEvent();
event.setCategoryPath(category.getPath());
sendEvent(prepareEvent(event, categoryId));
return resultResource;
}
This is causing the following exception:
Caused by: org.h2.jdbc.JdbcSQLException: Referential integrity constraint violation: "FKLYKUEHJG9UL0LR36O5XDD3MLF: CUSTOMER.CATALOGUE_ITEM_CATEGORY FOREIGN KEY(CATEGORY_ID) REFERENCES CUSTOMER.CATEGORY(ID) (1)"; SQL statement:
delete from customer.category where id=? [23503-192]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:345)
at org.h2.message.DbException.get(DbException.java:179)
at org.h2.message.DbException.get(DbException.java:155)
at org.h2.constraint.ConstraintReferential.checkRow(ConstraintReferential.java:426)
at org.h2.constraint.ConstraintReferential.checkRowRefTable(ConstraintReferential.java:443)
at org.h2.constraint.ConstraintReferential.checkRow(ConstraintReferential.java:318)
at org.h2.table.Table.fireConstraints(Table.java:967)
at org.h2.table.Table.fireAfterRow(Table.java:985)
at org.h2.command.dml.Delete.update(Delete.java:101)
at org.h2.command.CommandContainer.update(CommandContainer.java:98)
at org.h2.command.Command.executeUpdate(Command.java:258)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:160)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:146)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204)
... 109 common frames omitted
I've tried replacing this bit:
item.getCategories().removeIf(catalogueItemCategory -> categoryId.equals(catalogueItemCategory.getCategory().getStringId()));
with this:
item.getCategories().stream()
.filter(catalogueItemCategory -> categoryId.equals(catalogueItemCategory.getCategory().getStringId()))
.forEach(catalogueItemCategoryRepository::delete);
but it's not making any difference. Hibernate is deleting the objects out of order. It should be deleting the CatalogueItemCategory objects before the Category objects, but it's not.
How do I get it to delete in the correct order?
Edit: missed this class which provides the parent of the CatalogueItemCategory:
#MappedSuperclass
public class CatalogueItemOwned extends Updateable {
...
#ManyToOne
#JoinColumn(name = "catalogue_item_id", nullable = false, insertable = true)
private CatalogueItem catalogueItem;
...
}
There are dirty solutions, that are both anti-pattern, but would probably work:
Either use a flush after each delete, or delete with a native query.
That said, I recommend not to use any of these solutions, but to search for a cleaner solution.
I've solved this by explicitly deleting the category and all dependencies as follows:
#Transactional
public DependentItemsResultResource delete(String categoryId, boolean force) {
List<CatalogueItem> catalogueItems = catalogueItemRepository.getByCategory(getCustomerStringId(), categoryId);
DependentItemsResultResource resultResource = new DependentItemsResultResource();
resultResource.setDependentCatalogueItems(new SearchResponse().from(catalogueItems, CatalogueItemSummaryResource::new));
if (!catalogueItems.isEmpty()) {
if (!force) {
resultResource.setDeleted(false);
return resultResource;
}
catalogueItems.forEach(item -> sendEvent(prepareEvent(
new CatalogueItemUpdatedEvent(), item.getCatalogue().getStringId(), item.getStringId())));
}
Category category = getCategory(categoryId);
resultResource.setDeleted(true);
categoryRepository.deleteCatalogueItemCategoriesByCategory(category);
categoryRepository.deleteCategory(category);
CategoryDeletedEvent event = new CategoryDeletedEvent();
event.setCategoryPath(category.getPath());
sendEvent(prepareEvent(event, categoryId));
return resultResource;
}
The repo methods are:
#Modifying
#Query("delete from Category c where c = :category")
void deleteCategory(#Param("category") Category category);
and
#Modifying
#Query("delete from CatalogueItemCategory where category = :category")
void deleteCatalogueItemCategoriesByCategory(#Param("category") Category category);
Not the best, but also not the worst. Not using native deletes and it's still within the main transaction.

Spring Data Jpa and Specification - how to work with ManyToOne and ManyToMany relations?

I have a simple model in my project.
[UpdatePackage] >- (ManyToOne) - [Version] -< [UseCase] - (ManyToMany)
public class UpdatePackage implements Comparable<UpdatePackage> {
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = COLUMN_ORIG_VERSION, nullable = true)
private Version origVersion;
// setters and getters
}
#Entity
#Table(name = Version.TABLE_NAME)
public class Version {
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = JVUC_TABLE, joinColumns = { #JoinColumn(name = JVUC_COLUMN_VERSION, referencedColumnName = COLUMN_ID) }, inverseJoinColumns = { #JoinColumn(name = JVUC_COLUMN_USECASE, referencedColumnName = UseCase.COLUMN_ID) })
private final Set<UseCase> useCases = new HashSet<UseCase>();
// setters and getters
}
#Entity
#Table(name = UseCase.TABLE_NAME)
public class UseCase {
#Column(name = COLUMN_NAME, nullable = false)
private String name;
// setters and getters
}
For implementation of filter I would like to use Spring Data Jpa and Specification from spring.data.jpa.domain
For instance I would like to find list of UpdatePackage with given usecase names.
I understand that for ManyToOne relation I need use Join and for ManyToMany I need to use Fetch.
My implementation of Specification interface looks like this:
public static Specification<UpdatePackage> useCaseNames(final List<String> useCaseNames) {
return new Specification<UpdatePackage>() {
#Override
public Predicate toPredicate(Root<UpdatePackage> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
final Join<UpdatePackage, Version> version = root.join(UpdatePackage_.destVersion,
JoinType.LEFT);
Fetch<Version, UseCase> useCase = version.fetch(Version_.useCases, JoinType.LEFT);
return null;
// return useCase.get(UseCase_.name).in(useCaseNames);
}
};
}
When I run a integration test I got NPException in line:
Fetch<Version, UseCase> useCase = version.fetch(Version_.useCases, JoinType.LEFT);
because fields joins and fetches of object version are null.
I don't know what I do in wrong way and I cannot find any answer in Internet.
Does anyone know what is wrong in this code?
Stack:
java.lang.NullPointerException
at org.hibernate.ejb.criteria.path.AbstractFromImpl.constructJoin(AbstractFromImpl.java:261)
at org.hibernate.ejb.criteria.path.AbstractFromImpl.fetch(AbstractFromImpl.java:549)
I've found a solution. I had a bug in static model.
static model
From:
public static volatile SingularAttribute<Version, UseCase> useCases;
To:
public static volatile SetAttribute<Version, UseCase> useCases;
and in implementation of specification:
public static Specification<UpdatePackage> useCaseNames(final List<String> useCaseNames) {
return new Specification<UpdatePackage>() {
#Override
public Predicate toPredicate(Root<UpdatePackage> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
final Join<UpdatePackage, Version> version = root.join(UpdatePackage_.destVersion,
JoinType.LEFT);
final Join<Version, UseCase> useCase = version.join(Version_.useCases);
return useCase.get(UseCase_.name).in(useCaseNames);
}
};
}

Categories

Resources