I am building a spring boot backend and trying to solve my Lazy Loading Problems with a Dto or the join fetch solution. I've got the following entity:
#Entity
public class Project implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//Some other stuff
#OneToMany(cascade = CascadeType.REMOVE)
#JoinTable(name = "project_categories",
joinColumns = #JoinColumn(name = "project_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "categories_id", referencedColumnName = "id"))
private Set<Categories> categories;
#OneToMany(cascade = CascadeType.REMOVE)
#JoinTable(name = "project_services",
joinColumns = #JoinColumn(name = "project_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "services_id", referencedColumnName = "id"))
private Set<Services> services;
//Constructors, Getters, Setter, ...
And in addition to that the Categories/Services Entity.
#Entity
#Table(name = "categories_table")
public class Categories {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long catId;
#ManyToOne
#JsonIgnore
private Project project;
//Construcor, Getter, Setter, ...
Now as I mentioned before I've tried to use a dto or the Join fetch method.
With my Join Fetch Method I've got the following Repo setup:
#Query("SELECT p FROM Project p JOIN FETCH p.categories " +
"JOIN FETCH p.services WHERE p.id = :projectId")
Optional<Project> findFullProjectById(#Param("projectId") Long id);
Nothing special here, but when I try to run it I get a ObjectNotFound Exception. But I know for sure it exists.
With the dto approach I've got the following setup:
#Query("SELECT new path.ProjectDto(p, p.categories, " +
"p.services) FROM Project p WHERE p.id = :projectId")
Optional<Project> findFullProjectById(#Param("projectId") Long id);
Here I've got a SQL Syntax Error when executing the above method.
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '. as col_1_0_, . as col_2_0_ from Project project0_ inner join project_categorie' at line 1
Which makes sense, because spring boot tries to execute the following query:
select project0_.id as col_0_0_, . as col_1_0_, . as col_2_0_ from Project project0_ inner join project_categories categories1_ on project0_.id=categories1_.project_id inner join categories_table categories2_ on categories1_.categories_id=categories2_.id inner join project_services services3_ on project0_.id=services3_.project_id inner join services_table services4_ on services3_.services_id=services4_.id where project0_.id=?
I really hope some of you guys could help me here.
Cheers, Nicklas
Related
I have 2 entities with a ManyToMany association between them - FeedbackApp & FeedbackAppProfile and each of them has a tenant-id FK to Tenant entity.
FeedbackApp entity:
public class FeedbackApp {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "tenant_id")
private Tenant tenant;
/*
Marked as the owner side.
*/
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "feedbackApp_profile_mapping",
joinColumns = #JoinColumn(name = "feedbackApp_id"),
inverseJoinColumns = #JoinColumn(name = "profile_id"))
Set<FeedbackProfile> profiles;
}
The FeedbackProfile entity:
public class FeedbackProfile {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "tenant_id")
private Tenant tenant;
#ManyToMany(mappedBy = "profiles", fetch = FetchType.EAGER)
Set<FeedbackApp> feedbackApps;
}
The feedbackApp_profile_mapping join table has 2 columns and looks like this:
My question: I need to create a query that gets all feedback apps for a specific feedback profile and tenant id. Is it possible to get it with Hibernate/JPA OR I have to manually query my join table?
Let Jpa worry about the optimal Sql query to generate. Your Jpa/criteria/specification query should be something like
select fp.feedbackApps from FeedbackProfile fp LEFT JOIN FETCH fp.feedbackApps where fp.id=:feedback_profile_id and fp.tenant.id=:tenant_id
Since you are asking about efficiency, better remove fetch = FetchType.EAGER from the two many-to-many mappings and use join fetch or named entity graphs to do the joins only when you need to.
Thanks to #GabiM direction, I created a join fetch query which did the job for what I needed:
#Query(value = "SELECT f FROM FeedbackApp f JOIN FETCH f.profiles p WHERE p.id = ?1 AND p.tenant.id = ?2")
Set<FeedbackApp> getFeedbackAppsByProfileId(long profileId, long tenantId);
I've made some research but without any specific answer.
I know how #JsonView... #JsonIgnore works... but my point here is about back end, the point of view from there. I'm working on spring boot and by default OSIV is enabled, so as far as I know, if I'm not wrong, if I make a call in database on an #Entity that has #ManyToMany association it will eagerly fetch everything.
Till there I have no issues, the problem is that the associated Collection also has Collections... And some services need to fetch them and others don't... Then I keep getting LazyInitializationException.
#Entity
#EntityListeners(AuditingEntityListener.class)
#Inheritance(strategy = InheritanceType.JOINED)
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(nullable = false, updatable = false)
private int id;
private String categoryTitle;
private String categoryDescription;
#ManyToMany
#JoinTable(
name = "Category_Parent",
joinColumns = #JoinColumn(name = "id_category", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "id_category_parent")
)
private Set<Category> parentCategory;
#ManyToMany
#JoinTable(
name = "Category_Parent",
joinColumns = #JoinColumn(name = "id_category_parent", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "id_category")
)
private Set<Category> subCategory;
Then to prevent that error I used #Query like this
#Repository
public interface CategoryRepository extends JpaRepository<Category, Integer> {
#Query("from Category c left join fetch c.subCategory left join fetch c.parentCategory")
List<Category> getAllCategories();
}
Now I'm able to fetch it lazly... I used that #Query way because it is the only one I know to fetch the associated Collections... I heared about EntityGraph and Hibernate.initialize() but have no knowledge on how to proceed (would appreciate some link).
So, then I have Json exception because the json response is infinite. How can I avoid this new issue? Using DTO?
I appreciate.
------ EDIT ------
I've used #JsonView on the properties that I want to send as response, however if I use #JsonView over subCategory only, it works, but if I use on parentCategory I got the infinite loop once again... Can't solve it.
You can add fetch = FetchType.LAZY to your #ManyToMany or #OneToMany annotation like this: #ManyToMany(fetch = FetchType.LAZY). More instruction is at https://www.baeldung.com/hibernate-lazy-eager-loading
Scenario:
I have two entities User and Program
#Entity
#Table(name = "user")
public class UserEntity implements Serializable{
#Id
public Long id;
public String firstName;
public String email;
#OneToOne(cascade = CascadeType.ALL)
#JoinTable(
name = "user_program",
joinColumns = {
#JoinColumn(name = "user_id", referencedColumnName = "id")
},
inverseJoinColumns = {
#JoinColumn(name = "program_id", referencedColumnName = "id")
}
)
public ProgramEntity program;
}
JPQL:
SELECT
Program.name,
COUNT(user.id) AS user_count
FROM UserEntity AS user
INNER JOIN ProgramEntity AS program on ________ GROUP BY Program.name
I tried to get the number of users in each program but I couldn't get the result due to the JoinTable (intermediate table) is not an entity. Can anyone suggest a JPQ to connect the join table?
You can join using the entity object, 'ON' is not necessary. For you example,
SELECT prg.name, COUNT(user.id) AS user_count FROM UserEntity AS user INNER JOIN user.program AS prg GROUP BY prg.name
I have a example JPA entity:
#Entity
#Table(name = "tb_group")
public class Group
{
...
#ManyToMany
#JoinTable(name = "tb_group_user",
joinColumns = #JoinColumn(name = "fk_group_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "fk_user_id", referencedColumnName = "id"))
private List<User> users;
private Integer userSize;
...
}
My question is, how can i initialize userSize field, with the size value of users field,that is a Lazy Load field?
I know its a dumb question, but i can't find a good strategy to solve this problem.
I tried this solution, but haven't succeed:
private Integer userSize = users.size();
I'm confused with this problem. Can you help me with an example?
EDIT:
I tried solution #Formula("select count(gu.fk_user_id) from tb_group_user gu where gu.fk_group_id = id") suggested by Ady Junior, but i receive this exceptions, when i try get groups:
ERROR org.hibernate.engine.jdbc.spi.SqlExceptionHelper You have an error in your SQL syntax;
check the manual that corresponds to your MariaDB server version for the right syntax to use near
select count(gu.fk_user_id) from tb_group_user gu where gu.fk_group_id = group
ERROR br.com.loopec.loopkey.server.controller.ExceptionsHandler
Unhandled Exception: org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nested exception is
org.hibernate.exception.SQLGrammarException: could not extract ResultSet
EDIT 2:
I got it solved the problem. Ady Junior give me a good solution, and the error was due to my stupidity. Inside #Formule("select count(gu.fk_user_id) from tb_group_user gu where gu.fk_group_id = id") i forgot putied parentheses '(' ')' between query.
The correct solution for my problem this:
#Entity
#Table(name = "tb_group")
public class Group
{
...
#ManyToMany
#JoinTable(name = "tb_group_user",
joinColumns = #JoinColumn(name = "fk_group_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "fk_user_id", referencedColumnName = "id"))
private List<User> users;
#Formula("(select count(gu.fk_user_id) from tb_group_user gu where gu.fk_group_id = id)")
private Integer userSize;
...
}
Thanks Ady Junior and Thanks Christian Beikov
devsaleh, try use #Formula to write a count query.
There are many features about this annotation. For instance, in this awesome post written by Vlad Mihalcea: https://vladmihalcea.com/how-to-map-calculated-properties-with-jpa-and-hibernate-formula-annotation/
#Entity
#Table(name = "tb_group")
public class Group
{
...
#ManyToMany
#JoinTable(name = "tb_group_user",
joinColumns = #JoinColumn(name = "fk_group_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "fk_user_id", referencedColumnName = "id"))
private List<User> users;
#Formula("(select count(gu.fk_user_id) from tb_group_user gu where gu.fk_group_id = id)")
private Integer userSize;
...
}
#devsaleh, Thank you very much!
Best Regards!
You could use extra-lazy collections with #LazyCollection(LazyCollectionOption.EXTRA) but I wouldn't recommend that: https://vladmihalcea.com/hibernate-extra-lazy-collections/
The approach Ady Junior proposed i.e. to use #Formula("select count(gu.fk_user_id) from tb_group_user gu where gu.fk_group_id = id") is a way you could go, but probably it's better to use a DTO query and determine the size in the query you use to load the data. Something like this
entityManager.createQuery("SELECT g.name, SIZE(g.users) FROM Group g")
I am trying to implement an example like this: A Person class has a list of places that it likes. But when I want to query it, I want result as each person with only the most favorite place(just the first one not all of them). So I have done this:
#Entity
class Person{
...
#ManyToMany(cascade = {CascadeType.REFRESH,CascadeType.PERSIST}, fetch = FetchType.EAGER)
#JoinTable(name = "person_favorite_place",
joinColumns = #JoinColumn(name = "person_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "place_id", referencedColumnName = "id")
)
#OrderColumn(name="favorite_place_order")
List<Place> favoritePlaces;
}
And in repository, I did:
public interface PersonRepository extends JpaRepository<Person, Long> {
#Query(value = "select person0_.id as id1_0_0_, person0_.age as age2_0_0_, person0_.name as name3_0_0_, favoriteme1_.person_id as person_id1_1_1_, place2_.id as place_id2_1_1_, favoriteme1_.favorite_place_order as favorite3_1_, place2_.id as id1_3_2_, place2_.invented as invented2_3_2_, place2_.name as name3_3_2_ from person person0_ left outer join person_favorite_place favoriteme1_ on person0_.id=favoriteme1_.person_id left outer join place place2_ on favoriteme1_.place_id=place2_.id where person0_.id=:personId and favoriteme1_.favorite_place_order = 0", nativeQuery = true)
Person getPersonWithFavoritePlace(#Param("personId") Long personId);
}
As expected, the query needs to return 1 Place for each Person but it always returns all the places that Person likes. What am I doing wrong in here?