I am trying to map an Entity from 3 tables, using #Entity, #Table, #SecondaryTables and #Column annotations as illustrated in the following example.
The Tables are:
1. employee (primary)
2. employee_detail
3. job_description
#Entity
#Table(name="EMPLOYEE")
#SecondaryTables({
#SecondaryTable(name="EMPLOYEE_DETAIL",
pkJoinColumns = #PrimaryKeyJoinColumn(name="EMPLOYEE_ID")),
#SecondaryTable(name="JOB_DESCRIPTION",
pkJoinColumns = #PrimaryKeyJoinColumn(name="JD_ID"))
})
public class Employee {
#Id
#Column(name = "ID", table= "EMPLOYEE")
private int id;
#Column(name = "FIRST_NAME", table = "EMPLOYEE")
private String firstname;
#Column(name = "LAST_NAME", table = "EMPLOYEE")
private String lastname;
#Column(name = "BANK_ACCOUNT_NO", table = "EMPLOYEE_DETAIL")
private String bankacctnumber;
#Column(name = "JOB_SUMMARY", table = "JOB_DESCRIPTION")
private String jobsummary;
#Column(name = " ??? ", table = " ?? ")
private String uniqueid;
//getters and setters for above fields
...
}
My question is, if I would like to create the field "uniqueid" by concatenating
column "ID" in table "EMPLOYEE" AND column "JOB_CODE" in table "JOB_DESCRIPTION"
This entity corresponds with the following sql query string (I used string builder for clarity):-
StringBuilder sql = new StringBuilder();
sql.append("SELECT");
sql.append(" e.FIRST_NAME AS firstname,");
sql.append(" e.LAST_NAME AS lastname,");
sql.append(" d.BANK_ACCT_NUMBER AS bankacctnumber,");
sql.append(" j.JOB_SUMMARY AS jobsummary,");
sql.append(" CONCAT(e.ID,SUBSTR(j.JOB_CODE,3,8)) AS uniqueid");
sql.append(" FROM employee e");
sql.append(" LEFT JOIN employee_detail d ON d.EMPLOYEE_ID = e.ID");
sql.append(" LEFT JOIN job_description j ON j.JD_ID = e.JD_ID ");
sql.append(" WHERE e.ID = 1 ");
exactly how should the mapping of the columns be done for the field "uniqueid"? Is this possible?
No this is not possible because JPA is using the mapped columns for reading AND writing. So a column can not be concatenated.
But you can write a simple method in your class that concatenates the fields.
Another possibility to could be a view that uses your SQL statement but then you can only read and now write because the view contains fields of more than one table.
Related
How to join a table with a selection using CriteriaBuilder?
Suppose I have this query:
SELECT
tbl1.*,
tbl2.total
FROM
table_1 tbl1
LEFT JOIN
(SELECT col_id AS id, SUM(value) AS total FROM table_2 WHERE active = 1 GROUP BY col_id) tbl2
ON
tbl1.id = tbl2.id;
Where the definition of table_1 is:
CREATE TABLE table_1(
id NUMBER(19, 0),
-- other columns
)
... and table_2 ...
CREATE TABLE table_2(
id NUMBER(19, 0),
col_id NUMBER(19, 0),
value NUMBER(14, 2),
-- other columns
FOREING KEY (col_id) REFERENCES table_1(id);
)
This is not possible with plain JPA or Hibernate. With Hibernate you can model this query if the subquery is static, but for dynamic subqueries you will need something like Blaze-Persistence which works on top of JPA/Hibernate and provides support for these things.
For the static query solution you can do this:
#Entity
#Table(name = "table1")
public class Table1 {
#Column(name = "id")
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "id", insertable = false, updatable = false)
private Table2 table2;
}
#Entity
#Subselect("SELECT col_id AS id, SUM(value) AS total FROM table_2 WHERE active = 1 GROUP BY col_id")
public class Table2Query {
#Column(name = "id")
private Integer id;
#Column(name = "total")
private BigDecimal total;
}
Here is a nice article by Vlad Mihalcea about Blaze-Persistence if you want a dynamic solution i.e. where the query structure isn't fixed: https://vladmihalcea.com/blaze-persistence-jpa-criteria-queries/
Use the join method in root and use it to get the values from the other table.
Note: you need to add the relation in the entity depending on the relationship of these tables (onetone, manytoone or onetomany). Something like this:
Entity code Table1:
#OneToOne
private Table2 table2;
Search code example:
(Root<Table1> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
Join<Table1, Table2> joinTable2 = root.join("table2");
cb.equal(joinTable2.get("active"), 1);
.. other filters ..
};
I'm getting what I think are needless CROSS JOINs when I'm doing a select IN SUBQUERY, which is hurting performance. I'm using Postgres if that makes a difference.
I'm aiming to generate the following query
select a1.first_name from author a1
where a1.last_name = ?
and (a1.id in
(select distinct b.author_id
from book b
where (b.published_on between ? and ?)
group by b.author_id
having count(b.author_id) >= 2))
But I get
select a1.first_name from author a1
where a1.last_name = ?
and (a1.id in
(select distinct b.author_id
from book b
cross join author a2 where b.author_id = a2.id -- <<< I don't want this cross join!
and (b.published_on between ? and ?)
group by b.author_id
having count(b.author_id) >= 2))
Code
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<String> cq = cb.createQuery(Author.class);
Root<Author> authorRoot = cq.from(Author.class);
Subquery<Long> countSubquery = cq.subquery(Long.class);
Root<Book> bookRoot = countSubquery.from(Book.class);
Expression<Long> count = cb.count(bookRoot.get(Book_.author));
countSubquery.select(bookRoot.get(Book_.AUTHOR))
.distinct(true)
.where(cb.between(bookRoot.get(Book_.publishedOn),
LocalDate.of(2021, MARCH, 1),
LocalDate.of(2021, MARCH, 31)))
.groupBy(bookRoot.get(Book_.author))
.having(cb.greaterThanOrEqualTo(count, 2L));
cq.where(
cb.equal(authorRoot.get(Author_.lastName), "Smith"),
cb.in(authorRoot.get(Author_.ID)).value(countSubquery));
cq.select(authorRoot.get(Author_.FIRST_NAME));
TypedQuery<String> query = entityManager.createQuery(cq);
return query.getResultList();
In reality I'm generating the queries from a user driven query builder, this code recreates the exact problem I'm having.
When using the query builder the user could end up with multiple select in subqueries so I need this to perform as well as possible.
I don't see why I should need any join/cross join for my query to work.
Entities
#Entity
public class Author {
#Id
#GeneratedValue
private Long id;
private String firstName;
private String lastName;
#OneToMany(mappedBy = "author", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Book> books;
}
#Entity
public class Book {
#Id
#GeneratedValue
private Long id;
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "author_id")
private Author author;
private LocalDate publishedOn;
}
This expression: bookRoot.get(Book_.author) means you're joining Author to Book implicitly.
To get rid of the extra join, you would have to either use a native query, or map Book.author_id once more as a simple column:
#Column(name = "author_id", insertable = false, updatable = false)
private Long authorId;
And use Book_.authorId instead.
The below code snippet does not work:
#Id
private Long id;
#ManyToMany(fetch = FetchType.LAZY)
#JoinFormula("""
(SELECT DISTINCT ON (product11.id, text26.language, textsourced28.source) text26.id
FROM product AS product11
JOIN producttexttitle AS producttexttitle27 ON product11.id = producttexttitle27.product
JOIN text AS text26 ON producttexttitle27.text = text26.id
JOIN textsourced AS textsourced28 ON text26.id = textsourced28.text
WHERE product11.id=id
ORDER BY product11.id, text26.language, textsourced28.source, textsourced28.time DESC)
""")
private List<Text> titles;
However this does,
#Id
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinFormula("""
(SELECT DISTINCT ON (product11.id, text26.language, textsourced28.source) text26.id
FROM product AS product11
JOIN producttexttitle AS producttexttitle27 ON product11.id = producttexttitle27.product
JOIN text AS text26 ON producttexttitle27.text = text26.id
JOIN textsourced AS textsourced28 ON text26.id = textsourced28.text
WHERE product11.id=id
ORDER BY product11.id, text26.language, textsourced28.source, textsourced28.time DESC
LIMIT 1)
""")
private Text titles;
The only problem is that the latter one only returns one instance, where I am looking for several.
Can figure it out.
#CollectionTable or #ElementCollection usage perhaps?
I need to supply a custom sql.
I have a User entity with skills property as a type List. I want to query the User table against a list of skills in such a way that if all the skills are present in skill column then only a match is found unless no.
I have used JPQL for this but it matches each element in the list one by one using the IN clause.
User Class
#Entity(name = "App_User")
//table name "user" is not allowed in postgres
public class User {
#Id
#GeneratedValue(generator = "UUID")
#GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
#Column(name = "user_id", updatable = false, nullable = false)
#Setter(AccessLevel.NONE)
private UUID id;
#Column(name = "user_name")
#NotBlank(message = "Name is mandatory")
private String name;
#Column(name = "user_email")
#NotBlank(message = "Email is mandatory")
private String email;
// Current point balance of the user
#Column(name = "points")
private int points;
#ElementCollection(fetch = FetchType.EAGER)
#Column(name = "skills")
#NotEmpty
private List<String> skills = new ArrayList();
}
JPA query that I have used is
SELECT u FROM App_User u JOIN u.skills skill where skill in :skillList
If I want to match a list of skills like this Arrays.asList("skill1","skill2","skill3")then I want only those users in the result who have all of these skills, not one or two. Above used IN clause return the same result.
I have read that it is not possible to compare two lists in JPQL so how can I achieve this using CriteriaBuilder CriteriaQueryAPI?
You can do this
#Query(value = "SELECT u FROM User u LEFT JOIN u.skills sk WHERE sk IN :skillList"
+ " GROUP BY u HAVING COUNT( sk) = :skillListSize")
List<User> findBySkills(#Param("skillList") List<String> skills,
#Param("skillListSize") long skillListSize);
Here, group by user and then check group having all skills or not using size. So it will fetch all user having all skills those are given.
Or use this way
#Query(value = "SELECT u FROM User u LEFT JOIN u.skills sk GROUP BY u"
+ " HAVING SUM(CASE WHEN sk IN (:skillList) THEN 1 ELSE 0 END) = :skillListSize")
List<User> findBySkills(#Param("skillList") List<String> skills,
#Param("skillListSize") long skillListSize);
And if you want a solution for user having exact same skill not more than the given list then see this solution.
The problem that you want to solve is called Relational Division.
SELECT u.* FROM App_User u
INNER JOIN
(
SELECT skills FROM App_User WHERE skills IN (list values)
GROUP BY skills
HAVING COUNT(DISTINCT skills) = (size of list)
) w ON u.user_name = w.user_name
I want to fetch records from database without the primary key, for a table that i have created. Here is the model class for it.
#Entity
#Table(name = "DOCTORS_INFORMATION")
public class DoctorInformation {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "DOCTOR_ID")
private int ID;
#Column(name = "FIRST_NAME")
private String firstName;
#Column(name = "LAST_NAME")
private String lastName;
#Column(name = "ADDRESS_LINE1")
private String addressLine1;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "CITY")
private Location location;
//along with setters and getters
It is very difficult to remember the Doctor_Id for every doctor, and logically First_Name or anything cannot be made a primary key. All the tutorials and lecture videos that i have gone through fetch records using session.get where session is an object of class Session. In the source code of Session class all the overloaded methods of get mandatory take id as a parameter...
Is their a workaround for the above problem?
There are multiple options. All of the following examples search for a doctor whose lastName contains Johnson.
Criteria
String lastNameToSearchFor = "Johnson";
List doctors = session.createCriteria(DoctorInformation.class)
.add( Restrictions.like("lastName", "%"+lastNameToSearchFor+"%") ) // the leading wild card can become a problem since it cannot be indexed by the DB!
.addOrder( Order.asc("lastName") )
.list();
http://docs.jboss.org/hibernate/orm/3.5/javadocs/org/hibernate/Criteria.html
HQL
String lastNameToSearchFor = "Johnson";
String hql = "FROM " + DoctorInformation.class.getName() + " di WHERE di.lastName LIKE :lastName ORDER BY di.lastName ASC";
Query query = session.createQuery(hql);
query.setParameter("lastName", "%" + lastNameToSearchFor + "%"); // again, the leading wild card may be a problem
List doctors = query.list();
http://www.tutorialspoint.com/hibernate/hibernate_query_language.htm
Native SQL
String lastNameToSearchFor = "Johnson";
String sql = "SELECT * FROM DOCTORS_INFORMATION WHERE lastName LIKE :lastName ORDER BY lastName ASC";
Query query = session.createSQLQuery(sql).addEntity(DoctorInformation.class);
query.setString("lastName", "%" + lastNameToSearchFor + "%"); // again, the leading wild card may be a problem
List doctors = query.list();
If you want to add capability to search with, let's say firstName, then you might want to look into Hibernate's Disjunction: Creating a hibernate disjunction query programatically