Hibernate is generating invalid SQL for a particular criteria query. I can manually fix the query by adding single quotes to the value being used in the WHERE clause.
To fix it, I changed the query from:
where (role0_.ROLE_ID=2L )
to:
where (role0_.ROLE_ID=`2L` )
How to force hibernate to add single quotes (in mysql it is single quotes but in other database systems it might be something else) to enclose the values used in generated SQL queries?
The full generated query is:
select permission1_.PERMISSION_ID as PERMISSION1_12_,
permission1_.IS_REQUIRED as IS2_12_,
permission1_.SOURCE_ROLE_ID as SOURCE3_12_,
permission1_.TARGET_ROLE_ID as TARGET4_12_
from (
select ROLE_ID,
NAME,
DESCRIPTION,
IS_ACTION,
LABEL,
null as FIRST_NAME,
null as LAST_NAME,
null as PASSWORD_HASH,
1 as clazz_ from GROUPS
union
select ROLE_ID,
NAME,
null as DESCRIPTION,
null as IS_ACTION,
null as LABEL,
FIRST_NAME,
LAST_NAME,
PASSWORD_HASH,
2 as clazz_ from USERS
)
role0_ inner join PERMISSIONS permission1_ on role0_.ROLE_ID=permission1_.SOURCE_ROLE_ID
where (role0_.ROLE_ID=2L )
Basically I'd like this single quotes to be added by Hibernate.
The criteria query that generated this query is:
EntityManager entityManager = getEntityManager();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
Class<?> queryScopeClass = temp.pack.commons.user.Role.class;
Root<?> from = criteriaQuery.from(queryScopeClass);
Path<?> idAttrPath = from.get("id");
// also tried criteriaBuilder.equal(idAttrPath, new Long(2))
Predicate predicate = criteriaBuilder.equal(idAttrPath, criteriaBuilder.literal(new Long(2)))
criteriaQuery.where(predicate);
Path<?> attributePath = from.get("permissions");
PluralAttributePath<?> pluralAttrPath = (PluralAttributePath<?>)attributePath;
PluralAttribute<?, ?, ?> pluralAttr = pluralAttrPath.getAttribute();
Join<?, ?> join = from.join((SetAttribute<Object,?>)pluralAttr);
TypedQuery<Object> typedQuery = entityManager.createQuery(criteriaQuery.select(join));
return (List<P>)typedQuery.getResultList();
Please let me know if you have any clues on how to force Hibernate to add those single quotes to the values (not the column/table name).
In my entity Role, the id property that appears in the WHERE clause is of long type, of course.
Follow up: The type of the id column in the database is bingint:
+---------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+-------+
| ROLE_ID | bigint(20) | NO | PRI | NULL | |
...
This is how the Role class has been annotated:
#Entity(name="Role")
#Table(name = "ROLES")
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
#javax.persistence.TableGenerator(
name="GENERATED_IDS",
table="GENERATED_IDS",
valueColumnName = "ID"
)
public abstract class Role implements Serializable {
private static final long serialVersionUID = 1L;
/**
* The id of this role. Internal use only.
*
* #since 1.0
*/
#Id #GeneratedValue(strategy = GenerationType.TABLE, generator="GENERATED_IDS")
#Column(name = "ROLE_ID")
protected long id;
/**
* Set of permissions granted to this role.
*
* #since 1.0
*/
#OneToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }, mappedBy="sourceRole")
protected Set<Permission> permissions = new HashSet<Permission>();
...
}
I use table per class inheritance strategy, that's why you see the union in the generated query for User and Group entities. They extend Role. Id is defined in Role.
Thank you!
Eduardo
The hibernate property hibernate.globally_quoted_identifiers=true will do the trick
Change your id to the Long class type instead of a primitive. Hibernate will then simply generate the query to be ROLE_ID=2, which is 100% valid since numbers don't require ticks or quotes.
Related
I'm sending a very simple query to the database, but I'm getting an error. It feels like I'm missing something very simple. I guess it wouldn't allow me to create it because the word order is a keyword on the h2 db, so I put it in quotation marks within the table annotation.
#Query(value = "select * from `ORDER` o where o.basket_id= :basketId ", nativeQuery = true)
Optional<Order> getOrderByBasketId(Long basketId);
#Entity
#Getter
#Setter
#Table(name = "`ORDER`")
public class Order extends BaseExtendedModel{
private BigDecimal price;
#Enumerated(EnumType.STRING)
private OrderStatus orderStatus;
#OneToOne
private Customer customer;
#OneToOne(cascade = CascadeType.MERGE)
private Basket basket;
#OneToOne(cascade = CascadeType.ALL, mappedBy = "order")
private OrderAddress orderAddress;
}
{
"errorMessage": "could not prepare statement; SQL [select * from `ORDER` o where o.basket_id= ? ]; nested exception is org.hibernate.exception.SQLGrammarException: could not prepare statement"
}
The problem is easier to identidy when you have a look at the logs. You'll see an entry like this:
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "ORDER" not found; SQL statement:
So let's see what SQL statements are executed. So we add the following to application.properties
spring.jpa.show-sql=true
Assuming you let spring boot create your tables, you will see the following:
Hibernate: drop table if exists "order" CASCADE
Hibernate: create table "order" ...
And when we hit the repository method we see
select * from `ORDER` o where o.name= ? [42102-200]
So why did it create the table in lowercase, even though we specified #Table(name = "`ORDER`")?
The default for spring.jpa.hibernate.naming.physical-strategy is org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy, which
replaces dots with underscores
changes CamelCase to snake_case
lower-cases table names.
But we want it to take the names we use in #Table. That works when setting the property to spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl.
Yor native query will need the matching casing though.
You need to use index parameters
#Query(value = "select * from `ORDER` o where o.basket_id= ?1", nativeQuery = true)
Optional<Order> getOrderByBasketId(Long basketId);
or named parameters
#Query(value = "select * from `ORDER` o where o.basket_id= :basketId", nativeQuery = true)
Optional<Order> getOrderByBasketId(#Param("basketId") Long basketId);
I'm mapping sql view to entity class with hibernate #Subselect annotation.
Basically, it looks somewhat like this:
#Subselect(
"SELECT table1.*, table2.id as tid FROM "
+ "table1 INNER JOIN table2 on table2.field = table1.field"
)
#Entity
#Immutable
class Entity {
// fields
}
When the join works i may get something like the following
========================================
| table1.id | table1.field | table2.id |
========================================
| 1 | 1 | 1 |
========================================
| 1 | 1 | 2 |
========================================
So several records in table2 can join to one row in table1. This is fine, however in java Entity I want to map it as one to many relationship(one entity to many table2 enities), here's what I wrote, which worked for others kind of relationships:
#Subselect(
"SELECT table1.*, table2.id as tid FROM "
+ "table1 INNER JOIN table2 on table2.field = table1.field"
)
#Entity
#Immutable
class Entity {
#OneToMany
#JoinColumn(name = "tid", updatable = false, insertable = false)
private Set<Table2Entity> elements = new HashSet<>();
}
However, the set in the entity is always empty, why is that ?
The above approach works for one to one and many to one relationships.
Turns, it's not required to do join with sql, to bring one to many relationship for the view entity. I solved it like this:
Subselect(
"SELECT * from table1"
)
#Entity
#Immutable
class Entity {
#OneToMany
#JoinColumn(name = "field", updatable = false, insertable = false)
private Set<Table2Entity> elements = new HashSet<>();
}
I only needed to put column name, on which tables needed to be joined.
I have two tables - user and booking. Each user may have many bookings (one-to-many relationship).
user: booking:
id | name | id | country | user_id | price |
-------------| ------------------------------------|
1 | Alice | 1 | Italy | 1 | 2000 |
2 | Bob | 2 | France | 1 | 2500 |
3 | Spain | 1 | 3000 |
I want to select all users and all bookings where booking's price is greater than 2000 using Query DSL. If a user doesn't have any bookings or bookings don't match the condition I still want to select this user.
First, let's have a look at how it would look like using a simple SQL left join query:
SELECT u.*, b.* FROM user u LEFT JOIN booking b ON u.id = b.user_id AND b.price > 2000
The above query should provide the following result:
id | name | id | country | user_id | price |
-------------|----------------------------------------|
1 | Alice | 2 | France | 1 | 2500 |
1 | Alice | 3 | Spain | 1 | 3000 |
2 | Bob | null | null | null | null |
Now I want to do it using JPA with Query DSL
JPA-related stuff:
#Entity
public class User {
#Id
private Long id;
private String name;
#OneToMany(cascade = ALL, fetch = EAGER, orphanRemoval = true, mappedBy = "user")
private List<Booking> bookings;
// getters and setters
}
#Entity
public class Booking {
#Id
private Long id;
private String name;
private Integer price;
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "user_id")
private User user;
// getters and setters
}
Query DSL:
public List<User> getUsersAndBookings() {
QUser user = QUser.user;
QBooking booking = QBooking.booking;
JPAQuery<User> jpaQuery = new JPAQuery(entityManager);
List<User> result = jpaQuery.from(user).leftJoin(user.bookings, booking).on(booking.price.gt(2000)).fetchJoin().fetch();
return result;
}
In fact, this code is not working and I get the following exception:
org.hibernate.hql.internal.ast.QuerySyntaxException: with-clause not allowed on fetched associations; use filters [select user from com.example.demo.entity.User user left join fetch user.bookings as booking with booking.price > ?1]
The problem is that the condition clause is specified in on method - on(booking.price.gt(2000)).
After some research I found that this condition should be specified in where method and should look like this:
List<User> result = jpaQuery.from(user).leftJoin(user.bookings, booking).where(booking.price.gt(2000)).fetchJoin().fetch();
This works, but not how I would expect it to work, since it doesn't return ALL users, it returns only one user (Alice), which has some bookings, matching the condition clause. Basically, it just filters the merged table (result table after left join operation) and that's not what I'm looking for.
I want to retrieve all users, and if there are no any bookings for a specific user, then just have null instead of booking list for this user.
Please help, been struggling for hours without any success.
Versions used:
Spring Boot 2.0.2
Spring Data JPA 2.0.7
Hibernate 5.2.16.Final
QueryDSL 4.1.4
You can use isNull expression in where clause to get the rows that have null values.
Your query should be like this:
jpaQuery.from(user)
.leftJoin(user.bookings, booking)
.fetchJoin()
.where(booking.price.gt(2000).or(booking.id.isNull())).fetch();
Hibernate produced query:
select
user0_.id as id1_1_0_,
bookings1_.id as id1_0_1_,
user0_.name as name2_1_0_,
bookings1_.country as country2_0_1_,
bookings1_.price as price3_0_1_,
bookings1_.user_id as user_id4_0_1_,
bookings1_.user_id as user_id4_0_0__,
bookings1_.id as id1_0_0__
from
user user0_
left outer join
booking bookings1_
on user0_.id=bookings1_.user_id
where
bookings1_.id is null
or bookings1_.price>?
It seems there is no JPA way for this. But I got it fixed in Hibernate way, using Filters org.hibernate.annotations.Filter.
#Entity
#FilterDef(name = "anyName", parameters = {
#ParamDef(name = "price", type = "integer")
})
public class User {
#Id
private Long id;
private String name;
#OneToMany(cascade = ALL, fetch = EAGER, orphanRemoval = true, mappedBy = "user")
#Filter(name = "anyName", condition = "price > :inputPrice")
private List<Booking> bookings;
}
Before querying the db, you must enable this filter.
Session session = enityManager.unwrap(Session.class);
session.enableFilter("anyName").setParameter("inputPrice", 2000);
// fetch using hql or criteria; but don't use booking.price.gt(2000) or similar condition there
session.disableFilter("anyName");
Now the result will have a User even if all of his booking prices are below 2000 and bookings list will be empty as expected.
NOTE: The word price in condition should be exactly same as the db column name; not as the model property name.
I have a MySQL table:
mysql> show create table items\G
*************************** 1. row ***************************
Table: items
Create Table: CREATE TABLE `items` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.02 sec)
I create new rows from a Java program via an Entity class:
#Entity
#Table(name = "items", schema = "office_db")
#XmlRootElement
#NamedQueries({
#NamedQuery(name = "Items.findAll", query = "SELECT i FROM Items i"),
#NamedQuery(name = "Items.findById", query = "SELECT i FROM Items i WHERE i.id = :id"),
#NamedQuery(name = "Items.findByName", query = "SELECT i FROM Items i WHERE i.name = :name"),
#NamedQuery(name = "Items.findByCreated", query = "SELECT i FROM Items i WHERE i.created = :created")
})
public class Items implements Serializable {
#Column(name = "name",length = 128)
private String name;
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 = "created")
#Temporal(TemporalType.TIMESTAMP)
private Date created;
#OneToMany(mappedBy = "itemId")
private Collection<Documents> documentsCollection;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "items")
private Collection<ItemAttributes> itemAttributesCollection;
(more stuff ...)
I only set the NAME column, and as expected, the ID and CREATED are set by default:
mysql> select * from items;
+----+--------+---------------------+
| id | name | created |
+----+--------+---------------------+
| 2 | Case 2 | 2017-10-31 13:47:52 |
| 3 | Case 3 | 2017-10-31 13:48:02 |
+----+--------+---------------------+
2 rows in set (0.00 sec)
However, when I reload the table into Java later in the same session:
public List<Items>findItems(){
TypedQuery<Items> query=
em.createNamedQuery("Items.findAll",Items.class);
return query.getResultList();
}
the ID column is loaded correctly, but the CREATED column comes up as blank. CREATED shows up correctly if I relaunch the application (this runs on a glassfish server). My guess is that the reason for this difference is the #GeneratedValue annotation on ID, but I can't apply it on the CREATED field, it seems, or at least not naively. What is the correct way to make the generated timestamp load?
The answer to my conundrum, it appears, is to call em.refresh(), according to this: Invalidating JPA EntityManager session - this is for Hibernate, but it seems to be the same for EclipseLink. I run in to an exception, in my case, but I will post that in another question.
I have a problem with hibernate native sql join query. My query is below and works on Mysql db.
SELECT c.cart_id, u.name, u.surname, c.totalPrice
FROM sandbox.cart c JOIN
sandbox.user u
ON u.id = c.placedBy
I am using hibernate in code and encountered an exception
java.sql.SQLException: Column 'id' not found.
com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1055)
com.mysql.jdbc.SQLError.createSQLException(SQLError.java:956)
com.mysql.jdbc.SQLError.createSQLException(SQLError.java:926)
com.mysql.jdbc.ResultSetImpl.findColumn(ResultSetImpl.java:1093)
Query in code here
Session session = hibernateUtil.getSessionFactory().getCurrentSession();
SQLQuery query = session.createSQLQuery(ORDER_PER_USER_QUERY);
query.addEntity(OrderPerUser.class);
return query.list();
Table column name
Cart
| cart_id | placedBy | totalPrice
User
| id | email | name | surname
My mapped class is
#Entity
public class OrderPerUser {
#Id
private long id;
private String name;
private String surName;
private long cartId;
private double totalPrice; }
You need to remove the line:
query.addEntity(OrderPerUser.class);
After that, you need to rewrite the code and map your object manually, because your OrderPerUser is not an entity:
Session session = hibernateUtil.getSessionFactory().getCurrentSession();
SQLQuery query = session.createSQLQuery(ORDER_PER_USER_QUERY);
List<OrderPerUser> returnList new ArrayList<>();
for(Object[] row : query.list()){
OrderPerUser orderPerUserObj = new OrderPerUser();
oderPerUserObj.setCartId(Long.parseLong(row[0].toString()));
//put other properties here
returnList.add(orderPerUserObj);
}
return returnList;
Edit1: Now I see that you added the mapped class, but OrderPerUser should not be an entity in your case, but a regular DTO. An entity requires an ID, but you can't select the ID in this case, because OrderPerUser is not part of a table, it is just some selected data that you want in your memory and not in the database. So you should make your OrderPerUser a regular data transfer object.
Please read about entities, data transfer objects, data access objects to see what each object should do.
My guess is that your OrderPerUser class which you try to use for collecting the result is expecting a column with name id, and you have no such column in your query...
Try using the query:
SELECT u.id, c.cart_id, u.name, u.surname, c.totalPrice
FROM sandbox.cart c
JOIN sandbox.user u ON u.id = c.placedBy