Why #OneToMany relations generate additional SQL select queries? - java

#Entity
public class Author {
#OneToMany(cascade = {PERSISTE, MERGE},
mappedBy = "author")
private List<Book> books = new ArrayList<>();
public void addBook(Book book) {
this.books.add(book);
book.setAuthor(this);
}
//other fields
}
#Entity
public class Book{
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "author_id")
private Author author;
}
public class BookService {
AuthorRepository repository;
#Transactional
public void saveBooks(){
Book newBook = new Book);
Author author = repository.findByid(1L);
author.addBook(newBook);
}
}
In a database, an author already contains one book.
I create a new book for an author and want to save it.
But when the function addBook is called the existing book is loaded from the database. I see the additional SQL query in logs:
#Query["select book ...."]
How can I fix it?
I don't want to have additional SQL selects

This is just how PersistentBag works - Hibernate loads it when you perform action on list. I would simply replace adding new Book to Author's books list with referencing created Book to existing Author
public class BookService {
BookRepository bookRepository;
AuthorRepository authorRepository;
#Transactional
public void saveBooks() {
Book newBook = new Book();
Author author = authorRepository.findByid(1L);
newBook.setAuthor(author);
bookRepository.save(newBook);
}
}

The reason for this is the basic premise under which JPA works: You load an object graph, you manipulate it, at the end of the transaction JPA makes sure the changes end up in the database.
I see two ways to avoid the select.
reverse the relationship: When the Book references the Author there is no need to load the collection, since it doesn't even exist in the first place. If you actually need the collection you can always use a dedicated query for that.
Drop back to SQL and just execute a SQL insert statement. Of course this can lead to inconsistencies between your database and the 1st level cache of JPA. Make sure you understand how the 1st level cache works and what it is used for.

Related

Problem when using Spring Specification with One-To-Many relationship

I'm having a problem with Specification as stated in the title.
Here is my FishingLocation class:
#Entity
#Table(name = "tbl_fishing_location")
public class FishingLocation {
...
#JsonIgnore
#OneToMany(mappedBy = "id", fetch = FetchType.EAGER)
private List<Lake> lakeList;
...
}
And here is my Lake class:
#Entity
#Table(name = "tbl_lake")
public class Lake {
...
#JsonIgnore
#ManyToOne
#JoinColumn
private FishingLocation fishingLocation;
...
}
They both have a StaticMetamodel as follow:
#StaticMetamodel(FishingLocation.class)
public class FishingLocation_ {
public static volatile ListAttribute<FishingLocation, Lake> lakeList;
public static volatile SingularAttribute<FishingLocation, Long> id;
}
#StaticMetamodel(Lake.class)
public class Lake_ {
public static volatile SingularAttribute<Lake, Long> id;
public static volatile SingularAttribute<Lake, FishingLocation> fishingLocation;
}
I have created a Specification to filter FishingLocation as follow (the fishing method is not relevant):
public static Specification<FishingLocation> fishingMethodIdIn(Set<Long> fishingMethodIds) {
if (fishingMethodIds == null || fishingMethodIds.isEmpty()) {
return null;
}
return root.join(FishingLocation_.lakeList)
.join(Lake_.fishingMethodList)
.get(FishingMethod_.id).in(fishingMethodIds);
};
}
The problem is that when I run the program and send a request to filter, Hibernate showed me this SQL query:
select
fishingloc0_.id as id1_7_,
fishingloc0_.active as active2_7_,
...
from
tbl_fishing_location fishingloc0_
.... (some inner joins)
inner join
tbl_lake lakelist4_
on fishingloc0_.id=lakelist4_.id
....
It is supposed to be on fishingloc0_.id=lakelist4_.fishing_location_id. So where is the problem in my code? Huge thanks to anyone helping me with this problem.
Edit: This is just a small fraction of my code. My filter has multiple criteria and join many tables so I cannot just use premade function in FishingLocationRepository. I want to create dynamic query which should be build depend on what user chooses to filter by.
you don't need to call this all using custom queries the Hibernate will take care of everything with OneTO Many and ManyTOOne mappings.
You can get the required data by simply calling the getter methods of its class.
for example, in the case of Fishing Location, you can get all the lakes related to that location by calling new FishingLocation().getLakeList().
same for the other case. https://www.javatpoint.com/hibernate-many-to-one-example-using-annotation

Hibernate: Save multiple association issue

I have three entities with relations.
public class Site {
#OneToMany
private List<Page> pages;
}
public class Page {
#OneToMany
private List<Article> articles;
#OneToMany
private List<TopArticle> topArticles;
}
public class Article {
#ManyToOne
private Page page;
}
public class TopArticle {
#ManyToOne
#JoinColumn(name = "PageId")
private Page page;
#OneToOne
#JoinColumn(name = "ArticleId")
private Article article;
}
Fields page and arctile in TopArticle have database restrictions - NOT NULL. (Resctrictions are required)
I have to save Site with all associations.
Article article = new Article();
TopArticle topArticle = new TopArticle();
Page page = new Page();
page.getArticles().add(article);
page.getTopArticles().add(topArticle);
Site site = new Site();
site.getPages().add(page);
siteDAO.save(site);
Sometimes it saves site.
But sometimes it throws an error.
com.microsoft.sqlserver.jdbc.SQLServerException: Cannot insert the value NULL into column 'ArticleId', table '...TopArticle'; column does not allow nulls. INSERT fails
It seems that after Page is saved, it tries to save associations List articles and List topArticles.
Perhaps, when List articles are saved before List topArticles it works.
In another way it fails.
Questions:
Is an issue in saving properties order in JPA?
How can I enforce Hibernate to save articles before topArticles?
Is there any other solutions?
Thanks
With a bi-directional relationship it is your responsibility to ensure both sides are set correctly on the in-memory model otherwise you will get the error you are seeing.
So you need to call:
article.setPage(page);
And ideally you should encapsulate the operation so the in-memory model is always consistent:
public class Page{
public List<Article> getArticles(){
return Collections.unmodifiableList(articles); //force through add method
}
public void addArticle(Article article){
articles.add(article);
article.setPage(this);
}
}

Cannot persist Entity with Shared ID

I am practicing JavaEE technologies. Now I am focusing in JPA with Hibernate. The project has the following entities:
Book:
#Entity #Table(name = "book")
public class Book extends BaseEntity<Long> {
#Id
private Long id;
#NotNull
private String name;
#OneToOne(mappedBy = "book", cascade = CascadeType.ALL)
private BookDetails details;
//getters/setters
}
BookDetails:
#Entity
#Table(name = "book_details")
public class BookDetails extends BaseEntity<Long> {
#Id
private Long id;
#MapsId
#OneToOne
private Book book;
#NotNull
#Column(name = "number_of_pages")
private int numberOfPages;
//getters/setters
}
Now, the respective EJB Service classes:
BookService:
#Stateless
public class BookService extends BaseEntityService<Long, Book> {
public void createBook(Book book) {
super.getEntityManager().persist(book);
}
//update, delete, find and findAll methods
}
BookDetailsService:
#Stateless
public class BookDetailsService extends BaseEntityService<Long, BookDetails> {
public void createBookDetails(BookDetails details) {
super.getEntityManager().persist(details);
//super.persist(details); //This method would not work to persisting entity with shared Id, because it verifies if ID is null
}
//update, delete and find methods
}
The problem:
When I try to persist a new book along with its details as follows:
Book b = new Book();
b.setId(123L);
b.setName("Bible");
bookService.createBook(b);
//The entity Book is correctly persisted in the DB.
BookDetails d = new BookDetails();
d.setNumberOfPages(999);
d.setBook(b);
//b.setDetails(d); //don't do this
bookDetailsService.createBookDetails(d);
//BookDetails is not persisted in the DB, throws exception....
Throws the following exception:
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '123' for key 'PRIMARY'
The Book entity is persisted but not the BookDetails.
I followed this tutorials:
Hibernate Tips: How to Share the Primary Key in a One-to-One Association
The best way to map a #OneToOne relationship with JPA and Hibernate
Aditional Information:
JDK 1.8
Payara 5
MySQL 8.0
Hibernate 5.3.4
OmniPersistence library. (Utilities for JPA, JDBC and DataSources)
You can look at the project repository here.
I could already solve the problem and it was due to the way the entities persisted and the transactionality of the methods in a JavaEE application.
When calling a business method of an EJB in which classes are persisted, upon completion of this method the transaction ends, therefore the persistence context is closed and the attached classes (managed) become detached (unmanaged). See this.
So after persisting Book b:
bookService.createBook (b);
That transaction ends and that is why the book is persisted and, in addition, it is no longer managed. So when I link the book to the details:
d.setBook (b);
That is something that must be done to persist an entity that shares Id, the parent entity (in this case b) has to be managed.
There are two solutions that i could find:
SoluciĆ³n 1:
Attach book b to the persistence context of the details creation transaction. See this.
b = getEntityManager.merge(b);
SoluciĆ³n 2: (The one that I chose)
Make the call of the BookService business method within the business method of the BookDetailsService, which implies transferring the dependency injection. Thus, a single transaction is made by persisting the two entities.
//BookDetailsService class
#Inject
BookService bookService;
public void createBookDetails(BookDetails details) {
Book b = new Book();
b.setId(details.getId());
b.setName("Bible");
bookService.createBook(b);
details.setBook(b);//b still managed
super.persist(details);
}
I think this solution is more cleaner and coherent, because BookDetailsService will need always the services from BookService.
#OneToOne on BookDetails property of Book class indicates that there is a one-to-one association from Book to BookDetails. Also, note the use of cascade = CascadeType.ALL.Since BookDetails entity cannot exist without Book entity, with this cascade setting, BookDetails entity will be updated/deleted on subsequent update/delete on Book entity.
You have to add cascadetype on book details:
#OneToOne(mappedBy = "book", cascade = CascadeType.ALL)
private BookDetails details;
Refer here for more details
CascadeType.ALL will do all the operations such as save, delete and merge when book entity is saved.
Now as we have added CascadeType.ALL in Book entity you have to set bookdetails object in book. See below:
Book b = new Book();
BookDetails d = new BookDetails();
b.setBookDetails(d);
b.setId(123L);
b.setName("Bible");
d.setId(111L); //random id that is not existing if d is new record
d.setNumberOfPages(999);
bookService.createBook(b);
Saving book entity will save book as well as book details entity. You may also try hibernate.show_sql=true to see what all queries are executed when you save book entity.

ManyToMany query in JPA

I have two tables/entities. All working fine (getters and setters omitted).
#Entity
public class Book() {
#Id;
int id;
#ManyToMany
List<Category> categories;
}
#Entity
public class Category {
#Id
int id;
#ManyToMany
List<Book> books;
}
JPA/Hibernate creates database structure as expected with a join table called book_category that contains the fields book_id and category_id.
Categories are created in advance so not all of them have books.
I want to query for distinct categories that has books in them.
The following SQL-query does it:
SELECT DISTINCT <things> FROM book_category JOIN category ON category.id = book_category.category_id
But how can i get the list of categories from the Repository?
If you want all categories which have books assigned to them, then I think you just need a query on the Category entity which checks the size of its book collection:
select c from Category where size(c.books) > 0
Even if querying the junction table directly would be possible, e.g. via a native query, I don't think that this would be considered good JPA practice. Rather, the two many-to-many entities you already have should allow you to perform the queries you need.
As I understood, you want a JPA Repository for your Categories. This will create a repository and then you can use it to get the categories which have books in them. You will just need to get the Book Id. JPA is very flexible, so this may not be what you want exactly, but try it out yourself.
public interface CategoryRepository extends JpaRepository <Category, Integer>
{
List<Category> findByBookId(Integer book);
}
EDIT: Alright, it's been a long day and im tired af. How about if you try the reverse?
public interface BookRepository extends JpaRepository <Book, Integer>
{
List<Book> findAllByCategoryId(Integer category);
}
then check if it's null or not.

Hibernate: Change instance of base class to subclass

I would like to change a concrete superclass to one of its subclass. I've included an example below:
#Entity
#Table(name = "employees")
#Inheritance(strategy = InheritanceType.JOINED)
public class Employee {
#Id
#Column(name = "id")
private String id;
public Employee( String id ) {
this.id = id;
}
...
}
#Entity
#Table(name = "managers")
#PrimaryKeyJoinColumn(name = "id", referencedColumnName = "id")
public class Manager extends Employee {
public Manager( String id ) {
super(id);
}
...
}
#Entity
#Table(name = "history")
public class History {
...
/**
*
*/
#ManyToOne
#JoinColumn(name = "employee_id")
private Employee employee;
...
}
The three classes I'm working with are Employee, Manager and History. All Managers are Employees, but not all Employees are Managers. All Employees (and Managers) have a History. Employees may be promoted to Management. When this happens an Employee's history should be retained by keeping their Employee ID the same. This will allow a Manager's History through employment to be easily found.
Implementing the promotion operation is complicated by constraints withing the database: the database will not allow removing the old Employee and creating a new Manager with the same ID without removing all of the History objects by cascading operation - which Human Resources won't allow, otherwise my job would be easy!
Is it possible to add or attach the Manager (new managers) row to an existing Employee without resorting to custom SQL operation?
I've tried the following solutions without success:
public void promote( Employee employee ) {
/* copy over the ID of the employee to the manager. Will not work because employee with ID already exists */
Manager manager = new Manager(employee.getId());
this.entityManager.persist( manager );
}
... or ...
public void promote( Employee employee ) {
/* detach the employee then merge. Will not work: fails, again, with a NonUniqueObject Exception */
this.entityManager.detach( employee );
Manager manager = new Manager(employee.getId());
this.entityManager.merge( manager );
}
How can I get this done? Am I even on the right track with detach() and merge()?
Is this possible in Hibernate/JPA?
Any ideas will be helpful at this point as I'm stumped!
Aaron
As you're no doubt starting to see, you're rowing against the wind here. Unfortunately, it looks like you have a classic example of using inheritance to represent role. Both Hibernate--an Object-Relational Mapper--and Java--an Object-oriented language--are going to fight you on this one because your object model is wrong. I'd say your best bet is to fix it now with a refactoring and data migration. The end result should be that everyone is an Employee, and some Employees have some kind of "manages" relationship with one or more departments or other Employees.
I ran into a similar situation and I don't believe a flawed data model is to blame since the data model matches the real world model quite well.
You can insert the subclass record manually to achieve the desired result. Sometimes it's requisite to go around Hibernate to get something done, so this is what I did in this case as a workaround:
sessionFactory.getCurrentSession().createSQLQuery(
"insert into manager (employee_id, rank) " +
"values (:employeeId, :rank) ")
.setParameter("employeeId", employeeId)
.setParameter("rank", rank)
.executeUpdate();

Categories