I'm trying to convert the following SQL query into JPQL query:
SELECT *
FROM movie m INNER JOIN movie_genre mg ON m.id = mg.movie_id
WHERE mg.genre_id = (SELECT mg2.genre_id FROM movie_genre mg2 WHERE mg2.movie_id = ?1 AND mg2.movie_id <> mg.movie_id AND mg.genre_id = mg2.genre_id)
GROUP BY mg.movie_id ORDER BY count(*) DESC
The problem is that i don't have a model class to represent movie_genre table because it's auto generated table from ManyToMany relationship.
so is there any way to convert that query into JPQL
In the moment I'm using a native query instead:
#Query(value = "SELECT * FROM movie m INNER JOIN movie_genre mg ON m.id = mg.movie_id " +
"WHERE mg.genre_id = (SELECT mg2.genre_id FROM movie_genre mg2 WHERE mg2.movie_id = ?1 AND mg2.movie_id <> mg.movie_id AND mg.genre_id = mg2.genre_id) " +
"GROUP BY mg.movie_id ORDER BY count(*) DESC", nativeQuery = true)
Page<Movie> findRelatedMoviesToAMovieById(#Param("id") int id, Pageable pageable);
Edit :
here's the models:
Movie
#Entity
public class Movie extends DateAudit {
private Long id;
private String name;
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinTable(name = "movie_genre",
joinColumns = #JoinColumn(name = "movie_id"),
inverseJoinColumns = #JoinColumn(name = "genre_id")
)
private List<Genre> genres = new ArrayList<>();
}
Genre
#Entity
public class Genre {
private Long id;
private String name;
#ManyToMany(mappedBy = "genres", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<Movie> movies = new HashSet<>();
}
Despite the SQL query you provided, I re-thought your requirement and translated that to a JPQL. So far what I understood you were finding related movies to a movie having common genres. If this is correct, you can achieve this by
SELECT m FROM Movie m JOIN m.genres mg
WHERE mg.id IN
(SELECT g.id FROM Genre g JOIN g.movies gm WHERE gm.id = :movieId)
AND m.id <> :movieId
GROUP BY m ORDER BY count(*) DESC
This will generate the SQL as below
select distinct movie0_.id as id1_1_, movie0_.name as name2_1_
from movie movie0_ inner join movie_genre genres1_ on movie0_.id=genres1_.movie_id inner join genre genre2_ on genres1_.genre_id=genre2_.id
where (genre2_.id in
(select genre3_.id from genre genre3_ inner join movie_genre movies4_ on genre3_.id=movies4_.genre_id inner join movie movie5_
on movies4_.movie_id=movie5_.id where movie5_.id=?))
and movie0_.id <> ?
group by movie0_.id order by count(*) desc
Querying Collection Association in JPQL
When you are using JPQL, you are in the Object Relationship world. So when you are querying for an association that is a collection, you access them through that collection field. And when it is a ManyToMany association, so you are not having a join table that does have a mapped entity, you need to Join with the collection field. And JPA vendor automatically translates that to join with the join table.
Like if you are querying movies of certain genres, this will go
SELECT m FROM Movie m JOIN m.genres mg WHERE mg.id = :genreId
Performance Concern
As you notice in the generated SQL, there are many levels of join to fetch and filter the data. This leads to a performance bottleneck. To overcome this, you can have entity for the movie_genre table.
This scenario is fairly discussed here
The best way to map a many-to-many association with extra columns when using JPA and Hibernate
You do not need a model movie_genre in order to write a JPQL statement. Hibernate knows about it implicitly when doing a JPQL statement.
Example:
SELECT m from Movie m left join m.genres g where g.name = 'Action'
The bidirectional "m.genres g" works with all bidirectional JPQL statements, including many-to-many where the association model is implicit and not actually present.
Example Code:
#Entity
#Table(name = "MOVIE")
public class Movie {
#Id
#GeneratedValue
private Long id;
#Column
private String name;
#ManyToMany(cascade = {CascadeType.ALL})
#JoinTable(name = "movie_genre",
joinColumns = #JoinColumn(name = "movie_id"),
inverseJoinColumns = {#JoinColumn(name = "genre_id")}
)
private Set<Genre> genres = new HashSet<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Genre> getGenres() {
return genres;
}
public void setGenres(Set<Genre> genres) {
this.genres = genres;
}
}
#Entity
#Table(name = "GENRE")
public class Genre {
#Id
#GeneratedValue
private Long id;
#Column
private String name;
#ManyToMany(mappedBy = "genres", cascade = {CascadeType.ALL})
private Set<Movie> movies = new HashSet<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Movie> getMovies() {
return movies;
}
public void setMovies(Set<Movie> movies) {
this.movies = movies;
}
}
Working JPQL Example
#PersistenceContext
EntityManager entityManager;
#Test
#Transactional
public void test() {
Set<Movie> actionMovies = new HashSet<>();
Set<Movie> dramaMovies = new HashSet<>();
Set<Genre> dramaGenres = new HashSet<>();
Set<Genre> actionGenres = new HashSet<>();
Set<Genre> generes = new HashSet<>();
Movie actionMovie = new Movie();
actionMovie.setName("Batman");
actionMovies.add(actionMovie);
Movie dramaMovie = new Movie();
dramaMovie.setName("Forest Gump");
dramaMovies.add(dramaMovie);
Genre actionGenre = new Genre();
actionGenre.setName("Action");
generes.add(actionGenre);
actionGenres.add(actionGenre);
Genre dramaGenre = new Genre();
dramaGenre.setName("Drama");
generes.add(dramaGenre);
dramaGenres.add(dramaGenre);
//Bidirectional sets
actionGenre.setMovies(actionMovies);
dramaGenre.setMovies(dramaMovies);
actionMovie.setGenres(actionGenres);
dramaMovie.setGenres(dramaGenres);
genreRepository.saveAll(generes);
//Example JPQL join through not present association model.
Query query = entityManager.createQuery("SELECT m from Movie m left join m.genres g where g.name = 'Action'");
List<Movie> resultList = query.getResultList();
assertEquals("Batman",resultList.get(0).getName());
}
Related
I'm new in spring boot and now I'm trying to joining two tables where one of them are composite table (idk if it's have any effect or not). Please take a look on my code and help me.
Employees.java (Model)
#Entity
#Table(name = "employees")
public class Employees {
#Id
#Column(name = "emp_no")
private long emp_no;
#Column(name = "name")
private String name;
}
Salaries.java (Model, The composite table)
#Entity
#Table(name = "salaries")
public class Salaries {
#Column(name = "emp_no") //Primary Key
private long emp_no;
#Column(name = "salary")
private int salary;
#Column(name = "date") //Primary Key
private LocalDate date;
}
EmployeeRepo.java (Repository)
public interface EmployeesRepository extends JpaRepository<Employees, Long> {
String salary = "SELECT employees.name, salaries.salary FROM employees INNER JOIN salaries ON employees.emp_no=salaries.emp_no LIMIT 0,10";
#Query(value=salary, nativeQuery = true)
List<Employees> getUserSalary();
}
EmpController.java (Controller)
#GetMapping("/salary")
public ResponseEntity<List<Employees>> getSalaries(#RequestParam(required = false) String args){
List<Employees> employees = new ArrayList<Employees>();
employeesRepository.getUserSalary().forEach(employees::add);
if(employees.isEmpty()){
return new ResponseEntity<>(employees, HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(employees, HttpStatus.OK);
}
application.properties
spring.datasource.url= jdbc:mysql://127.0.0.1:3306/employees?useSSL=false
spring.datasource.username= ""
spring.datasource.password= ""
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto= update
And the error message:
Column 'emp_no' not found.
I dont understand why I get this error when I have emp_no column both on my tables in db.
I guess your problem is
String salary = "SELECT employees.name, salaries.salary FROM employees INNER JOIN salaries ON employees.emp_no=salaries.emp_no LIMIT 0,10";
#Query(value=salary, nativeQuery = true)
List<Employees> getUserSalary();
you are trying to save name,salary in Employees which only has emp_no, name. So the error is sating you have mapped name but emp_no is not available from query.
Create POJO/model like below
public class EmpSalary {
private String name;
private double salary;
}
Then update your repository to return EmpSalary
String salary = "SELECT employees.name, salaries.salary FROM employees INNER JOIN salaries ON employees.emp_no=salaries.emp_no LIMIT 0,10";
#Query(value=salary, nativeQuery = true)
List<EmpSalary> getUserSalary();
I would like to be able to include an #Entity from another table using a foreign key. I'm following guides but I'm still confused and can't seem to get this working. The end goal would be something like this:
#Entity
#Table(name = "Labor", schema = "dbo", catalog = "database")
public class LaborEntity {
private int laborId;
private Timestamp laborDate;
private Integer jobNumber;
private Integer customerId;
//mapping of customer to labor
private CustomerEntity customer;
#Id
#Column(name = "LaborID", nullable = false)
public int getLaborId() {
return laborId;
}
public void setLaborId(int laborId) {
this.laborId = laborId;
}
#Basic
#Column(name = "LaborDate", nullable = true)
public Timestamp getLaborDate() {
return laborDate;
}
public void setLaborDate(Timestamp laborDate) {
this.laborDate = laborDate;
}
#Basic
#Column(name = "JobNumber", nullable = true)
public Integer getJobNumber() {
return jobNumber;
}
public void setJobNumber(Integer jobNumber) {
this.jobNumber = jobNumber;
}
#Basic
#Column(name = "CustomerID", nullable = true)
public Integer getCustomerId() {
return customerId;
}
public void setCustomerId(Integer customerId) {
this.customerId = customerId;
}
#ManyToOne(targetEntity = CustomerEntity.class)
#NotFound(action = NotFoundAction.IGNORE)
#JoinColumn(name = "CustomerID", //in this table
referencedColumnName = "CustomerID", //from CustomerEntity
insertable = false, updatable = false,
foreignKey = #javax.persistence.ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
public CustomerEntity getCustomer() {
return this.customer;
}
public void setCustomer(CustomerEntity customer) {
this.customer = customer;
}
Anyway, the end goal is to get the Customer data from the Customer table as part of the Labor entity so it can be accessed directly with something like getCustomerEntity(). I supposed I would have to accomplish it by querying first using a JOIN like so:
TypedQuery<LaborEntity> query = entityManager.createQuery(
"SELECT l FROM LaborEntity l " +
"INNER JOIN CustomerEntity c " +
"ON l.customerId = c.customerId " +
"WHERE l.laborDate = '" + date + "'", LaborEntity.class);
List<LaborEntity> resultList = query.getResultList();
And then I can simply access the Customer that's associated like so:
resultList.get(0).getCustomer().getCustomerName();
Am I dreaming or is this actually possible?
Yes, this is completely possible.
(I'm not sure about what was the question though - but assuming you only want get it working)
You query needs to be a JPQL, not an SQL. And the Join is different on JPQL:
"SELECT l FROM LaborEntity l " +
"JOIN l.customer c " +
"WHERE ... "
The join starts from the root entity and then you use the field name (not column).
You can also use JOIN FETCH, then the associated entity (customer) will be loaded in the same query. (that is, Fetch EAGER)
Other recommendations:
Don't concat the parameters like that date. Instead use setParameter.
You don't need those #Basic
You don't need that targetEntity = CustomerEntity.class. It'll be detected automatically.
I am working on a spring boot project and using JPA for querying the database with Entity manager.
i.e.
Query query = entityManager.createNativeQuery("SELECT * FROM TABLE_NAME WHERE ID = 1"); // SUPPOSE
List<Object[]> result = (List<Object[]>) query.getResultList();
now after this what I want to achieve here is creating an Object of that corresponding result.
i.e. ResultObject obj = (ResultObject) result.get(0);
// here ResultObject is user defined.
but this type of casting is not possible directly.
so what I am currently doing is:
ResultObject obj = new ResultObject();
obj.setArribute1((String) obj[0]);
obj.setArribute2((Integer) obj[1]);
...
and on average i will be having 15 attributes per object. so its really tiresome...
I have tried using:
List<ResultObject> obj = (List<ResultObject>)query.getResultList();
but doesn't work.
Either use ConstructorResult (JPA) or ResultTransformer (Hibernate) or QLRM.
ConstructorResult is JPA standard and you have to create a Annotation with the column mapping:
#SqlResultSetMapping(
name = "BookValueMapping",
classes = #ConstructorResult(
targetClass = BookValue.class,
columns = {
#ColumnResult(name = "id", type = Long.class),
#ColumnResult(name = "title"),
#ColumnResult(name = "version", type = Long.class),
#ColumnResult(name = "authorName")}))
From https://thorben-janssen.com/result-set-mapping-constructor-result-mappings/
And ResultTransformer is Hibernate proprietary and you must use the Hibernate session:
List<PersonSummaryDTO> dtos = session.createNativeQuery(
"SELECT p.id as \"id\", p.name as \"name\" " +
"FROM Person p")
.setResultTransformer( Transformers.aliasToBean( PersonSummaryDTO.class ) )
.list();
From https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#sql-dto-query
Or QLRM is a library that maps the result to a DTO using the constructor:
JpaResultMapper jpaResultMapper = new JpaResultMapper();
Query q = em.createNativeQuery("SELECT ID, NAME FROM EMPLOYEE");
List<EmployeeTO> list = jpaResultMapper.list(q, EmployeeTO.class);
https://github.com/72services/qlrm
if you have set up a DatabaseConfig like this tutorial then you can simply create a class that you annotate with #Entity and #Table(name = "yourDatabaseTableName") Don't forget to define:
#Id
#Column(name = "ID")
private Long id;
and annotate all your colums with #Column(name = "databaseColumnName")
Then, create an interface that you annotate with #Repository which extends JpaRepository<YourEntityClass, Long>where the Long-parameter is the type you've given to the id-variable of your Entity.
Now you can use simple JPA-methodqueries like findAll() or you can create your own JPQL-queries like:
#Query("SELECT e FROM Entity e "
+ "WHERE e.id = :id")
Optional<Entity> findById(#Param("id") Long id);
It's even possible to use NativeQueries in this way:
#Query(value = "SELECT e FROM Entity e "
+ "WHERE e.id = :id",
nativeQuery = true)
Optional<Entity> findById(#Param("id") Long id);
Id suggest creating a POJO that can be mapped to your table you're retrieving values from:
#Entity
#Table(name = "MyTable")
#NamedQueries({
#NamedQuery(name = "MyTable.findAll", query = "SELECT m FROM MyTable m")})
public class MyTable implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "id")
private Integer id;
#Basic(optional = false)
#Column(name = "name")
private String name;
#Basic(optional = false)
#Column(name = "display_name")
private String displayName;
public MyTable() {
}
public MyTable(Integer id) {
this.id = id;
}
public MyTable(Integer id, String name, String displayName) {
this.id = id;
this.name = name;
this.displayName = displayName;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
#Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof MyTable)) {
return false;
}
MyTable other = (MyTable ) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
#Override
public String toString() {
return "MyTable[ id=" + id + " ]";
}
}
Obviously fill in the fields as you need with the corresponding database datatype repersentation.
Notice how i have NamedQueries we can now take advantage of those named queries to do our fetches
TypedQuery<MyTable> query = entityManager.createNamedQuery("MyTable.findAll", MyTable.class);
List<MyTable> results = query.getResultList();
this will do all the casting and conversions for you. You can add all the named queries you want.
https://www.objectdb.com/java/jpa/query/named
UPDATE
If you need to dynamically create a query you can do the following:
String query = "SELECT m FROM MyTable m Where m.id =:id and m.name=:name";
///modify the query as needed based off of other conditions)
TypedQuery<MyTable > query = em.createQuery(query, MyTable .class);
query.setParameter("id", id);
query.setParameter("name", name);
List<MyTable> results = query.getResultList();
https://www.objectdb.com/java/jpa/query/api
I'm trying to lazily load the ingredients of a product in a self relationship. A product can have zero or more ingredients. The relationship is stored in the ProductComposition entity.
These are my entities:
Product
#Entity(name = Product.TABLE_NAME)
//#NamedEntityGraph(name = "graph.Product.ingredients", attributeNodes = //#NamedAttributeNode("ingredients"))
public class Product {
public static final String TABLE_NAME = "Product";
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long idProduct;
private String name;
#OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE}, mappedBy = "product")
private List<OrderDetail> orders;
#OneToMany(fetch = FetchType.LAZY,cascade = CascadeType.ALL, mappedBy = "ingredient", orphanRemoval=true)
private List<ProductComposition> ingredients;
ProductComposition
#Entity(name = ProductComposition.TABLE_NAME)
#IdClass(ProductCompositionId.class)
public class ProductComposition {
public static final String TABLE_NAME = "ProductComposition";
#Id
#ManyToOne //(fetch = FetchType.LAZY)
#PrimaryKeyJoinColumn(name = "PrincipalProductID")
private Product principalProduct;
#Id
#ManyToOne //(fetch = FetchType.LAZY)
#PrimaryKeyJoinColumn(name = "IngredientID")
private Product ingredient;
private int quantity;
ProductCompositionId
class ProductCompositionId implements Serializable{
private long principalProduct;
private long ingredient;
In the method get of my Dao I've tried different things:
Fetching with a CriteriaQuery the ingredients and then set them to the product
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<ProductComposition> q = cb.createQuery(ProductComposition.class);
Root<ProductComposition> product = q.from(ProductComposition.class);
product.fetch("principalProduct", JoinType.LEFT);
q.select(product).where(cb.equal(product.get("principalProduct"), id));
List<ProductComposition> ingredients = entityManager.createQuery(q).getResultList();
Product p = entityManager.find(Product.class, id);
p.setIngredients(ingredients);
Using an Entity Graph
EntityGraph<Product> graph = (EntityGraph<Product>) entityManager.getEntityGraph("graph.Product.ingredients");
Map<String, Object> ingredients = new HashMap<>();
ingredients.put("javax.persistence.fetchgraph", graph);
Product p = entityManager.find(entityClass, id, ingredients);
Calling the method initialize
p = productDao.get(p.getIdProduct()); //the get here just calls entityManager.find(Product.class, id)
Hibernate.initialize(p.getIngredients());
System.out.println("Ingredients size: "+p.getIngredients().size()); //gives 0
After calling those two above lines I get the following two logs, but p still has no ingredients after:
Hibernate: select product0_.idProduct as idProduc1_4_0_, product0_.name as name2_4_0_, orders1_.product_idProduct as product_3_3_1_, orders1_.foodOrder_idFoodOrder as foodOrde2_3_1_, orders1_.foodOrder_idFoodOrder as foodOrde2_3_2_, orders1_.product_idProduct as product_3_3_2_, orders1_.quantity as quantity1_3_2_, foodorder2_.idFoodOrder as idFoodOr1_2_3_, foodorder2_.CustomerID as Customer2_2_3_, foodorder2_.DeliverymanID as Delivery3_2_3_, foodorder2_.RestaurantID as Restaura4_2_3_ from Product product0_ left outer join OrderDetail orders1_ on product0_.idProduct=orders1_.product_idProduct left outer join FoodOrder foodorder2_ on orders1_.foodOrder_idFoodOrder=foodorder2_.idFoodOrder where product0_.idProduct=?
Hibernate: select ingredient0_.ingredient_idProduct as ingredie2_5_0_, ingredient0_.principalProduct_idProduct as principa3_5_0_, ingredient0_.ingredient_idProduct as ingredie2_5_1_, ingredient0_.principalProduct_idProduct as principa3_5_1_, ingredient0_.quantity as quantity1_5_1_, product1_.idProduct as idProduc1_4_2_, product1_.name as name2_4_2_ from ProductComposition ingredient0_ inner join Product product1_ on ingredient0_.principalProduct_idProduct=product1_.idProduct where ingredient0_.ingredient_idProduct=?
However, all tries can't load the ingredients. They just return an empty list.
What am I doing wrong in this methods?
I would prefer to keep the relationship as lazy. Also because otherwise hibernate will return cannot simultaneously fetch multiple bags referring to Product.orders and Product.ingredients
This is my Entity configuration
#Entity
#NamedQuery(name = "Payment.findByEmail", query = "SELECT p FROM Payment p JOIN p.additionalAuthData a " +
"WHERE KEY(a) = 'email' AND VALUE(a) = ?1 AND (p.paymentType = 4 OR p.paymentType = 10)")
public class Payment {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Column(name = "payment_type")
private Integer paymentType;
/** other properties, getters and setters */
#ElementCollection
#CollectionTable(name = "additional_auth_data")
#MapKeyJoinColumn(name = "id", referencedColumnName = "id")
#MapKeyColumn(name = "field")
#Column(name = "data_value")
private Map<String, String> additionalAuthData;
}
The NamedQuery findByEmail("test#example.com") generates the following SQL
select -- all fields ...
from payment payment0_ inner join additional_auth_data additional1_ on payment0_.id=additional1_.id
where
additional1_.field='email' and (select additional1_.data_value from additional_auth_data additional1_ where payment0_.id=additional1_.id)='test#example.com' and (payment0_.payment_type=4 or payment0_.payment_type=10)
which is wrong: it may work if you have only one row but it blows up otherwise. H2 complains Scalar subquery contains more than one row and PostgreSQL more than one row returned by a subquery used as an expression. In fact, query's where condition compares a scalar value ('test#example.com') with a subquery.
The correct SQL should be:
select -- all fields
from payment payment0_ inner join additional_auth_data additional1_ on payment0_.id=additional1_.id
where additional1_.field='payerEmail' and additional1_.data_value='test#example.com' and (payment0_.payment_type=4 or payment0_.payment_type=10)
Is the HSQL correct? Is there a way to instruct Hibernate to generates a clever, better SQL? Is this a Hibernate bug?
Note: Hibernate shipped with Spring Boot Starter 1.3.7.RELEASE
Edit:
Using an #Embeddable class
#ElementCollection
#JoinTable(name = "additional_auth_data", joinColumns = #JoinColumn(name = "id"))
#MapKeyColumn(name = "field")
#Column(name = "data_value")
private Set<AdditionalData> additionalAuthData;
#Embeddable
public static class AdditionalData {
#Column(name = "field", nullable = false)
private String field;
#Column(name = "data_value")
private String dataValue;
protected AdditionalData() {
}
public AdditionalData(String field, String dataValue) {
this.field = field;
this.dataValue = dataValue;
}
/** Getters, setters; equals and hashCode on "field" */
}
#NamedQuery(name = "Payment.findByEmail", query = "SELECT p FROM Payment p JOIN p.additionalAuthData a " +
"WHERE a.field = 'email' AND a.dataValue = ?1 AND (p.paymentType = 4 OR p.paymentType = 10)")
solves the problem, and the SQL is correct, but it looks just plain wrong, like shooting a fly with a bazooka...
It generates correct SQL without value().
Use just a=?1
But I would expect is should generate it simple also with it.