How to optimize Carstesian Product when loading Hibernate Entity - java

To demonstrate my problem, I created a simple Spring Boot application. It has one Entity, which has ID, two String properties and two Sets<String> sets.
package com.mk.cat.domain;
import javax.persistence.*;
import java.util.Set;
#Entity
#Table(name = "cat")
public class Cat {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
#Column(name = "sex")
private String sex;
#ElementCollection(fetch = FetchType.EAGER)
#Column(name = "color")
#CollectionTable(
name = "cat_color",
joinColumns = #JoinColumn(name = "cat_id"))
private Set<String> colors;
#ElementCollection(fetch = FetchType.EAGER)
#Column(name = "nickname")
#CollectionTable(
name = "cat_nickname",
joinColumns = #JoinColumn(name = "cat_id"))
private Set<String> nicknames;
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 String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Set<String> getColors() {
return colors;
}
public void setColors(Set<String> colors) {
this.colors = colors;
}
public Set<String> getNicknames() {
return nicknames;
}
public void setNicknames(Set<String> nicknames) {
this.nicknames = nicknames;
}
}
There is also a simple code, which persists and loads the Cat Entity from DB.
package com.mk.cat;
import com.google.common.collect.Sets;
import com.mk.cat.domain.Cat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class CatApplication implements CommandLineRunner {
private final CatRepository catRepository;
private static final Logger LOGGER = LoggerFactory.getLogger(CatApplication.class);
#Autowired
public CatApplication(CatRepository catRepository) {
this.catRepository = catRepository;
}
public static void main(String[] args) {
SpringApplication.run(CatApplication.class, args);
}
#Override
public void run(String... args) throws Exception {
Cat cat = new Cat();
cat.setName("Ben");
cat.setSex("Male");
cat.setNicknames(Sets.newHashSet("Fluffy", "Mr. Tomcat", "Catburger"));
cat.setColors(Sets.newHashSet("Black", "White"));
final Cat saved = catRepository.save(cat);
LOGGER.info("Cat saved={}", cat);
catRepository.findOne(saved.getId());
}
}
I traced Hibernate and I found, that the Cat is loaded from DB by this SQL.
select cat0_.id as id1_0_0_,
cat0_.name as name2_0_0_,
cat0_.sex as sex3_0_0_,
colors1_.cat_id as cat_id1_1_1_,
colors1_.color as color2_1_1_,
nicknames2_.cat_id as cat_id1_2_2_,
nicknames2_.nickname as nickname2_2_2_
from cat cat0_
left outer join cat_color colors1_ on cat0_.id=colors1_.cat_id
left outer join cat_nickname nicknames2_ on cat0_.id=nicknames2_.cat_id
where cat0_.id=1
The Hibernate then gets this Cartesian product from the rows of the cat table and two tables, that represent the Cat#colors and Cat#nicknames sets.
id1_0_0_ name2_0_0_ sex3_0_0_ cat_id1_1_1_ color2_1_1_ cat_id1_2_2_ nickname2_2_2_
1 Ben Male 1 Black 1 Fluffy
1 Ben Male 1 Black 1 Catburger
1 Ben Male 1 Black 1 Mr. Tomcat
1 Ben Male 1 White 1 Fluffy
1 Ben Male 1 White 1 Catburger
1 Ben Male 1 White 1 Mr. Tomcat
Hibernate then goes through each and every line, parses every single item of the ResultSet and creates the Entity. Is it somehow possible to optimize this approach? I would like to select the Cat#colors and Cat#nicknames sets by a subselect, due to serious performance problems. In the real case, I fetch 1500 Entities, that have complex structure and it is not uncommon, that one fetched Entity generates 25.000 rows in the corresponding ResultSet causing a very long parsing time.
The lazy loading in my case is not the option I would like to use, because it brings clutter to the code. As far as I know, the lazily loaded Collection must be initialized by first call and this is quite a big usability price to pay in my real application.
I would appreciate 3 separate selects, one from the cat table, one from the cat_color table and one from the cat_nickname table.

I found the solution for Hibernate, the #Fetch(FetchMode.SELECT) did the work, because it made Hibernate to select the nicknames by a separate select instead of join.
#Fetch(FetchMode.SELECT)
#ElementCollection(fetch = FetchType.EAGER)
#Column(name = "nickname")
#CollectionTable(
name = "cat_nickname",
joinColumns = #JoinColumn(name = "cat_id"))
private Set<String> nicknames;

Related

How to list column from another table via foreign key in Spring Boot

I'm struggling with this problem. I have table "Cities" which has foreign key to table "Countries" with country_id referenced to country from which is city. In my web application I can list all the data from "Cities" table but I can't find a way to list name of country. This is my service class method.
public List<City> listAll() {
List<City> cities = repo.findAll();
return cities;
}
In "City" entity I have field Country by which I can find in method name of country but I don't know how to return it together with cities.
Addition:
#GetMapping("/cities")
public String getAllCities(Model model) {
List<City> listCities = service.listAll();
model.addAttribute("showListCities", listCities);
return "cities";
}
City.java:
package com.bookflight.BookFlight.gradovi;
import com.bookflight.BookFlight.drzave.Drzave;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "cities")
public class City {
#Id
#Column(name = "city_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(nullable = false, length = 45, name = "city_name")
private String city_name;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "cou_id", referencedColumnName = "cou_id")
private Country countries;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getcity_name() {
return city_name;
}
public void setcity_name(String city_name) {
this.city_name = city_name;
}
public Countries getCountries() {
return countries;
}
public void setCountries(Country countries) {
this.countries = countries;
}
}
NOTE: Every variable name here is in my native language so I literally translated it word by word to better understand your solution afterwards.
You have add FetchType to the #ManyToOne annotation arguments:
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name = "cou_id", referencedColumnName = "cou_id")
private Country countries;
and her a short description for each fetch type:
FetchType.LAZY will only fire for primary table. If in your code you call any other method that has a parent table dependency then it will fire query to get that table information.
FetchType.EAGER will create join of all table including relevant parent tables directly.
And you can add a method in your city Class to return your country name and this method will be available in your view-layer:
public String getCountryName(){
return countries == null ? null : countries.getName();
//not sure how the country class is implemented
}

Spring JPA joint table doesn't return all fields

I created two tables -student and subject.
Then I created a third table student_subject, which is a joint table to connect students with subjects. It has 5 fileds: row id, student id, subject id and the created_at and updated_at fields (which comes from the AuditModel class):
package com.rest.web.postgresapi.data.models;
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
#Entity
#Table(uniqueConstraints = {
#UniqueConstraint(columnNames = {"student_id", "subject_id"})
})
public class StudentSubject extends AuditModel {
#Id
#GeneratedValue(generator = "enrollment_generator")
#SequenceGenerator(
name = "enrollment_generator",
sequenceName = "enrollment_sequence",
initialValue = 4420
)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "student_id", nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
#JsonIgnore
private Student student_id; // If I put private Long student_id, it fails
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "subject_id", nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
#JsonIgnore
private Subject subject_id; // If I put private Long subject_id, it fails
// Constructors
protected StudentSubject() {}
public StudentSubject(Student student_id, Subject subject_id) {
this.student_id = student_id;
this.subject_id = subject_id;
}
// Getters
public Long getId() {
return id;
}
public Student getStudent_id() {
return student_id;
}
public Subject getSubject_id() {
return subject_id;
}
// Setters
public void setId(Long id) {
this.id = id;
}
public void setStudent_id(Student student) {
this.student_id = student;
}
public void setSubject_id(Subject subject) {
this.subject_id = subject;
}
}
The application perfectly creates the tables in the database and I can get and post in the student and subject tables. No problem with that. The pain comes with the controller for the joint table.
This is the controller for the student_subject joint table table
package com.rest.web.postgresapi.controllers;
import com.rest.web.postgresapi.data.models.StudentSubject;
import com.rest.web.postgresapi.repository.StudentSubjectRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
#RestController
public class StudentSubjectController {
#Autowired
private StudentSubjectRepository studentSubjectRepository;
#GetMapping("/enrollments")
public List<StudentSubject> getAllStudentsSubjects(){
return studentSubjectRepository.findAll();
}
#PostMapping("/enrollments/student/subject")
public StudentSubject createStudentSubject(#Valid #RequestBody StudentSubject studentSubject) {
return studentSubjectRepository.save(studentSubject);
}
}
There are two problems:
1 .- when I do the get from the student_subject table, It only retrieves the id of the row and the created_at and updated_at fields. No student_id nor subject_id.
response from get
2 .- when I do the post (from postman) to insert a row, I got the following error:
Detail: Failing row contains (4671, 2018-11-20 11:04:34.176, 2018-11-20 11:04:34.176, null, null).
I provide both student_id and subject_id, as you can see at this screenshot from postman, but the error clearly states both fields are null:
postman post
It seems that my definition of the table is somehow wrong. What am I missing in my code?
Spring MVC uses Jackson to serialize/deserialize Java objects to/from JSON.
If you annotate an attribute with #JSONIgnore then Jackson will ignore it.
This is the reason why you don't see the student_id field or the subject_id field in your JSON response of the GET method. Because Jackson is ignoring them when converts from object to json.
And this is the reason why your POST fails. Because Jackson is ignoring the received attributes. Jackson is creating an empty entity and JPA is saving the entity without student_id and subject_id.
Solved by replacing
#JsonIgnore
with
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
as indicated in this answer

Value too long for Column X Binary (255) in Java SpringBoot with JPA

I'm trying to create a Game that has several sites, such as a Pyramid or a Temple. I get the same error for all the sites, so I'll just use on a as an example - Temple. What I'm trying to do is to initialize the gameboard, by creating new sites and assigning them to the game, and vice versa. Setting the game in the site classes works fine, but setting the sites in the parent "Game.java" throws following error:
2017-04-13 17:23:10.183 WARN 5764 --- [ main]
o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 22001, SQLState:
22001 2017-04-13 17:23:10.183 ERROR 5764 --- [ main]
o.h.engine.jdbc.spi.SqlExceptionHelper : Value too long for column
"TEMPLE BINARY(255)":
"X'aced00057372002d63682e757a682e6966692e7365616c2e736f707261667331372e656e746974792e73697465732e54656d706c65bfa968665c9a87790200...
(2722)"; SQL statement: update game set burial_chamber=?,
current_player=?, market=?, name=?, obelisk=?, ownerid=?, pyramid=?,
shipyard=?, status=?, temple=? where game_id=? [22001-191] 2017-04-13
17:23:10.185 INFO 5764 --- [ main]
o.h.e.j.b.internal.AbstractBatchImpl : HHH000010: On release of
batch it still contained JDBC statements
import javax.persistence.*;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
#Entity
public class Temple implements Serializable {
#Column
private boolean isDockEmpty = true;
#Id
#GeneratedValue
#Column(name = "id", updatable = false, nullable = false)
private long id;
#ElementCollection
private List<Color> stones = new ArrayList<Color>();
public List<Color> getStones (){
return stones;
}
#Column (name = "name")
private String name = "Temple";
#OneToOne
#JoinColumn (name = "game_id")
private Game game;
#OneToOne
#JoinColumn(name = "SHIP_ID")
private Ship ship;
public long getId(){
return id;
}
public void fillDock (){isDockEmpty = false;}
public void setId(long id) {
this.id = id;
}
/*public void setStones(List<Stone> stones) {
this.stones = stones;
}*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Game getGame() {
return game;
}
public void setGame(Game game) {
this.game = game;
}
public Ship getShip() {
return ship;
}
public void setShip(Ship ship) {
this.ship = ship;
}
}
This call here (game.setTemple(newTemple)), in the BoardService.java class throws the error:
private void createAndAssignSites(Game game) {
BurialChamber newBurialChamber = new BurialChamber();
Pyramid newPyramid = new Pyramid();
Obelisk newObelisk = new Obelisk();
Temple newTemple = new Temple();
Market newMarket = new Market();
newBurialChamber.setGame(game);
newPyramid.setGame(game);
newObelisk.setGame(game);
newTemple.setGame(game);
newMarket.setGame(game);
// game.setBurialChamber(newBurialChamber);
// game.setPyramid(newPyramid);
// game.setObelisk(newObelisk);
game.setTemple(newTemple);
// game.setMarket(newMarket);
gameRepository.save(game);
burialChamberRepository.save(newBurialChamber);
pyramidRepository.save(newPyramid);
obeliskRepository.save(newObelisk);
templeRepository.save(newTemple);
marketRepository.save(newMarket);
Here the Game.java class without getters and setters and other trivial methods:
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.*;
import ch.uzh.ifi.seal.soprafs17.constant.GameStatus;
import ch.uzh.ifi.seal.soprafs17.entity.sites.*;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
#Entity (name = "game")
public class Game implements Serializable {
private static final long serialVersionUID = 1L;
private List<User> players = new ArrayList<>();
private List<Move> moves = new ArrayList<>();
public Game (){}
public Game (String name, long ownerID, User player){
this.name = name;
this.ownerID = ownerID;
this.status = GameStatus.PENDING;
players.add(player);
}
private Long id;
#Id
#GeneratedValue
#Column (name = "game_id")
public Long getId (){
return id;
}
#Column(nullable = false)
private String name;
#Column(nullable = false)
private Long ownerID;
#Column
private GameStatus status;
#Column
private Integer currentPlayer = 0;
#OneToMany(mappedBy="game")
public List<Move> getMoves(){
return moves;
}
#JsonIgnore
#LazyCollection(LazyCollectionOption.FALSE)
#OneToMany (mappedBy="game",cascade = CascadeType.ALL)
public List<User> getPlayers (){
return players;
}
public void setPlayers (List<User> players){
this.players = players;
}
#OneToOne
private BurialChamber burialChamber;
#OneToOne
private Market market;
#OneToOne
private Obelisk obelisk;
#OneToOne (mappedBy = "game")
private Pyramid pyramid;
#OneToOne
private Shipyard shipyard;
#OneToOne (mappedBy = "game")
private Temple temple;
I don't understand what that 2722 character String is and where it is generated. And why the assignment works in one direction but not in the other...
Hope you guys can point me to the error source.
Thanks
Arik
The issue is that you are mixing annotations on both fields and methods.
The JPA provider will determine which strategy you are using by looking for the #ID annotation which in the case of Game is on the method.
#Id
#GeneratedValue
#Column (name = "game_id")
public Long getId (){
return id;
}
Essentially then the #OneToOne annotation on Temple is ignored as it is on the Field:
#OneToOne (mappedBy = "game")
private Temple temple;
So Hibernate essentially tries then to persist Temple as a Binary value in the Game table as it is not aware of the relationship - it simply sees it as a simple field.
You can mix the annotations as detailed below but that is rarely required. Use one or the other in most cases:
http://howtodoinjava.com/jpa/field-vs-property-vs-mixed-access-modes-jpa-tutorial/

Unable to display data from relationships in templates

I'm stuck with trying to display data for a One-to-One relationship in Twirl templates (using Play Framework Java - 2.5.10). Basically I have a User model:
package models;
import java.sql.Date;
import javax.persistence.*;
import com.avaje.ebean.Model;
#Entity
#Table(name = "users")
public class User extends Model {
#Id
#Column(name = "id")
public Long id;
#Column(name = "first_name")
public String firstName;
#Column(name = "middle_name")
public String middleName;
#Column(name = "last_name")
public String lastName;
#Column(name = "date_of_birth")
public Date dateOfBirth;
#Column(name = "sex")
public String sex;
#OneToOne
#JoinColumn(name = "time_zone_id")
public TimeZone timeZone;
public static Finder<Long, User> find = new Finder<>(User.class);
}
and the Farmer model:
package models;
import com.avaje.ebean.Model;
import javax.persistence.*;
import java.util.List;
#Entity
#Table(name="farmers")
public class Farmer extends Model {
public enum Status {INACTIVE, ACTIVE}
#Id
#Column(name="id")
public Long id;
#OneToOne(cascade = CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn(name="user_id")
public User user;
#Column(name="profile_pic_url")
public String profilePicUrl;
#Column(name="access_url")
public String accessUrl;
#Column(name="status")
public String status = Status.INACTIVE.name();
#OneToMany(mappedBy = "farmer", targetEntity = Farm.class, fetch = FetchType.LAZY)
public List<Farm> farms;
public static Finder<Long, Farmer> find = new Finder<>(Farmer.class);
public static List<Farmer> getAllActive() {
return Farmer.find.where().eq("status", Status.ACTIVE.name()).findList();
}
}
Notice there's a one-to-one with User model with fetch type set to eager. Now, I want to display data of farmers in my template, where a farmer's name is actually the name in the associated User model.
So I did this in my controller:
public class FarmerController extends Controller {
public Result all() {
return ok(farmers.render(Farmer.getAllActive()));
}
public Result farmer(Long id, String url) {
return ok(farmer.render());
}
}
Now this gets me the right farmer data, but when I try to display the name via the User model, I get null. More specifically, writing this results in nulls (I get nullnull, actually):
<div><h4>#(farmer.user.firstName + farmer.user.lastName)</h4></div>
What am I missing?
As discussed at the comments, this is because play-enhancer does not works for views or any Scala code at all. Since Twirl compiles scala.html code to scala code, this compiled code is not touched by the enhancer.
The solution is then to manually create the get for the relationship:
public class Farmer extends Model {
public User getUser() {
return this.user;
}
}
This is Java code and then will be handled as expected. Of course, you have to change your views to use farmer.getUser instead of farm.user.
Also, as stated at the docs, byte code enhancement involves some magic. But you can avoid it at all and just use regular POJOs (with explicitly declared gets and sets).

HQL with reflexive association doesn't work

I'm having a trouble with a class that's associated with itself. The object it's this:
Category.java
package com.borjabares.pan_ssh.model.category;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import com.borjabares.pan_ssh.util.Trimmer;
#Entity
public class Category {
private long categoryId;
private String name;
private Category parent;
public Category() {
}
public Category(String name, Category parent) {
this.name = name;
this.parent = parent;
}
#SequenceGenerator(name = "CategoryIdGenerator", sequenceName = "CategorySeq")
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "CategoryIdGenerator")
public long getCategoryId() {
return categoryId;
}
public void setCategoryId(long categoryId) {
this.categoryId = categoryId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = Trimmer.trim(name);
}
#ManyToOne(optional=false, fetch=FetchType.EAGER)
#JoinColumn(name="categoryId", insertable=false, updatable=false, nullable = true)
public Category getParent() {
return parent;
}
public void setParent(Category parent) {
this.parent = parent;
}
#Override
public String toString() {
return "Category [\ncategoryId=" + categoryId + ", \nname=" + name
+ ", \nparent=" + parent + "]";
}
}
The association it's only one level deep. Insert, and other queries are working. But when I try to select only parent categories or non parent categories Hibernate only returns 0 results for parent categories or all the results of the table.
The queries, as they are right now is like this, but I've a lot of other queries with joins, is null and other methods obtaining always the same result.
#SuppressWarnings("unchecked")
public List<Category> listParentCategories() {
return getSession().createQuery(
"SELECT c FROM Category c WHERE c.parent is null ORDER BY c.name")
.list();
}
Thank you, and sorry for the mistakes I've maybe made writing this.
EDIT:
Insertion works fine, when I list all the categories in jUnit and print them I've got this:
Category [
categoryId=416,
name=Deportes,
parent=null],
Category [
categoryId=417,
name=Formula 1,
parent=Category [
categoryId=416,
name=Deportes,
parent=null]],
Category [
categoryId=418,
name=F?tbol,
parent=Category [
categoryId=416,
name=Deportes,
parent=null]]
Besides in the insertion I'm controlling that a category can only be parent or child, and a category can't be his own father.
You're using the same column (categoryId) to uniquely identify a category, and to reference the parent category. This can't possibly work, since all categories would obviously have themselves as parent. You need another column to hold the parent category ID:
#Id
#SequenceGenerator(name = "CategoryIdGenerator", sequenceName = "CategorySeq")
#GeneratedValue(strategy = GenerationType.AUTO, generator = "CategoryIdGenerator")
#Column(name = "categoryId") // not necessary, but makes things clearer
public long getCategoryId() {
return categoryId;
}
// optional must be true: some categories don't have a parent
#ManyToOne(optional = true, fetch = FetchType.EAGER)
// insertable at least must be true if you want to create categories with a parent
#JoinColumn(name = "parentCategoryId", nullable = true)
public Category getParent() {
return parent;
}

Categories

Resources