Specifying join condition "ON columns" in hibernate HQL? - java

I am learning Hibernate, and I have a question about basic HQL join syntax. I am following this tutorial. Say I have a Product and Category entity,
#Entity
#Table(name = "CATEGORY")
public class Category {
private long id;
private String name;
private Set<Product> products;
public Category() {
}
public Category(String name) {
this.name = name;
}
#Id
#Column(name = "CATEGORY_ID")
#GeneratedValue
public long getId() {
return id;
}
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
public Set<Product> getProducts() {
return products;
}
// other getters and setters
}
#Entity
#Table(name = "PRODUCT")
public class Product {
private long id;
private String name;
private String description;
private float price;
private Category category;
public Product() {
}
public Product(String name, String description, float price,
Category category) {
this.name = name;
this.description = description;
this.price = price;
this.category = category;
}
#Id
#Column(name = "PRODUCT_ID")
#GeneratedValue
public long getId() {
return id;
}
#ManyToOne
#JoinColumn(name = "CATEGORY_ID")
public Category getCategory() {
return category;
}
// other getters and setters
}
So I have to join category and Product, I will something like this in sql
select * from Category A inner join Product B on A.id=B.category_id,
In HQL, it seems we drop the "on" condition, the HQL for the above query is
String hql = "from Product p inner join p.category";
Query query = session.createQuery(hql);
Why is on not required in HQL?

If you have an association (for an example #ManyToOne), you don't need on, Hibernate will add it to the SQL.
It was a problem prior to Hibernate 5.1, If you don't have an association. From Hibernate 5.1 you can use ad hoc joins:
How to join unrelated entities with JPA and Hibernate
Apart that, HQL also defines a with clause to qualify the join conditions:
Hibernate docs: Explicit joins

Related

Hibernate n+1 issue with 3 entities

I have 3 entities. Category, Subcategory and Product
#Entity
#JsonInclude(value = JsonInclude.Include.NON_EMPTY)
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String title;
private String picture;
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
#JsonIgnore
private List<Subcategory> subcategories;
//getters and setters...
#JsonProperty("productCount")
private int getProductCount() {
//Counting products
int productCount = 0;
//!!!!!
//My problem starts here!
for (final Subcategory subcategory : subcategories) {
productCount += subcategory.getProducts().size();
}
return productCount;
}
}
#Entity
#JsonInclude(JsonInclude.Include.NON_EMPTY)
public class Subcategory {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String title;
#ManyToOne
#JoinColumn(name = "parent_category_id", nullable = false)
#JsonIgnore
private Category category;
#OneToMany(mappedBy = "subcategory", cascade = CascadeType.ALL)
private List<Product> products;
//getters and setters
}
#Entity
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String title;
private String unit;
private String icon;
#ManyToOne
#JoinColumn(name = "subcategory_id", nullable = false)
#JsonIgnore
private Subcategory subcategory;
//getters and setters
}
They are related to each other, but when I want to count the number of products in each category, about 120 queries are executed (depending on the number of subcategories and products)
I was able to reduce it to around 60 queries by adding #EntityGraph to my category repository:
#EntityGraph(type = EntityGraph.EntityGraphType.FETCH, attributePaths = {"subcategories"})
However 60 query is still too much. I can't add subcategories.products to this entity graph annotation because that causes org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
I can suppress this exception by changing the data type of products to Set from List but that creates a Cartesian product and makes the performance worse (it returns around 18,000 rows).
How can I fix this issue without creating a Cartesian product?

Spring Data JPA fetches all columns when mapped model is included

Interface based projection - I want to select only few columns from Product and ProductImages DB tables.
Issue: If I mention mapped model in interface public Set<ProductImagesDBResult> getProductImages(); it fetches all columns.
from both Product and ProductImages tables.
Note: If I comment this out, it fetches only selected fields.
Entity models Product.java
#Entity
#Table(name = "products")
#EntityListeners(AuditingEntityListener.class)
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#OneToMany(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
#JoinColumn(name = "product_id")
private Set<ProductImage> productImages;
#ManyToOne
#JoinColumn(name = "category_id")
private Category category;
private String sku;
private String slug;
private String title;
private String description;
private BigDecimal oldPrice;
private BigDecimal newPrice;
private int status;
/* getters and setters */
}
Entity model ProductImage.java
#Entity
#Table(name = "product_images")
public class ProductImage {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#JoinColumn(name = "product_id")
#ManyToOne
#JsonIgnore
private Product product;
private String image;
private int isDefault;
private int imgOrder;
}
ProductDetailsDBResult.java (Projection interface)
public interface ProductDetailsDBResult {
public long getId();
public String getSlug();
public String getTitle();
public Set<ProductImagesDBResult> getProductImages();
interface ProductImagesDBResult {
public long getId();
public String getImage();
}
/* getters and setters */
}
ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
Optional<ProductDetailsDBResult> findBySlug(String slug);
}
ProductService.java
ProductDetailsDBResult p = productRepository.findBySlug(slug)
.orElseThrow(() -> new RecordNotFoundException("Invalid product slug " + slug));
The issue is:
When this line is commented out:
public interface ProductDetailsDBResult {
/* other get methods ... */
// public Set<ProductImagesDBResult> getProductImages();
}
Logs show (correct SQL as only mentioned columns are fetched):
Hibernate: select product0_.id as col_0_0_, product0_.slug as col_1_0_, product0_.title as col_2_0_ from products product0_ where product0_.slug=?
But when this line is uncommented: public Set<ProductImagesDBResult> getProductImages();
SQL fetches all fields from both Product, Category and ProductImages tables: (Product is mapped with Category and ProductImages models)
Hibernate: select product0_.id as id1_5_, product0_.category_id as categor11_5_, product0_.created_at as created_2_5_, product0_.description as descript3_5_, product0_.new_price as new_pric4_5_, product0_.old_price as old_pric5_5_, product0_.sku as sku6_5_, product0_.slug as slug7_5_, product0_.status as status8_5_, product0_.title as title9_5_, product0_.updated_at as updated10_5_ from products product0_ where product0_.slug=?
Hibernate: select category0_.id as id1_0_0_, category0_.description as descript2_0_0_, category0_.is_featured as is_featu3_0_0_, category0_.preview_image as preview_4_0_0_, category0_.slug as slug5_0_0_, category0_.title as title6_0_0_ from categories category0_ where category0_.id=?
Hibernate: select productima0_.product_id as product_5_4_0_, productima0_.id as id1_4_0_, productima0_.id as id1_4_1_, productima0_.image as image2_4_1_, productima0_.img_order as img_orde3_4_1_, productima0_.is_default as is_defau4_4_1_, productima0_.product_id as product_5_4_1_ from product_images productima0_ where productima0_.product_id=?
How can I fetch only the mentioned columns from both tables?
Any help would be appreciated. Thanks in advance! :)

Hibernate generates extra left join with many to one association

I have a simple Hibernate #ManyToOne mapping between two entities, using an association table thanks to annotation #JoinTable.
Here is my mapping:
#Entity
public class Customer {
private Long id;
private Address address;
#Id
#GeneratedValue
public Long getId() {
return id;
}
#ManyToOne
#JoinTable(
name = "customer_address_association",
joinColumns = #JoinColumn(name = "customer_id"),
inverseJoinColumns = #JoinColumn(name = "address_id")
)
public Address getAddress() {
return address;
}
public void setId(Long id) {
this.id = id;
}
public void setAddress(Address address) {
this.address = address;
}
}
and
#Entity
public class Address {
private Long id;
private String street;
#Id
#GeneratedValue
public Long getId() {
return id;
}
public String getStreet() {
return street;
}
public void setId(Long id) {
this.id = id;
}
public void setStreet(String street) {
this.street = street;
}
}
When I query on Customer entity, I always get an extra left join to the join table. For example, a HQL query such as SELECT c.id from Customer c generates the following SQL query: select customer0_.id as col_0_0_ from customer customer0_ left outer join customer_address_association customer0_1_ on customer0_.id=customer0_1_.customer_id.
Full source code to reproduce is available here: https://github.com/ndionisi/hibernate-extra-left-join
I reproduce it with Hibernate 5.1, 5.2 and 5.3. Is there any way to prevent Hibernate from generating the left join to customer_address_association table? It impacts the performance on large tables.

Spring Data Join with Specifications

I'm trying to convert this raw sql query:
select product.* from following_relationship
join product on following_relationship.following=product.owner_id
where following_relationship.owner=input
Into Spring Data specifications, i think that my issue so far is on joining those tables.
Here is my current conversion in Specification:
protected Specification<Product> test(final User user){
return new Specification<Product>() {
#Override
public Predicate toPredicate(Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Join<FollowingRelationship,Product> pfJoin = query.from(FollowingRelationship.class).join("following");
pfJoin.on(cb.equal(pfJoin.get("following"),"owner"));
return query.where(cb.equal(pfJoin.get("following"),user)).getGroupRestriction();
}
};
}
And I'm getting this exception :
Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessA
piUsageException: org.hibernate.hql.internal.ast.InvalidWithClauseException: with clause can only reference columns in the driving table
I will like to add that I'm new at Spring framework for instance this is my first application on spring, so my apologies for the newbie question ;)
Edit: added entities Product, FollowingRelationShip
Entity
#JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "json_id_prop")
public class FollowingRelationship extends BaseEntity {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "OWNER", referencedColumnName = "uuid")
private User owner;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "FOLLOWING", referencedColumnName = "uuid")
private User following;
public User getOwner() {
return owner;
}
public void setOwner(User owner) {
this.owner = owner;
}
public User getFollowing() {
return following;
}
public void setFollowing(User following) {
this.following = following;
}
}
#Entity
#Table(name = "product")
#JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "json_id_prop")
public class Product extends BaseEntity {
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "OWNER_ID", referencedColumnName = "uuid")
private User owner;
#NotNull
private String name;
#NotNull
private String description;
#NotNull
private String price;
#NotNull
private String brand;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getPrice() {
return price;
}
public void setPrice(String price) {
this.price = price;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public User getOwner() {
return owner;
}
public void setOwner(User owner) {
this.owner = owner;
}
}
Product and FollowingRelationShip entities do no have any explicit relationship, hence the join on my implementation about.What i want to achieve is to get all products from all users which another user follow in Spring data Specifications.
EDIT: Ok, I did quite a mess here, but I hope this time I'm closer to the right answer.
Consider (id's are auto-generated like 1 for John etc.):
INSERT INTO some_user (name) VALUES ('John');
INSERT INTO some_user (name) VALUES ('Ariel');
INSERT INTO some_user (name) VALUES ('Brian');
INSERT INTO some_user (name) VALUES ('Kelly');
INSERT INTO some_user (name) VALUES ('Tom');
INSERT INTO some_user (name) VALUES ('Sonya');
INSERT INTO product (owner_id,name) VALUES (1,'Nokia 3310');
INSERT INTO product (owner_id,name) VALUES (2,'Sony Xperia Aqua');
INSERT INTO product (owner_id,name) VALUES (3,'IPhone 4S');
INSERT INTO product (owner_id,name) VALUES (1,'Xiaomi MI5');
INSERT INTO product (owner_id,name) VALUES (3,'Samsung Galaxy S7');
INSERT INTO product (owner_id,name) VALUES (3,'Sony Xperia Z3');
INSERT INTO following_relationship (follower_id, owner_id) VALUES (4,1);
INSERT INTO following_relationship (follower_id, owner_id) VALUES (5,1);
INSERT INTO following_relationship (follower_id, owner_id) VALUES (4,2);
INSERT INTO following_relationship (follower_id, owner_id) VALUES (6,2);
INSERT INTO following_relationship (follower_id, owner_id) VALUES (6,3);
INSERT INTO following_relationship (follower_id, owner_id) VALUES (1,3);
Based on simplified version of entities that You provided, and SomeUser Entity like:
#Entity
public class FollowingRelationship {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#ManyToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn(name = "owner_id")
SomeUser owner;
#ManyToOne(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn(name = "follower_id")
SomeUser follower;
...
#Entity
public class Product {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#ManyToOne()
#JoinColumn(name = "owner_id")
private SomeUser owner;
#Column
private String name;
...
#Entity
public class SomeUser {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column
private String name;
#OneToMany(mappedBy = "owner")
private Set<Product> products = new HashSet<Product>();
#OneToMany(mappedBy = "owner")
private Set<FollowingRelationship> ownedRelationships = new HashSet<FollowingRelationship>();
#OneToMany(mappedBy = "follower")
private Set<FollowingRelationship> followedRelationships = new HashSet<FollowingRelationship>();
I have created Specification like:
public static Specification<Product> joinTest(SomeUser input) {
return new Specification<Product>() {
public Predicate toPredicate(Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Join<Product,SomeUser> userProd = root.join("owner");
Join<FollowingRelationship,Product> prodRelation = userProd.join("ownedRelationships");
return cb.equal(prodRelation.get("follower"), input);
}
};
}
And now, we can execute the query with:
SomeUser someUser = someUserRepository.findOne(Specification.where(ProductSpecifications.userHasName("Kelly")));
List<Product> thatProducts = productRepository.findAll(Specification.where(ProductSpecifications.joinTest(someUser)));
System.out.println(thatProducts.toString());
We get:
[Product [id=1, name=Nokia 3310], Product [id=4, name=Xiaomi MI5], Product [id=2, name=Sony Xperia Aqua]]
And this in My opinion is equivalent of: "get all products from all users which another user follow" - get products of all users that Kelly is following.

Join fetching #ManyToOne nested inside #OneToMany

I have created the following entities to manage a persistent shopping cart:
ShoppingCart.java:
#Entity
public class ShoppingCart {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#PrivateOwned
#OneToMany(mappedBy = "cart", cascade = CascadeType.ALL)
#OrderBy("creationTimestamp")
private List<ShoppingCartItem> items;
public ShoppingCart() {}
// Getters and setters...
}
ShoppingCartItem.java:
#Entity
#IdClass(ShoppingCartItemId.class)
public class ShoppingCartItem {
#Id
#ManyToOne
private Item item;
#Id
#ManyToOne
private ShoppingCart cart;
private int quantity;
#Column(precision = 17, scale = 2)
private BigDecimal price;
#Temporal(TemporalType.TIMESTAMP)
private Date creationTimestamp;
protected ShoppingCartItem() {}
#PrePersist
protected void prePersist() {
creationTimestamp = new Date();
}
public ShoppingCartItem(ShoppingCart cart, Item item, int quantity) {
this.cart = cart;
this.item = item;
this.quantity = quantity;
this.price = item.getPrice();
}
// Getters and setters...
}
Item.java:
#Entity
public class Item {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
private Brand brand;
private String model;
private String variant;
private String description;
#Column(precision = 17, scale = 2)
private BigDecimal price;
private int availability;
protected Item() {}
// Constructors, getters and setters...
}
When I issue the the following JPQL query:
SELECT c FROM ShoppingCart c JOIN FETCH c.items WHERE c.id = :id
I notice that all the ShoppingCartItems in the same ShoppingCart are retrieved as expected in a single query but the #ManyToOne private Item item; field is not in the join and a separate query for each ShoppingCartItem is issued to fetch that field when accessed.
Using EclipseLink, is there a way to have also the Items join fetched when join/batch fetching the ShoppingCartItems? How do I change the query and/or code?
If you are using EclipseLink you can take a look at the #BatchFetch and #JoinFetch annotations.
While the left join fetchs with aliases seems to be ignored, I've found this query hint that do the job:
Query query = entityManager.createQuery("SELECT c FROM ShoppingCart c WHERE c.id = :id");
query.setHint("eclipselink.left-join-fetch", "c.items.item.brand");
This is probably better than the annotation approach as it can be specified per single query.
UPDATE
Use of this hint broke #OrderBy("creationTimestamp") so ShoppingCartItems aren't returned in the order they were inserted anymore. This is probably due to a bug in EclipseLink but I think it doesn't really hurt so much since I actually need to have the items ordered only when showing the cart to the user and not, for example, when the user logs in and items in the anonymous cart must be transferred to the user cart.

Categories