Lazy loading collection of objects for insert with HQL - java

I need to load a collection of objects from DB via HQL with the purpose of using those objects only for setting an association in some new records.
Consider the following: I need to send an email to all students with nr of credits > 50 and I need to keep a trace of all the recipients included in the transmission.
First select the students:
Query query = sessionFactory.getCurrentSession().createQuery("from Student student left join fetch student.scores where student.credits>50");
List<Student> students = query.list();
this will return a list of students with all columns (non association attributes like first name, last name, date of birth...) loaded for each record.
The problem is that when I have a lot of data, the above query is very slow and occupies a lot of memory due to a large amount of useless data being transferred from db to app server.
I can't make all attributes lazy loaded directly in the entity as this will kill other areas of the application.
What I need is a way of loading the above collection with only the ids fetched as I am using the objects just to set them for some new objects in an association. I know this can be done for OneToMany associations easily but how can I do it for a direct queried collection?
List<Recipient> emailRecipients = new ArrayList<>();
for(Student student: students){
Recipient rec = new Recipient();
//this is the only usage of the student object
rec.setStudent(student);
...
set other properties for the recipient
sessionFactory.getCurrentSession().save(rec);
}
Inside the recipient, the Student object is setup like this:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "STD_ID", nullable = false)
private Student student;
As you can see, only the id of the Student object is needed, but I don't know how to do it.

You can use one Hibernate specific trick, which allows you to set the FK association even if you don't provide an actual managed entity. Let's say we have a studentId, we can simply set:
Student student = new Student();
student.setId(studentId);
rec.setStudent(student);
This is not supported by standard JPA, but it works with Hibernate. Just make sure you don't have any cascade propagation from child to parent (which you should not have anyway).

In the end I've loaded the studentId only and modified the mapping inside the recipient to include the studentId as well:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "STD_ID", insertable=false, updatable = false)
private Student student;
#Column(name = "STD_ID", nullable = false)
private Long studentId;
now I can simply set the studentId and the performance is much better.

Related

Hibernate ManyToMany: doesn't insert the same ID in both tables

I have a (classic) many to many relationship using Hibernate and a Oracle database.
I defined my entities as follows.
Student.java
#Entity
#Table
public class Student implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name = "STUDENT_ID")
private Long studentId;
#ManyToMany
#JoinTable(name = "STUDENT_PROJECT", joinColumns = {
#JoinColumn(name = "STUDENT_ID") }, inverseJoinColumns = {
#JoinColumn(name = "PROJECT_ID") })
private Set<Project> projects = new HashSet<>();
Project.java
#Entity
#Table
public class Project implements Serializable {
#Id
#Column(name="PROJECT_ID")
private int projectId;
#ManyToMany(mappedBy="projects")
private Set<Student> students = new HashSet<>();
I have a STUDENT_PROJECT table in my oracle database that consists of two fields, PROJECT_ID and STUDENT_ID with a composite primary key on them.
I have a sequence on my STUDENT table to auto generate their ID.
TRIGGER STUDENT_TRIGGER
BEFORE INSERT ON STUDENT
FOR EACH ROW
BEGIN
SELECT STUDENT_SEQUENCE.NEXTVAL
INTO :STUDENT_ID
FROM dual;
END;
Now my problem is that when I try to persist my Student entity, the ID field of the STUDENT table doesn't correspond to the STUDENT_ID field of STUDENT_PROJECT table.
Somehow it persists an ID for Student and a different one in the mapping table and I can't figure out why.
This is how I manipulate my objects
Student student = new Student();
// set some fields
Set<Project> projects = new HashSet<>();
// call to a private method to set its projects
student.setProjects(projects);
studentDao.persist(student);
I had to remove the foreign key in the STUDENT_PROJECT table on the ID of STUDENT (else the constraint wouldn't let me save of course) to finally notice that is was setting differents ID's but I don't understand why.
If you need more information let me know, I tried to keep it as small as possible, thanks.
UPDATE:
I have tried to remove the trigger on the STUDENT table and changed its Java configuration to
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "G1")
#SequenceGenerator(name = "G1", sequenceName= "STUDENT_SEQUENCE")
#Column(name = "STUDENT_ID")
private Long studentId;
I now get:
javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [fr.persistance.data.Student#3037]
It doesn't save to database at all and produces the above exception on the persist(), I guess I have a problem with my sequence as it seems it tries to persist two Student objects with the same ID (I am looping to save multiples students and their projects).
I have used the eclipse debug to inspect a few of my Student objects after the persist() call and they each have a different studentId in Java but once the loop and the transaction end, the exception occurs but it does seem like they each get a separate ID.
The problem is that the Ids of the students are generated by the database in a trigger, while Hibernate tries to create the value itself.
For an oracle database, GenerationType.AUTO will use a sequence. The sequence is accessed by Hibernate, which assigns the value (before the student is actually inserted in the database).
To make Hibernate use your sequence, add
#SequenceGenerator(name = "sequence-generator", sequenceName = "STUDENT_SEQUENCE")
and remove the trigger.
When you want to use a trigger, you need to map it as GenerationType.IDENTITY. (Not recommended though.)

Hibernate many to many fetching associated objects

#Entity
#Table(name = "MATCHES")
public class Match implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "MATCH_ID")
private Long id;
#ManyToMany(mappedBy = "matches", cascade = CascadeType.ALL)
private Set<Team> teams = new HashSet<Team>();
}
#Entity
#Table(name = "Teams")
public class Team implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "TEAM_ID")
private long id;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "TEAM_MATCH", joinColumns = { #JoinColumn(name = "TEAM_ID") }, inverseJoinColumns = {
#JoinColumn(name = "MATCH_ID") })
private Set<Match> matches = new HashSet<Match>();
}
I got those classes, now I want to get all the matches and let's say, print names of both teams.
public List getAllMatches() {
Session session = HibernateUtil.getSession();
Transaction t = session.beginTransaction();
Criteria criteria = session.createCriteria(Match.class, "match");
criteria.createAlias("match.teams", "mt", JoinType.LEFT_OUTER_JOIN);
List result = criteria.list();
t.commit();
session.close();
return result;
}
But when I invoke that method, result has size 2 when I got only 1 match in my table. Both of those matches in result have 2 teams, which is correct. I have no idea why this happends. What I want is to have one Match object with two Team objects in 'teams' set, but I have two of those Match objects. They are fine, but there are two of them. I'm completely new to this and have no idea how to fix those criterias. I tried deleting 'FetchType.LAZY' from #ManyToMany in Team but it doesn't work. Team also has properties like Players/Trainer etc. which are in their own tables, but I don't want to dig that deep yet, baby steps. I wonder tho if doing such queries is a good idea, should I just return Matches and then if I want to get Teams, get them in another session?
Edit: I added criteria.setResultTransformer(DistinctRootEntityResultTransformer.INSTANCE); and it works, is that how I was suppose to fix that or this is for something completely different and I just got lucky?
I think the duplication is a result of your createAlias call, which besides having this side effect is redundant in the first place.
By calling createAlias with those arguments, you are telling Hibernate to not just return all matches, but to first cross index the MATCHES table with the TEAM_MATCH table and return a result for each matching pair of rows. You get one result for a row in the matches table paired with the many-to-many mapping to the first team, and another result for the same row in the matches table paired with the many-to-many mapping to the second team.
I'm guessing your intent with that line was to tell Hibernate to fetch the association. This is not necessary, Hibernate will fetch associated objects on its own automatically when needed.
Simply delete the criteria.createAlias call, and you should get the result you expected - with one caveat. Because the association is using lazy fetching, Hibernate won't load it until you access it, and if that comes after the session is closed you will get a LazyInitializationException. In general I would suggest you prefer solving this by having the session opened and closed at a higher level of abstraction - getting all matches is presumably part of some larger task, and in most cases you should really use one session for the duration of the entire task unless there are substantial delays (such as waiting for user input) involved. Changing that would likely require significant redesign of your code, however; the quick solution is to simply loop over the result list and call Hibernate.initialize() on the teams collection in each Match. Or you could just change the fetch type to eager, if the performance cost of always loading the association whether or not you need it is acceptable.

Hibernate Extra Lazy oneToMany association, using contains() method comparing by other field than id?

As you know, you can declare a OneToMany association with ExtraLazy semantics, in Hibernate, and that means that size(), contains() and others might become smarter. It will run specific SQL statement and will not initialize the entire collection. Great feature for large collection.
The thing is that looks like the contains() method, only compares by Id (primary key). Suppose this example:
class Department {
........
#OneToMany(mappedBy = "department", cascade = CascadeType.ALL, fetch= FetchType.LAZY)
#LazyCollection(LazyCollectionOption.EXTRA)
private Collection<Employee> employees = new ArrayList<Employee>();
.........
public void addEmployee(Employee e) {
if (!this.employees.contains(e)) {
employees.add(e);
e.setDepartment(this);
}
}
}
The code above is a Department class with a OneToMany association to Employee (another Entity), using ExtraLazy. And on the addEmployee() method, I use the method contains() of the PersistentBag in this case.
When I see the SQL statements generated by Hibernate, I see that it issued a SQL comparing by ID of the Employee.
What if I want to check if the Employee exists by name, due to I don't want to have Employee with the same name in the Deparment? or other field of the Employee? Is there a way to do that?

Implementing hierarchical data structures with JPA (fixed depth)

I have a hierarchical data structure with a fixed depth of 4. For a better understanding, let's assume the following (just an example):
The "root" level is called countries
Each country contains an arbitrary amount of states
Each state countains an arbitrary amount of counties
Each county contains an arbitrary amount of cities
So there are always 1-N relationships between the levels.
A very important usecase (given the id of a country) is to load the whole "content" of a country at once with the smallest possible impact on the performance of the database.
In a first naive approach, I created 4 entitiy classes in Java where the entity "Country" contains a list of the type "State", the entity "State" contains a list of the type "County" and so on...
But what JPA creates afterwards are of course not 4 tables, but 7 (4 for the entities + 3 for the connection between the levels due to 1-N). I don't know if this is a good solution since there is a lot of joining going on under the hood.
I also tried to map the subtypes to their parent types (a city belongs to one county, a county belongs to one state, a state belongs to one country). This results in 4 tables, but makes it more difficult to retrieve all data at once from the application's point of view. If I'm not wrong, I would need 4 different requests instead of one.
How could I solve this problem? Is there a way to combine a simple table layout (with four tables, not seven) with easy to use entity classes (a parent type should know its children)?
If not, how would you realize this?
I'm using JPA with Hibernate and PostgreSQL.
You can avoid the 3 extra mapping tables by using the #JoinColumn annotation rather than the #JoinTable annotation that I suspect you are using.
So for example:
COUNTRY
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy="country")
private List<State> stateList;
STATE
#ManyToOne
#JoinColumn(name="country_id")
private Country country
The database tables would be as follows:
Country
country_id => primary key
State
state_id => primary key
country_id => foreign key
This way the mapping tables between all the 4 entities can be avoided.
You can achieve this pretty easily using JPQL:
SELECT DISTINCT country
FROM Country country
JOIN FETCH country.states states
JOIN FETCH states.counties counties
JOIN FETCH counties.cities cities
WHERE country.id = :countryId
Using fetchType = FetchType.EAGER on #OneToMany/#ManyToOne(believe that one is already EAGER by default) will achieve similar results.
It's very simple use bidirectional mapping. Go through that link
How to delete Child or Parent objects from Relationship?
Make some changes like below
Country Entity:
------
#OneToMany(mappedBy="Country ",cascade = CascadeType.ALL)
private List<States > states;
#OneToMany(mappedBy="Country ",cascade = CascadeType.ALL)
private List<Counties> counties;
#OneToMany(mappedBy="Country ",cascade = CascadeType.ALL)
private List<Cities> cities;
-------
setters & getters
States Entity:
-----
#ManyToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn(name="countryId")
private Country country ;
-----
Counties Entity:
--------
#ManyToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn(name="countryId")
private Country country ;
-------
Cities Entity:
#ManyToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn(name="countryId")
private Country country ;
---------
After compilation of all entity's do your insertion . Only 4 will create and read your data by using Country object id.
You already have the solution: four table is the way to go, with bidirectional relationships (use the mappedBy property in the not-owning side of every relationship). If the relationships are EAGER-fetched, than all entities are automatically loaded. If you want to use LAZY fetching, you could try a named query in order to load the entity with all relationships loaded:
SELECT DISTINCT c FROM Country c LEFT JOIN FETCH c.states s LEFT JOIN FETCH s.counties co...
Did you try to declare the fetch type of the relations explicitely to eager with your second approach (default is lazy, that's why you have to do four queries).
E.g.
#OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn ...
private ...;
see here: http://www.concretepage.com/hibernate/fetch_hibernate_annotation
Here is how your entities will look like:(You can use EAGER Loading instead of LAZY as well if you want)
Entity: Country
#Id
private Integer id;
#OneToMany(orphanRemoval=true fetch=FetchType.LAZY)
#JoinColumn(name="COUNTRY_ID")
private List<State> stateList;
Entity: State
This table has COUNTRY_ID that is Foreign Key to Country
#Id
private Integer id;
#OneToMany(orphanRemoval=true fetch=FetchType.LAZY)
#JoinColumn(name="STATE_ID")
private List<County> countyList;
#Column(name="COUNTRY_ID")
private Integer countryId;
Entity: County
This table has STATE_ID that is Foreign Key to State
#Id
private Integer id;
#OneToMany(orphanRemoval=true fetch=FetchType.LAZY)
#JoinColumn(name="COUNTY_ID")
private List<City> cityList;
#Column(name="STATE_ID")
private Integer stateId;
Entity: City
This table has COUNTY_ID that is Foreign Key to County
#Id
private Integer id;
#Column(name="COUNTY_ID")
private Integer countyId;
Your JPQL will be:
Select o from Country o where o.id=10
This will pick The Country Entity along with all the mappings like below.
Country
Holding List of States
Each States Holding List of Counties
Each Counties Holding LIst of Cities
For a requirement like yours, I would suggest to have a tree-like structure to maintain the hierarchical location data. It is relatively easy to implement & maintain and is more scalable & extensible.
In order to implement tree you need to have 2 tables LOCATION_NODE (Location ID, Location Name, Location Type[country, state, county, city]) & LOCATION_REL (Relation ID, Parent ID, Child ID). Below is the basic implementation of the tree idea.
public class LocationRel<T> {
private LocationNode<T> root;
public LocationRel(T rootData) {
root = new LocationNode<T>();
root.data = rootData;
root.children = new ArrayList<LocationNode<T>>();
}
public static class LocationNode<T> {
private T data;
private LocationNode<T> parent;
private List<LocationNode<T>> children;
}
}
This is the basic building block for a tree. You may need to add methods for add to, removing from, traversing, and constructors. But, once implemented, you have the freedom to add any new location type, change the hierarchy, add node, delete node etc with your hierarchical data.
Think out of the box.
Shishir
If you need the performance, I would suggest to de-normalize your tables and create 4 entities with following attributes (columns):
Country: id, name
State: id, countryId, name
County: id, countryId, stateId, name
City: id, countryId, stateId, countyId, name
(mapping is obvious)
Then you will be able to build a simple SQL queries.
If you need performance, prefer named queries as they are compiled at initialization time.
E.g. select all cities by country: "SELECT id, name FROM city WHERE country_id=?"
You may even not declare a references between entities using #ManyToOne, but just declare a simple #Columns. API call will, most likely, accept IDs (countryId, stateId), so you'll be better to pass that IDs as parameters to DAO. Most likely, you have a locations tables filled in once by sql script and the data should not be modified. Create foreign keys to guarantee data integrity.
And do you really need a tree-like structure in memory? If so, create it by hand, it is not very complex.
Searching Online, I found a couple of Links on JPQL which I think might help.
Link 1
Link 2
Anyways,
JPQL is one of the best ways to achieve this, try out this Query
SELECT DISTINCT country FROM Country country JOIN FETCH country.states states JOIN FETCH states.counties counties JOIN FETCH counties.cities cities WHERE country.id = :countryId
A solution that is useful, if you have relations that point to their parent only is the following:
With records:
#Entity
public class Country
{
#Id
private Long id;
}
#Entity
public class State
{
#Id
private Long id;
#ManyToOne(optional = false)
#JoinColumn(name = "country_id", referencedColumnName = "id", nullable = false)
Country country;
}
#Entity
public class County
{
#Id
private Long id;
#ManyToOne(optional = false)
#JoinColumn(name = "state_id", referencedColumnName = "id", nullable = false)
State state;
}
#Entity
public class City
{
#Id
private Long id;
#ManyToOne(optional = false)
#JoinColumn(name = "county_id", referencedColumnName = "id", nullable = false)
County county;
}
You can get all cities of a country with:
public interface CityRepository extends JpaRepository<City, Long>
{
List<City> findByCounty(County county); // county is a direct field of City
#Query("SELECT c FROM City c WHERE c.county.state.country = ?1")
List<City> findByCountry(Country country);
}

Unconsistent entity object collection state

I am having inconsistent object state when using Hibernate
I have class School that have a collection of student
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "LINK_SCH_STUDENT", joinColumns = #JoinColumn(name = "SCHOOL_ID"),
inverseJoinColumns = #JoinColumn(name = "STUDENT_ID"))
#LazyCollection(LazyCollectionOption.EXTRA)
private List<Students> associatedStudents;
I have a transactional method that links a student to a school:
#Transactional
LinkStudentToSchool (schoolId, StudendID){
flush();
int result = getSession().createSQLQuery("INSERT INTO " + getSchemaName()
+ ".LINK_SCH_STUDENT(SCHOOL_ID, STUDENT_ID) " +
"Values (:recipient_group_id, :contentId)")
}
I am writing integration test for this method.
#Transactional
TestLinkStudentToSchool {
school =new School ("GeorgiaTech", ....)
schoolId = saveOrUpdate (school)
studentID = saveOrUpdate (new Student ("LazyStudent", ....))
LinkStudentToSchool (schoolId, studentID)
Hibernate.initialize (school.getAssociatedStudent);
school.getAssociatedStudent // RETURN NULL !!!
}
Why is this happening, I already called a transactional method to add a student a school (update the link table), but the state of the collection of students
in the school did not get updated !!
Is this a caching issue ? is it because I have nested #transactional?
I will really appreciate any feedback
Thanks
You start by creating a Scool instance and saving it. It becomes associated to the Hibernate session. At this time, the list of students is null, because you neglected to initialize the list to an empty list (this should be the default state of a new school).
Then you execute a SQL query, like if Hibernate didn't exist and you were using JDBC. Hibernate doesn't know what this query does. It doesn't know that the query inserts a row to the join table. And it can't magically know that this corresponds to a new association between the student and the school. It can't magically initialize the field of your school in memory and replace it with a list containing the student. Don't bypass Hibernate by using SQL queries. Use the entities:
// correct default state of a school:
private List<Students> associatedStudents = new ArrayList<>();
...
School school = new School("GeorgiaTech", ....);
session.save(school);
Student student = new Student("LazyStudent", ....);
session.save(student);
// now associate the student with the school:
school.getAssociatedStudents().add(student);
// done. Hibernate will insert the row in the association table for you.

Categories

Resources