Interface projections with nested associations - java

Let's assume there are two entities:
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Author {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
#ManyToMany(mappedBy = "authors")
private Set<Book> books;
}
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String fileName;
private String fileType;
#Lob
private byte[] data;
#ManyToMany
#JoinTable(
name = "book_authors",
joinColumns = #JoinColumn(name = "book_id"),
inverseJoinColumns = #JoinColumn(name = "author_id"))
private Set<Author> authors;
}
The following DTO interface projection is used to query only the columns that are needed.
public interface AuthorView {
String getFirstName();
String getLastName();
Set<BookView> getBooks();
interface BookView {
String getTitle();
}
}
A simple findAllBy query method is declared in the repository:
public interface AuthorRepository extends JpaRepository<Author, Long> {
#EntityGraph(attributePaths = "books")
List<AuthorView> findAllBy();
}
The method executes the following query:
select
author0_.id as id1_0_0_,
book2_.id as id1_1_1_,
author0_.first_name as first_na2_0_0_,
author0_.last_name as last_nam3_0_0_,
book2_.data as data2_1_1_,
book2_.file_name as file_nam3_1_1_,
book2_.file_type as file_typ4_1_1_,
book2_.title as title4_1_1_,
books1_.author_id as author_i2_2_0__,
books1_.book_id as book_id1_2_0__
from
author author0_
left outer join
book_authors books1_
on author0_.id=books1_.author_id
left outer join
book book2_
on books1_.book_id=book2_.id
Even though the projection doesn't contain data, file_name, and file_type properties, they are fetched from the database which is causing performance issues, especially if the files are large.
The problem is that Spring Data JPA fetches the entire entities and uses them to perform a programmatic mapping, according to Thorben Janssen's blog.
Is there any solution to prevent fetching entire entities when using interface-based DTO projections except for writing massive queries?

Recently, I found out Blaze Persistence - Entity View Module, which looks promising.
According to #Christian Beikov's answer, EntityView projections can be used almost like Spring Data Projections with the Spring Data integration, and those projections fetch only the necessary properties. Additionally, there is no need to use #EntityGraph, since the integration handles it ad-hoc by adapting the query generation.

Related

Java entity modeling with table that relates to several others

I have a doubt about how the modeling of my entity would be. Come on, I have a table in the database that serves to save documents from my system, this table has the columns id, fk_id (element foreign key), fk_table (entity name) and file_name (stores the name of my file) .
I did a lot of research before posting my question here, but I didn't find anything related to it, what would my entities, user, patient and doctor?
DB:
id
fk_id
fk_table
file_name
1
21
user
test1.jpg
2
32
doctor
test2.pdf
3
61
user
test10.pdf
4
100
patient
test5.jpg
Class:
public class User{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String LastName;
// What would a one-to-many relationship look like?
}
public class patient{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
// What would a one-to-many relationship look like?
}
You can use #Where. But be aware that #Where is a Hibernate annotation. It's not in the JPA standard.
For example in the User entity: (I assume that your table is mapped to an entity called Document)
#Where( clause = "fk_table = 'user'")
#JoinColumn(name = "fk_id")
#OneToMany
private List<Document> documents = new ArrayList<>( );
The following is based only on standard JPA annotations. The idea is to create an inheritance hierarchy for the documents table. The base is:
#Entity
#Table(name = "XX_DOCUMENT")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "fk_table")
public abstract class BaseDocument {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#Column(name = "file_name")
private String fileName;
}
Here we define that all entities extending this will go to the same table, with the fk_table column to discriminate. The entities extending it are defined as follows:
#Entity
#DiscriminatorValue("doctor")
public class DoctorDocument extends BaseDocument {
#ManyToOne
#JoinColumn(name = "fk_id")
private Doctor doctor;
}
#Entity
#DiscriminatorValue("patient")
public class PatientDocument extends BaseDocument {
#ManyToOne
#JoinColumn(name = "fk_id")
private Patient patient;
}
// and so on
The interesting thing is that we are reusing the column fk_id to point to the right table. From a small experiment, Hibernate seems to not have problems with it. I would suggest that you manage the DB creation another way just to be safe.
The Doctor, Patient etc need not have a common base class, e.g.:
#Entity
#Table(name = "XX_DOCTOR")
public class Doctor {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#OneToMany(mappedBy = "doctor")
private Collection<DoctorDocument> documents = new ArrayList<>();
// any doctor-specific fields
}
#Entity
#Table(name = "XX_PATIENT")
public class Patient {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#OneToMany(mappedBy = "patient")
private Collection<PatientDocument> documents = new ArrayList<>();
// any patient-specific fields
}
// and so on
You can read a (doctor, patient, ...)'s documents from the relevant collection. You can even query BaseDocument instances based on any criteria.
You can even go ahead and do more fabcy stuff with the Java code. E.g. define an interface HasDocuments:
public interface HasDocuments<D extends BaseDocument> {
Collection<D> getDocuments();
}
Doctor, Patient, ..., implements this, so they can all be treated the same way.

How to join 4 tables in 1 DTO with Jpa Query

So I have 4 tables
An Employer
#Entity
#EqualsAndHashCode(callSuper = false)
#Table(name = "employers")
#NoArgsConstructor
#AllArgsConstructor
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "jobPostings"})
#PrimaryKeyJoinColumn(name="employer_id", referencedColumnName = "id")
public class Employer extends User {
#Column(name = "company_name")
private String companyName;
#Column(name = "website")
private String website;
#Column(name = "phone_number")
private String phoneNumber;
#OneToMany(mappedBy="employer")
private List<JobPosting> jobPostings;
}
A City
#Data
#Entity
#Table(name="cities")
#AllArgsConstructor
#NoArgsConstructor
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "jobPostings"})
public class City {
#Id
#Column(name="id")
private int id;
#Column(name="city_name")
private String cityName;
#OneToMany(mappedBy="city")
private List<JobPosting> jobPostings;
}
A Job Position
#AllArgsConstructor
#Data
#Entity
#Table(name="job_positions")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "jobPostings"})
public class JobPosition {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="job_position_id")
private int id;
#Column(name="title")
private String title;
#OneToMany(mappedBy="jobPosition")
private List<JobPosting> jobPostings;
}
And A JobPosting(like a job advertisement)
#Entity
#Table(name="job_postings")
#Data
#NoArgsConstructor
#AllArgsConstructor
//#JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "city", "jobPosition","employer"})
public class JobPosting {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
private int id;
#Column(name="job_requirements")
private String jobRequirements;
#Column(name="salary_min")
private int salaryMin;
#Column(name="salary_max")
private int salaryMax;
#Column(name="application_deadline")
private LocalDate applicationDeadline;
#Column(name="number_of_openings")
private int numberOfOpenings;
#Column(name="stream_date")
private LocalDate streamDate;
#ManyToOne()
#JoinColumn(name="city_id")
private City city;
#ManyToOne()
#JoinColumn(name="job_position_id")
private JobPosition jobPosition;
#ManyToOne()
#JoinColumn(name= "employer_id")
private Employer employer;
}
I have implemented the necessary mapping for all of them and it is working fine.
However, I want to Join them in a DTO like:
#Data
#AllArgsConstructor
#NoArgsConstructor
public class JobPostingWithJobPositionCityEmployerDto {
private int id;
private String jobRequirements;
private int salaryMin;
private int salaryMax;
private LocalDate applicationDeadline;
private int numberOfOpenings;
private LocalDate streamDate;
private String cityName;
private String title;
private String companyName;
}
to get the fields I want in a more clean way, I am trying to use #Query annotation of springframework.jpa but I can not quite manage it since I just learned about this, the query I am using is:
#Query(value ="Select new kodlamaio.hrms.entities.dtos.JobPostingWithJobPositionCityEmployerDto"
+ "(j.id, j.jobRequirements, j.salaryMin, j.salaryMax, j.numberOfOpenings, j.streamDate, j.applicationDeadline, c.cityName, p.title, e.companyName)"
+ " From Employer e Inner Join e.jobPostings j, "
+ "From City c Inner Join c.jobPostings j, "
+ "From JobPosition p Inner Join p.jobPostings j", nativeQuery = true)
List<JobPostingWithJobPositionCityEmployerDto> getJobPostings();
I dont even know if this is the correct way to do this, I keep getting syntax error, I looked up answers but couldnt quite grasp what they were saying, there were a lot of different scenarios.
So if anybody could help me with this Query and recommend some sources to learn about different commands, I would appreciate it so much, Thanks in advance.
Mapping the result of a query with a DTO using the new keyword in a query only works for JPQL, it will not work for SQL (which is what you are using).
It also looks to me if you are trying to write a too complex query as everything can be achieved/reached through the JobPosting class, which will implicitly do the join when using JPQL.
So instead of your native SQL writing a JPQL should fix it.
Something like
#Query(value ="Select new kodlamaio.hrms.entities.dtos.JobPostingWithJobPositionCityEmployerDto"
+ "(jp.id, jp.jobRequirements, jp.salaryMin, jp.salaryMax, jp.numberOfOpenings, jp.streamDate, jp.applicationDeadline, jp.city.cityName, jp.jobPosition.title, jp. employer.companyName)"
+ " From JobPosting jp)
Which should do the trick. Your JPA provider should be smart enough to figure out what to join and retrieve.
Generaly this is named Projection, I believe that you have already created JobPostingWithJobPositionCityEmployerDto with a constructor of 10 args, with their respective data type of course. And since it is custom to jpa you can't use nativeQuery = true. These modifications should be fine.
#vlad-mihalcea to the rescue
https://vladmihalcea.com/the-best-way-to-map-a-projection-query-to-a-dto-with-jpa-and-hibernate/

How to stop Hibernate from eagerly fetching a relationship when it is mapped using a column (referencedColumnName) different than the primary key?

I'm mapping a relationship that does not use the entity's primary key. Using "referencedColumnName" with a column different than the primary key causes hibernate to eagerly fetch the association, by issuing an extra select, even when it's tagged with FetchType.LAZY.
My goal is to make it behave like a regular mapping, meaning it wouldn't issue an extra query every time I need to query the main entity.
I have already tried using #LazyToOne(LazyToOneOption.NO_PROXY), which sorts out the problem, but it does not operate well with Jackson's (JSON parsing library) module "jackson-datatype-hibernate5", which skips hibernate lazy proxies when serializing the results.
Here is a scenario almost like the one I have that causes the problem:
Entities:
#Entity(name = "Book")
#Table(name = "book")
public class Book
implements Serializable {
#Id
#GeneratedValue
private Long id;
private String title;
private String author;
#NaturalId
private String isbn;
//Getters and setters omitted for brevity
}
#Entity(name = "Publication")
#Table(name = "publication")
public class Publication {
#Id
#GeneratedValue
private Long id;
private String publisher;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(
name = "isbn",
referencedColumnName = "isbn"
)
private Book book;
#Column(
name = "price_in_cents",
nullable = false
)
private Integer priceCents;
private String currency;
//Getters and setters omitted for brevity
}
Repository (Spring-Data, but you could try directly with the EntityManager):
#Repository
public interface PublicationRepository extends JpaReadRepository <Publication, Long>
{
#Query ("SELECT d FROM Publication d WHERE d.publisher = ?1 ")
Optional <Publication> findByPublisher (String isbn);
}
Thanks
The only way to achieve what you are looking for is by moving the annotatation #Id to the isbn property.
You can leave the #GeneratedValue on the autoincrement property.
Notes:
1 - Make sure that your equals/hc are following the OID(Object ID) on your domain case the "NaturalId" ISBN.
2 - It will be good to ensure if possible on DB level that your natural ID has unique contraint on it.

How to implement ligh Entity version with Jpa repository?

Have a "full Entity" class:
#Entity(name = "vacancy_dec_to_words")
public class VacancyDescriptionToWords {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#JoinColumn(name = "vacancy_description_id")
#ManyToOne(cascade = CascadeType.ALL)
private VacancyDescription vacancyDescription;
#JoinColumn(name = "words_id")
#ManyToOne
private Words words;
#Column(name = "qty")
private int qty;
#Column(name = "create_date")
private Date date;
//...getters and setters
In some methods I need use only 2 column from this database: word_id and qty
I try the follow way:
Projections
https://docs.spring.io/spring-data/jpa/docs/2.1.2.RELEASE/reference/html/#projections
public interface QtyWords {
Long getWords();
Integer getQty();
}
JpaReposytory:
*Query, that I use tested and it workable, I use him in JpaRepository:
#Repository
public interface SmallVDTWRepository extends JpaRepository<VacancyDescriptionToWords, Long> {
#Query(nativeQuery = true,
value = "SELECT sum(qty), words_id FROM vacancy_desc_to_words WHERE vacancy_description_id IN (" +
"SELECT id FROM vacancy_description WHERE vacancy_id IN (" +
"SELECT id FROM vacancy WHERE explorer_id = :exp))" +
"GROUP BY words_id")
List<QtyWords> getDistinctWordsByExplorer(#Param("exp") long exp);
}
But I get some interesting result when I get list of entities:
List<QtyWords> list = vdtwService.getByExplorerId(72);
I am not get any exceptions, but I have the list with are unknowns objects. This objects contains my data, which I need(qty and words_id), but I cannot get them from him.
Can I use this method (Projection) to implement this task and, in general, how to correctly implement the 'Light Entity' in this case?
Spring provides two mechanisms that can be used to limit data to be fetched.
Projections
Projections can help you to reduce data, retrieved from database, by setting what exactly attributes you want to fetch.
Example:
#Entity
class Person {
#Id UUID id;
String firstname, lastname;
#OneToOne
Address address;
}
#Entity
static class Address {
#Id UUID id;
String zipCode, city, street;
}
interface NamesOnly {
String getFirstname();
String getLastname();
}
#Repository
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
Entity graph
Annotation EntityGraph can help you to reduce amount of queries to database, by setting what exactly related entities you need to fetch.
Example:
#Entity
#NamedEntityGraph(name = "GroupInfo.detail", attributeNodes = #NamedAttributeNode("members"))
public class GroupInfo {
#Id UUID id;
#ManyToMany //default fetch mode is lazy.
List<GroupMember> members = new ArrayList<GroupMember>();
}
#Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
#EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
GroupInfo getByGroupName(String name); //Despite of GroupInfo.members has FetchType = LAZY, it will be fetched because of using EntityGraph
}
There are two types of EntityGraph:
EntityGraphType.LOAD - is used to specify an entity graph, attributes that are specified by attribute nodes of the entity graph are treated as FetchType.EAGER and attributes that are not specified are treated according to their specified or default FetchType.
EntityGraphType.FETCH - is used to specify an entity graph, attributes that are specified by attribute nodes of the entity graph are treated as FetchType.EAGER and attributes that are not specified are treated as FetchType.LAZY.
PS: Also remember that you can set lazy fetch type: #ManyToOne(fetch = FetchType.LAZY) and JPA will not fetching child entities when parent is being fetched.

Initializing many-to-many association in Hibernate with join table

I have a Company entity that I fetch with a JPQL query with Hibernate. The entity has a many-to-many association with a Keyword entity. Since the join table has an additional column is_active, this table has been mapped to a CompanyKeyword entity. So the association is like this:
Company <-- CompanyKeyword --> Keyword
Now, the association from the Company entity is lazy, and it is not initialized by my JPQL query, as I want to avoid creating a cartesian product performance problem. That is why I want to initialize the association after running the JPQL query, e.g. like this:
#Service
class CompanyServiceImpl implements CompanyService {
#Autowired
private CompanyRepository companyRepository;
#Transactional
public Company findOne(int companyId) {
Company company = this.companyRepository.findOneWithSomeCustomQuery(companyId);
Hibernate.initialize(company.companyKeywords());
return company;
}
}
For a "normal" many-to-many association, this would work great, as all of the associated entities would be fetched in a single query. However, since I have an entity between Company and Keyword, Hibernate will only initialize the first part of the association, i.e. from Company to CompanyKeyword, and not from CompanyKeyword to Keyword. I hope that makes sense. I am looking for a way to initialize this association all the way without having to do something like this:
Company company = this.companyRepository.findOneWithSomeCustomQuery(companyId);
Hibernate.initialize(company.getCompanyKeywords());
for (CompanyKeyword ck : company.getCompanyKeywords()) {
Hibernate.initialize(ck.getKeyword());
}
The above code is neither clean, nor good in terms of performance. If possible, I would like to stick to my current approach of using a JPQL query to fetch my Company entity and then initializing certain associations afterwards; it would take quite a bit of refactoring to change this in my project. Should I just "manually" fetch the association with a second JPQL query, or is there a better way of doing it that I haven't thought of?
Below are my mappings. Thanks in advance!
Company
#Entity
#Table(name = "company")
public class Company implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column
private int id;
#Size(max = 20)
#OneToMany(fetch = FetchType.LAZY, mappedBy = "company")
private Set<CompanyKeyword> companyKeywords = new HashSet<>();
// Getters and setters
}
CompanyKeyword
#Entity
#Table(name = "company_service")
#IdClass(CompanyServicePK.class)
public class CompanyKeyword implements Serializable {
#Id
#ManyToOne(fetch = FetchType.LAZY, targetEntity = Company.class)
#JoinColumn(name = "company_id")
private Company company;
#Id
#ManyToOne(fetch = FetchType.LAZY, targetEntity = Keyword.class)
#JoinColumn(name = "keyword_id")
private Keyword keyword;
#Column(nullable = true)
private boolean isActive;
// Getters and setters
}
CompanyKeywordPK
public class CompanyServicePK implements Serializable {
private Company company;
private Service service;
public CompanyServicePK() { }
public CompanyServicePK(Company company, Service service) {
this.company = company;
this.service = service;
}
// Getters and setters
// hashCode()
// equals()
}
Keyword
#Entity
#Table(name = "keyword")
public class Keyword {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column
private int id;
// Fields and getters/setters
}
You'll indeed need to execute an additional JPQL query, fetching the company with its companyKeyWords and with the keyword of each CompanyKeyWord.
You could also doing it by simply looping and initializing every entity, and still avoid executing too many queries, by enabling batch fetching.

Categories

Resources