Spring Data Jpa Entity RelationShip Mappings
@OneToOne Unidirection
Java Entities
Capital Entity
@Entity
@Data
@NoArgsConstructor
@Table( name = "capital", schema = "public")
public class Capital {
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "public.capital_seq")
private Long id;
private String name;
public Capital(String name) {
this.name = name;
}
}
Country Entity
@Entity
@NoArgsConstructor
@Table(name = "country", schema = "public")
public class Country {
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator="public.country_seq")
private Long id;
private String name;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "capital_id_fk", // default "item_id", could by omitted
referencedColumnName = "id") // default could be omitted
private Capital capital; // in DB -> item_id_fk bigint H2
Hibernate creates automatically the tables the next way
Tables
create table public.capital (
id int8 not null,
name varchar(255),
primary key (id)
)
create table public.country (
id int8 not null,
name varchar(255),
capital_id_fk int8,
primary key (id)
)
Test the behaviour
Country country = new Country("Mexico");
Capital capital = new Capital("CDMX");
country.setCapital(capital);
Country _country = countryRepository.save(country);
log.info("savedCountry {}", _country);
Country countryFinded = countryRepository.findById(_country.getId()).get();
log.info("recoveredCountry: {}", countryFinded);
Capital recoveredCapital = capitalRepository.findById(_country.getCapital().getId()).get();
log.info("recoveredCapital: {}", recoveredCapital);
// given new Country , new Capital -> capital set country IMPOSSIBLE -> save capital
// never save Country
List<CountryCapital> countryCapitalList = countryRepository.getJoinCountryCapital();
countryCapitalList.forEach(e -> log.info("countryCapital: {}", e));
On the output the queries generated by hibernate on the test.
Creation of tables
Hibernate:
create table public.capital (
id int8 not null,
name varchar(255),
primary key (id)
)
Hibernate:
create table public.country (
id int8 not null,
name varchar(255),
capital_id_fk int8,
primary key (id)
)
Hibernate:
alter table if exists public.country
add constraint FK54ksb3rb0668rqk7cgvo7tab7
foreign key (capital_id_fk)
references public.capital
Queries executed by test
Hibernate:
select
nextval ('public.country_seq')
Hibernate:
select
nextval ('public.capital_seq')
Hibernate:
/* insert com.bext.onetooneunidirection.entity.Capital
*/ insert
into
public.capital
(name, id)
values
(?, ?)
Hibernate:
/* insert com.bext.onetooneunidirection.entity.Country
*/ insert
into
public.country
(capital_id_fk, name, id)
values
(?, ?, ?)
2022-06-20 19:11:32.881 INFO 19960 --- [ main] .o.SpringdatajpaentitymappingApplication : savedCountry Country{id=1, name='Mexico', capital=Capital(id=1, name=CDMX)}
Hibernate:
select
country0_.id as id1_1_0_,
country0_.capital_id_fk as capital_3_1_0_,
country0_.name as name2_1_0_,
capital1_.id as id1_0_1_,
capital1_.name as name2_0_1_
from
public.country country0_
left outer join
public.capital capital1_
on country0_.capital_id_fk=capital1_.id
where
country0_.id=?
2022-06-20 19:11:32.902 INFO 19960 --- [ main] .o.SpringdatajpaentitymappingApplication : recoveredCountry: Country{id=1, name='Mexico', capital=Capital(id=1, name=CDMX)}
Hibernate:
select
capital0_.id as id1_0_0_,
capital0_.name as name2_0_0_
from
public.capital capital0_
where
capital0_.id=?
2022-06-20 19:11:32.904 INFO 19960 --- [ main] .o.SpringdatajpaentitymappingApplication : recoveredCapital: Capital(id=1, name=CDMX)
Hibernate:
/* SELECT
new com.bext.onetooneunidirection.dto.CountryCapital(c.name,
i.name)
FROM
Country c
JOIN
c.capital i */ select
country0_.name as col_0_0_,
capital1_.name as col_1_0_
from
public.country country0_
inner join
public.capital capital1_
on country0_.capital_id_fk=capital1_.id
2022-06-20 19:11:32.922 INFO 19960 --- [ main] .o.SpringdatajpaentitymappingApplication : countryCapital: CountryCapital(countryName=Mexico, capitalName=CDMX)
============================================================
@JoinColumn to Specify the "JoinColumn"
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator="customer_seq")
private Long id;
private String name;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name="fk_item_id")
private Item item;
Table generated
create table customer (
id bigint not null,
name varchar(255),
fk_item_id bigint,
primary key (id)
)
-ToOne relationship, JPA specifies a default of FetchType.EAGER, better put ot Lazy.
-ToMany relationships. Better use pagination.
Bidirectinal associations requiere auxiliar operations when add and remove.
Order class
public void addItem(Item item){
item.setOrder(this);
this.items.add(item);
}
public void removeItem(Item item){
item.setOrder(null);
this.items.remove(item);
}
@OneToOne bidirectional
- Uning really @OneToMany bidirectional
- Using Shared Key
Using SharedKey not specified to do in spring yet.
In bidirectional associations needs to be care of update, the add or remove needs to perform the operation on both ends.
@OneToMany unidirectional
this should be avoided, Hibernate might crate unexpected tables and execute more sql statementes than you expected. with @JoinColumn skip the creation of extra table, but still create n+1 query, 2 select, insert, update. even with a JOIN FETCH query.
Is prefered to use @OneToMany bidirectional. Better to use ManyToOne unidirectional because Hibernate when initialize -ToMany loads all the associations.
https://www.bezkoder.com/jpa-many-to-many/
joining multiple entities in Spring JPA
https://stackoverflow.com/questions/70136727/joining-multiple-entities-in-spring-jpa
JPA cascade PERSIST Employee --- Address
If not specified, One must to save address and Employee, Else only save parent Employee and their soons will be saved automatically.
Thorben Janssen: JPA vs Hibernate: The differece between save, persist, merge and update
https://www.youtube.com/watch?v=RVlSVPATJio&list=PL50BZOuKafAbXxVJiD9csunZfQOJ5X7hP&index=8&ab_channel=ThorbenJanssen
Detached
| detached(entity)
merge(entity) evict(entity)
update(entity) clear()
| close()
| ^ find(class,id)
V | get(class,id)
load(class,id) DB
Transient persist(entity) --> Managed all queries
save(entity) persistent context <-------------------------------------
1st level cache
| ^ -- flush() ------------------------->
| |
remove(entity) | | persist(entity)
| | save(entity)
V |
Removed
JPA.persist or Hibernate.save
JPA.persist return void | Hibernate.save return primary key
Hibernate primary key value to store the entity in the first level cache. generates the primary key immediatley and (execute SQL trigger if necesary) when you call save or persist method. This is not
when uses IDENTITY strategy without and Active Transaction or with FlushMode.Manual.
When Persist Method: In this case hibernate delays the exccution of SQL Insert and creates a temporatily primary key value.
When Save Method: hibernate excetues the SQL Insert immediately and retrieves the primary key from database.
strategy.generationType.TABLE requiere row level Locks on the primary key table and doesn't scale well.
JPA.merge copies the state of a detached entity to a managed instance of the same entity.
Hibernate instead executes SQL select to retrieve the managed entity to the database. if the persistence context already contained a managed instance, Hibernate uses the existing one instead. it the copies all the attributes values to the managed entity and returns it to the caller. This way Hibernate lost the change made for the entity because use the existing one.
In contrast to JPA´s merge you can´t lose any changes by calling the update method. If the persistence context already contains a managed instance of the entity you want to update, it throws an exception.
Hibernate update just perform the SQL Update, doesn´t perfomr any dirty check.
This could be a problem if your DBA registered an update Trigger for the database table. To perform a dirty check (SQL Select) annotate the entity with @SelectBeforeUpdate. Now the behavior of update method is similar to the JPA´s merge method.
The difference between Update method, Hibernate only select the entity which you provided as a method parameter. JPA's Merge method Hibernate also select all the association with CascadeType.MERGE.
So select JPA Merge method if you reattach a huge graph of entities.
What to use Update or Merge.
- do you need dirty checks?
merge - Do you reattach a graph of entities?
Derived Queries with Spring Data JPA
- Use @Query(" ... ") to define custom query
- Method Start with
- findBy...(...)
- readBy...(...)
- queryBy...(...)
- getBy...(...)
- countBy...(...)
example
public interface AutorRepository extends JpaRepository<Autor, Long>{
List<Autor> findByFirstName(String firstName);
List<Autor> findByFirstNameAndLastName(String firstName, String lastName);
List<Autor> findByFirstNameContainingIgnoreCase(String firstName)
// Traverse association in derived queries
List<Autor> findByBooksTitle(String title)
}
- Comparison Operators
- Like
- Containing
- IgnoreCase
- Between
- LessThan or GreatherThan
- Order the result of derived query
public List<Author> findByFirstNameContainingIgnoreCaseOrderByYearAsc(String partOfFirstName);
public List<Author> findByFirstNameContainingIgnoreCaseOrderByYearDesc(String partOfFirstName);
public List<Author> findByFirstNameContainingIgnoreCaseOrderByBooksTitle(String partOfFirstName);
public List<Author> findByFirstNameContainingIgnoreCase(String partOfFirstName, Sort sort);
Sort sortYearAsc = Sort.by("year").ascending().and(Sort.by("firstName").descending());
List<Author> findByFirstNameContainingIgnoreCaseSortParameter = authorRepository.findByFirstNameContainingIgnoreCase("", sortYearAsc);
- Limiting the number of result
//Limiting
public List<Author> findFirst2ByFirstNameContainingIgnoreCaseOrderByYear(String partOfFisrtName);
- Paginate the results of a derived query
Custom Queries
JPQL - JPA Query Language is database agnostic but support subset of sql standard, not for complex queries.
Parameters
@Query("FROM Author WHERE firstName = ?1")
List<Author> findByFirstName(String firstName)
@Query("FROM Author WHERE firstName = ?1 AND lastName=?2")
List<Author> findByFirstNameAndLastName(String firstName, String lastName);
Sorting
@Query("FROM Author WHERE firstName = ?1" SORT BY lastName ASC")
List<Author> findByFirstNameOrderByLastName(String firstName);
@Query("FROM Author WHERE firstName = ?1", Sort sort)
List<Author> findByFirstName(String firstName, Sort sort);
in use
Sort mySort = Sort.by("year").ascending().and(Sort.by("firstName").descending());
List<Author> authors = autorRepository,findByFirstName("Jose Alberto", mySort);
Paginating
List<Author> findAll(Pageable pageable)
in use
// Paging
Pageable pageable = PageRequest.of(0 ,2);
Page<Author> findAllPageable = authorRepository.findAll(pageable);
log.info("findAllPageable.getTotalElements(): {}",findAllPageable.getTotalElements());
log.info(" findAllPageable.getTotalPages() : {}", findAllPageable.getTotalPages());
findAllPageable.getContent().forEach(author -> log.info("author: {}", author.getFirstName()));
log.info("Take another page ");
Pageable pageable1 = PageRequest.of(1, 2);
Page<Author> findAllPageable1 = authorRepository.findAll(pageable1);
findAllPageable1.getContent().forEach(author -> log.info("author: {}", author.getFirstName()));
SpEL Expressions for Entity Names & Advanced Like Expressions.
@Query("FROM Author a WHERE a.firstName = ?1" )
public List<Author> findByFirstNameSpEL_param( String firstName);
@Query("FROM Author a WHERE a.firstName = :firstName" )
public List<Author> findByFirstNameSpEL_paramname( String firstName);
@Query("SELECT a FROM Author a WHERE a.year = ?#{[0]}")
public List<Author> findAuthorByYearSpEL(int year);
@Query("FROM Author WHERE UPPER(firstName) LIKE %?#{[0].toUpperCase()}%")
public List<Author> findAuthorByfirstNameLikeSpel(String firstName);
@Query("SELECT a FROM #{#entityName} a WHERE a.year > 2000")
public List<Author> findBy_entityName();
DTO Projections
Better Performance than entity projections when read-only operations.
EntityGraphs
Provides and easy and reusable way to initialize required entity associations within the query.
avoids use of n+1 querys.
Calling Stored Procedures
...
Query Projections
---
ResultSet Mapping series and How to return DTOs from Native Queries with Spring JPA
Hibernate Proxies
How and When to use getReference() method
EntityManager em = e mf.createEntityManager();
em.getTransaction().begin();
ChessTournament chessTournament = em.getReference(ChessTourment.class, 1L);
===== No Select statement for ChessTournament is executed ======
ChessGame chessGame = new ChessGame();
chessGame.setTourment( chessTournament);
em.persist( chessGame);
em.getTransaction().commit();
==== Only insert statment gets executed ======
em.close()
Hibernate didn´t fetch the ChessTournament entity from the database, It pesist the chessGame with the reference of ChessTournament. If Using Spring Data JPA this can also be achieved by using
T getOne(Id arg0)
T getById(Id arg0) on the repository it internally call the getReference method
Hibernate Proxies
https://www.youtube.com/watch?v=rADkGuDZkU4&t=183s&ab_channel=ThorbenJanssen
- Generated a Subclass
- Intecepts all method invocations
- check if the proxied entity object has been initilized
if necessary, hibernate executes a database query to initialize the entity before it calls the intercepted method on the entity object. If this happens without an active hibernate Session this causes a
LazyInitializationException.
By default, the FetchType of All To-One associations is EAGER, that means Hibernate has to fetch the associated entity object when loading an entity. This can be changed by setting the assiciation with FetchType.LAZY,
Defining a LAZY loading for a All To-One association introduces a challenge to the persistence provider. It has to find a way to get notified when your business code wants to use the assiciation and fetch it from the database. For All to-Many associations, hibernate solves this by initializing the attribute with its own collection implementations. But this doesn't work for to-One associations. The entities aren´t required to implement any interface that Hibernate could implement.
This leaves hibernate with two options:
It can add some code to the getter mehod, it requires bytecode enhancement. or it can generate a proxy class that´s a subclass of your entity.
The proxy object of an entity can be requested by calling the getRefference method on the entityManager or HibernateSession. This gets you a proxy object that can be used to initialize a to-One association when persisting a new or updating an existing entity.
The getReference methos doesn´t trigger a database query, Hibenate instantiates a proxy object and only sets the primary key attribute. Hibernate delays the execution of a query until a call of getter/setter method of any non-primary key attiibute.
How to detect a proxy object.
Most of the time a LazyInitializationException will make you aware that you´ve been working with a proxy object. Hibernate throws it if don´t have an active session and call a getter method on any non-primary key attribute of an uninitialized proxy object.
Test
em = emf.createEntityManage();
ChessGame chessGame = em.find(ChessGame.class, this.chessGame.getId());
log.info(chassGame.getPlayerWhite().getClass().getName());
assertThat( chessGame.getPlayerWhite()).isInstanceOf(HibernateProxy.class);
assertThat( Hibernate.isInitializated(chessGame.getPlayerWhite())).isFalse();
To initialize the proxy the most common is to call a gette/setter method of a non-primary key attribute.
Use
Hibernate.initialize(chessGame.getPlayerWhite());
But if you already know that you will use a lazily fetched association in your code, is recomended to initialize it in the same query that fetches the entity.
To get a real Entity from thier proxy
unproxiedPlayer = Hibernate.unproxy(playerWhite, ChessPlayer.class);
assertThat(unproxiedPlayer).isInstanceOf(ChessPlayer.class);
5 ways to initialize lazy associations and when to use them
1 .- Call a method on the mapped association. Most inefficient approach, (n+1 select issue).
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Order order = em.find(Order.class, 1L);
order.getItems().size();
em.getTransaction().commit();
em.close();
2.- JOIN FECTH in JPQL
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Query q=em.createQuery("SELECT o FROM Order o JOIN FETCH o.items i WHERE o.id = :id")
q.setParameter("id", 1L);
Order order = (Order) q.getSingleResult();
order.getItems().size();
em.getTransaction().commit();
em.close();
3.- JOIN FETCH in Criteria API
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> q = cb.createQuery(Order.class);
Root<Order> o = q.from(Order.class);
o.fetch("items", JoinType.INNER);
q.select(o);
q.where(cb.equal(o.get("id"), 1L));
Order order = em.createQuery(q).getSingleResult();
order.getItems().size();
em.getTransaction().commit();
em.close();
4.- Named Entity Graph
Named Entity Graph
JPA 2.1 Entity Graph
– Part 2: Define lazy/eager loading at runtime: https://goo.gl/sSFbke
@Entity
@Table(name = "purchaseOrder")
@NamedEntityGraph(name = "graph-Order-items",
attributeNodes = @NamedAttributeNode(value= "items"))
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, nullable = false)
private Long id = null;
@Version
@Column(name = "version")
private int version = 0;
@Column
private String orderNumber;
@OneToMany(mappedBy="order", fetch = FetchType.LAZY)
private Set<OrderItem> items = new HashSet<OrderItem>();
public Long getId(){
return this.id;
}
...
=== On Test ===
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
EntityGraph<?> graph = em.getEntityGraph("graph-Order-items");
Map<String, Object> hints = new HashMap<String, Object);
hints.put("javax.persistence.fetchGraph", graph);
Order order = em.find(Order.class, 1L, hints);
order.getItems().size();
em.getTransaction().commit();
em.close();
5.- Dynamic Entity Graph
If you need to adapt your graph based on user input. More flexibility to and avoids entities with dozens of annotations. if you need to define a use case specific graph,
witch will not reuse. named EntityGraph is easier to reuse.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributes("items");
Map<String, Object> hints = new HashMap<String, Object);
hints.put("javax.persistence.loadgraph", graph);
Order order = em.find(Order.class, 1L, hints);
order.getItems().size();
em.getTransaction().commit();
em.close();
Think twice before using CascadeType.Remove
...
No hay comentarios:
Publicar un comentario