Hibernate - Retrieve parent + childrens that matches specific criterias - java

I'm working on a project where Hibernate criterias are still in use.
Here is a quick overview of the entities that we're using :
public class Location {
[...]
#OneToMany(mappedBy = "location")
private Set<EventLocation> eventLocations = new HashSet<>();
public class EventLocation {
[...]
#ManyToOne(optional = false, cascade = CascadeType.PERSIST)
#JoinColumn(name = "event_id")
private Event event;
#ManyToOne(optional = false, cascade = CascadeType.PERSIST)
#JoinColumn(name = "location_id")
private Location location;
public class Event {
[...]
#OneToMany(mappedBy = "event", cascade = CascadeType.ALL)
private Set<EventLocation> locations = new HashSet<>();
#OneToMany(mappedBy = "event", cascade = CascadeType.ALL)
private Set<EventTag> tags = new HashSet<>();
public class EventTag {
[...]
#ManyToOne(optional = false, cascade = CascadeType.PERSIST)
#JoinColumn(name = "event_id")
private Event event;
#ManyToOne(optional = false, cascade = CascadeType.PERSIST)
#JoinColumn(name = "tag_id")
private Tag tag;
public class Tag {
[...]
#OneToMany(mappedBy = "tag")
private Set<EventTag> eventTags = new HashSet<>();
I wonder if we have a way, using Hibernate Criteria API, to retrieve a list of Location which holds a filtered list of Event objects ?
For example, we'd like to have a query that would return a location but the eventLocations would only contains results if eventLocations.event.type = "SAMPLE_TYPE" AND eventLocations.event.tags.id IN (1,2,3).
We tried using this, but the eventLocation set contains objects that should have been filtered out.
var crit = getSession().createCriteria(Location.class, "loc");
crit.createAlias("loc.eventLocations", "eloc");
crit.createAlias("eloc.event", "event");
crit.createAlias("event.tags", "etags");
crit.createAlias("etags.tag", "tag");
crit.add(Restrictions.in("etags.id", Arrays.asList(1, 2, 3)));
crit.add(Restrictions.eq("event.type", "SAMPLE_TYPE"));
var locations = crit.list();
Generated query looks like that when I enable Hibernate statistics :
select
[...]
from
location LOC
inner join
event_location ELOC on LOC.id=ELOC.id_location
inner join
event EVENT ON ELOC.id_event=EVENT.id
inner join
event_tag ETAG ON EVENT.id=ETAG.event_id
inner join
tag TAG ON ETAG.id_tag=TAG.id
where
EVENT.type="SAMPLE_TYPE
AND TAG.id in (
1, 2, 3
)
This request runs fine if I run it using PgAdmin, but when using Hibernate directly, it seems like when I loop through locations.eventLocations, it contains eventlocation for events that should have been filtered out because they don't match the "SAMPLE_TYPE" type.
EDIT
It looks like this post asks for the same sub-filtering feature, but to this date no answer was provided. I faced the same behavior mentioned in Steven Francolla's comment (using direct child query returns only matching children's, but when I try to do child-child filtering I doesn't seems to work).
I'm still linking to this post because it explains the problem we're facing with a lot of added details.

Related

Spring Data JPA Specification search for property in nested collection

Say I have at least two entities.
#Entity
public class Process {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique = true)
private String name;
#ManyToAny(
metaColumn = #Column(name = "node_type"),
fetch = FetchType.LAZY
)
#AnyMetaDef(
idType = "long", metaType = "string",
metaValues = {
#MetaValue(targetEntity = Milestone.class, value = MILESTONE_DISC),
#MetaValue(targetEntity = Phase.class, value = PHASE_DISC)
}
)
#Cascade({org.hibernate.annotations.CascadeType.ALL})
#JoinTable(
name = "process_nodes",
joinColumns = #JoinColumn(name = "process_id", nullable = false),
inverseJoinColumns = #JoinColumn(name = "node_id", nullable = false)
)
private Collection<ProcessNode> nodes = new ArrayList<>();
...
}
#Entity
#ToString
#DiscriminatorValue(MILESTONE_DISC)
public class Milestone implements ProcessNode {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Collection<ResultDefinition> results;
#ToString.Exclude
#ManyToOne(fetch = FetchType.LAZY)
#Transient
private Process process;
...
}
Now I want to use spring data jpa specification to find (all) processes which have a milestone with name "S5".
Note that Milestone is a ProcessNode and there is another Entity called Phase which is also a ProcessNode. These can be contained in the "nodes" collection of my Process Entity.
I tried to write something like this:
public static Specification<Process> hasMilestoneWithName(final String milestoneName) {
return (Specification<Process>) (root, query, criteriaBuilder) -> {
Path<?> namePath = root.join("nodes").get("name");
return criteriaBuilder.equal(namePath, milestoneName);
};
}
This does not work, but throws:
Caused by: java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name [nodes] on this ManagedType [com.smatrics.dffs.processservice.model.entities.Process]
I don't really know how to use the API. Examples often refer to a meta-model that would be generated by the IDE or maven, but I really do not want to have any static generated resources. Please help me resolve this with Specification of spring-data-jpa without a generated meta-model.
Also if you could help me write the hql it would be awesome.
Thanks!
I would suggest a simpler alternative, coming from bottom-up:
Load Milestone entities with name=S5: findByName("S5")
Return the Process for each Milestone
Filter out the duplicates
Or you could even save a few SQL queries by returning not the Milestone entity but only the ID of the Process for each Milestone and then load the Process nodes by a list of IDs:
The (native) SQL equivalent would be
select *
from process
where id in (
select process_id
from milestone
where name = 'S5'
)
Regardless of my solution your join does not look completely correct to me but I can't point out what's wrong - maybe there are other methods on the JPA metamodel that return a CollectionJoin? Not sure. Probably it is because #ManyToAny is not JPA standard so the JPA criteria API does not recognize nodes as a valid "joinable" field.

Create referenced entity if null with JPA/Hibernate

I have this class:
public class Tenant {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#NaturalId
#Column(name = "name", nullable = false, updatable = false, unique = true)
private String name;
#OneToMany(mappedBy = "tenant", cascade = CascadeType.ALL, orphanRemoval = true)
private List<User> users;
#OneToMany(mappedBy = "tenant", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Role> roles;
#OneToOne(mappedBy = "tenant", cascade = CascadeType.ALL, orphanRemoval = true, optional = false)
private TenantLimits limits;
}
Where of course all referenced classes are entities. I'm able to create, update and retrieve everything from here, but since private TenantLimits limits; refers to an entity created after Tenant was created many of my Tenants elements don't contains any matched TenantLimits.
So my question is: How can I insert in the database a value in TenantLimits if is null when I'm going to retrieve Tenant? In Java I can easily check (of course) if the property is null and insert manually foreach retrieve, but since the retrieve of this entity is present in different places in my code I'd to have something that manage this automatically if exists
You are telling Hibernate that Tenant.limits cannot be null by mapping it with "optional=false". It will 100% adhere to this definition. It will only create valid tenants and I assume it will throw you exceptions if the state of the database is invalid. It won't let you fix your data.
You should fix the state of your database by any other means than with this particular Hibernate mapping.
You might have to migrate in 2 steps. Let's say, make the mapping "optional=true". Then you can run a Java process to fix your data (maybe by using an entity listener). Then - change it back to "optional=false".

cascade not working with hibernate #Any

I am using hibernate's #Any(one to any) annotation in my java code. Below here'a a code snippet:
#Entity
public class A {
//.. few properties
#Any(metaColumn = #Column(name = "type"), fetch = FetchType.EAGER)
#AnyMetaDef(idType = "long", metaType = "string", metaValues = {#MetaValue(targetEntity = Http.class, value = "http")})
#JoinColumn(name = "protocol_id")
#Cascade({org.hibernate.annotations.CascadeType.ALL})
#Fetch(FetchMode.SELECT)
private Protocol protocol;
//more code
}
Now when I update the field using session.update(a) where a is an instance of A, a new record for field protocol is created and the earlier one is not deleted. Desired result was that old record for field protocol gets deleted when I update using session.update(a) and new record gets created . Since I am using cascadeType ALL, why is this not working ?
Try to annotate with #OneToOne or #ManyToOne as needed.

Hibernate reloading entities in a fetch join

I am having a problem with Hibernate reloading the entities in a query even though they are being fetched as part of the main query.
The entities are as follows (simplified)
class Data {
#Id
String guid;
#ManyToOne(fetch = FetchType.LAZY)
#NotFound(action = NotFoundAction.IGNORE)
DataContents contents;
}
class DataClosure {
#Id
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "ancestor_id", nullable = false)
private Data ancestor;
#Id
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "descendant_id", nullable = false)
private Data descendant;
private int length;
}
This is modelling a closure table of parent / child relationships.
I have set up some criteria as follows
final Criteria criteria = getSession()
.createCriteria(DataClosure.class, "dc");
criteria.createAlias("dc", "a");
criteria.createAlias("dc.descendant", "d");
criteria.setFetchMode("a", FetchMode.JOIN);
criteria.setFetchMode("d", FetchMode.JOIN);
criteria.add(Restrictions.eq("d.metadataGuid",guidParameter));
criteria.add(Restrictions.ne("a.metadataGuid",guidParameter));
This results in the following SQL query
select
this_.descendant_id as descenda2_21_2_,
this_.ancestor_id as ancestor3_21_2_,
this_.length as length1_21_2_,
d2_.guid as metadata1_20_0_,
d2_.name as name5_20_0_,
a1_.guid as metadata1_20_1_,
a1_.name as name6_20_1_
from
data_closure this_
inner join
data d2_
on this_.descendant_id=d2_.metadata_guid
inner join
data a1_
on this_.ancestor_id=a1_.metadata_guid
where
d2_.guid=?
and a1_.guid<>?
which looks like it is correctly implementing the join fetch. However when I execute
List list = criteria.list();
I see one of these entries in the SQL log per row in the result set
Result set row: 0
DEBUG Loader - Loading entity: [Data#testGuid19]
DEBUG SQL -
select
data0_.guid as guid1_20_0_,
data0_.title as title5_20_0_,
from
data data0_
where
data0_.guid=?
Hibernate:
(omitted)
DEBUG Loader - Result set row: 0
DEBUG Loader - Result row: EntityKey[Data#testGuid19]
DEBUG TwoPhaseLoad - Resolving associations for [Data#testGuid19]
DEBUG Loader - Loading entity: [DataContents#7F1134F890A446BBB47F3EB64C1CF668]
DEBUG SQL -
select
dataContents0_.guid as guid_12_0_,
dataContents0_.isoCreationDate as isoCreat2_12_0_,
from
dataContents dataContents0_
where
dataContents0_.guid=?
Hibernate:
(omitted)
It is looks like even though the DataContents is marked as lazily loaded, it's being loaded eagerly.
So I either want some way in my query to fetch join DataClosure and Data and lazily fetch DataContents, or to fetch join the DataContents if that is not possible.
Edit:
Modelling the closure table like this
class DataClosure {
#Id
#Column(name = "ancestor_id", nullable = false, length =36 )
private String ancestorId;
#Id
#Column(name = "descendant_id", nullable = false, length =36 )
private String descendantId;
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "ancestor_id", nullable = false)
private Data ancestor;
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "descendant_id", nullable = false)
private Data descendant;
private int length;
}
fixed the problem. In other words, having #Id annotation on entities from other tables seemed to cause the issue, even though there was nothing wrong with the queries generated.
I think your problem here might be this
#NotFound(action = NotFoundAction.IGNORE)
There are plenty of google results where using that causes the lazy loading to become eager. I think it is a bug in Hibernate.
Adding this to the list of annotations should fix the problem
#LazyToOne(value=LazyToOneOption.NO_PROXY)
Since that informs Hibernate that you will not try to use that property later on so no proxy is required.

JPA manyToMany relationship middle table is not updated

I have CATEGORY, AD and CATEGORY_AD table, typical many to many relationship. Somehow nothing is inserted into CATEGORY_AD table. What am I missing?
In Category.java:
#ManyToMany
#JoinTable(name = "CATEGORY_AD", joinColumns = {
#JoinColumn(name = "CATEGORY_ID", referencedColumnName = "ID") }, inverseJoinColumns = {
#JoinColumn(name = "AD_ID", referencedColumnName = "ID") })
private List<Ad> ads;
In Ad.java:
#ManyToMany(mappedBy = "ads")
private List<Category> categories;
In my Service class:
Category laCat = new Category();
laCat.setId(categoryId);
laCat.getAds().add(ad);
ad.getCategories().add(laCat);
ad = adRepository.saveAndFlush(ad);
You are saving the 'owned' side instead of the 'owning' side.
Every ManyToMany relationship must have an owner, which in your case is Category. You can see that because in Category you have the definition of the #JoinTable in the ads List, and in Ad you refer to that list by #ManyToMany(mappedBy = "ads").
Whenever you save the owning side of the relationship, this will trigger a save on the join table too, but not the other way around. So saving the owned side (Ad) will do nothing on the CATEGORY_ADtable.
You should do something like this:
Category laCat = new Category();
laCat.setId(categoryId);
laCat.getAds().add(ad);
// Next line will insert on CATEGORY and CATEGORY_AD tables
laCat = categoryRepository.saveAndFlush(category);
// We add the category to the ad object to keep both sides in sync
ad.getCategories().add(laCat);
You can see that even if a save on Category triggers a save on the join table, it's still our responsibility manually add the category to the categories Listin the ad object so both sides are in sync, having the same elements. And there's no need to save the ad object already.
Try to save Category object also (before you invoke ad.getCategories().add(laCat);)
Please use hibernate #Cascade annotations on List<Category> categories;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;
#ManyToMany(mappedBy = "ads")
#Cascade(value = CascadeType.SAVE_UPDATE)
private List<Category> categories;
If you prefer using JPA annotations then you can try this:
#ManyToMany(mappedBy = "ads", cascade = CascadeType.PERSIST)
#Cascade(value = CascadeType.SAVE_UPDATE)
private List<Category> categories;
Please check this thread: what is cascading in hibernate

Categories

Resources