Getting Master in Hibernate and JPA

 

Setup H2 db

  • Add required dependency for h2 
  • Enable UI by spring.h2.console.enabled=true
  • Launch the app - check the logs for URL and dbUrl ,  provide dburl over console login
App logs


05:30  INFO 6117 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-06-30T17:46:18.383+05:30  INFO 6117 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1295 ms
2023-06-30T17:46:18.429+05:30  INFO 6117 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-06-30T17:46:18.591+05:30  INFO 6117 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:dfa8a6bb-c937-43f7-82b9-e7dadc19a4e5 user=SA
2023-06-30T17:46:18.593+05:30  INFO 6117 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-06-30T17:46:18.608+05:30  INFO 6117 --- [           main] o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:dfa8a6bb-c937-43f7-82b9-e7dadc19a4e5'



Fixing db URL 

spring.datasource.url=jdbc:h2:mem:testDB


Populating Data


Create data.sql file under the resources folder and place queries over there, on launch it tables are created and populated with data.




Spring JDBC

To work with jdbc add required dependencies of jpa and spring ddbc, and create crud operation with help of JdbcTemplate and BeanPropertyRowMapper

Advantage of spring JdbcTemplate it removes boilerplate code for creating statement connections and closing stuff.



Crud Operation Code


package com.jpa.h2demo.jdbc;


import com.jpa.h2demo.entities.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public class PersonJdbcDao
{

@Autowired
private JdbcTemplate jdbcTemplate;

public List<Person> findAll()
{
return jdbcTemplate.query("select * from person_demo ", new BeanPropertyRowMapper(Person.class));
}

public Person findById(int id)
{
return jdbcTemplate.queryForObject("select * from person_demo where id=? ", new Object[]{id},
new BeanPropertyRowMapper<Person>(Person.class));
}




public List<Person> findyByLocation(String location)
{
return jdbcTemplate.query("select * from person_demo where location=?",
new Object[]{location},
new BeanPropertyRowMapper<>(Person.class));
}


public int deleteById(int id)
{
// return how many rows got delete
return jdbcTemplate.update("delete from person_demo where id=? ", new Object[]{id});
}

public int insert(Person person)
{
return jdbcTemplate.
update("insert into person_demo (id,name,location,birth_date) values (?,?,?,?)"
, new Object[]{person.getId(),person.getName(),person.getLocation(),person.getBirthDate()} );
}

public int update(int id, String location)
{
return jdbcTemplate.update("update person_demo set location=? where id=?", new Object[]{location,id});
}
}






package com.jpa.h2demo.entities;

import java.util.Date;


public class Person
{

@Override
public String toString()
{
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", location='" + location + '\'' +
", birthDate=" + birthDate +
'}';
}


public Person()
{
}

public Person(int id, String name, String location, Date birthDate)
{
this.id = id;
this.name = name;
this.location = location;
this.birthDate = birthDate;
}

private int id;
private String name;
private String location;
private Date birthDate;

public int getId()
{
return id;
}

public void setId(int id)
{
this.id = id;
}

public String getName()
{
return name;
}

public void setName(String name)
{
this.name = name;
}

public String getLocation()
{
return location;
}

public void setLocation(String location)
{
this.location = location;
}

public Date getBirthDate()
{
return birthDate;
}

public void setBirthDate(Date birthDate)
{
this.birthDate = birthDate;
}
}


package com.jpa.h2demo;

import com.jpa.h2demo.entities.Person;
import com.jpa.h2demo.jdbc.PersonJdbcDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.Date;

@SpringBootApplication
public class H2DemoApplication implements CommandLineRunner
{

@Autowired
private PersonJdbcDao personJdbcDao;

public static void main(String[] args) {
SpringApplication.run(H2DemoApplication.class, args);
}

@Override
public void run(String... args) throws Exception
{

personJdbcDao.findAll().stream().forEach(System.out::println);
personJdbcDao.findyByLocation("Bangalore").stream().forEach(System.out::println);
System.out.println(personJdbcDao.findById(10001));
System.out.println("Number of rows got deleted " +personJdbcDao.deleteById(10003));
System.out.println("Number of rows got added " +personJdbcDao.insert(new Person(10005,"kaka","Pune",new Date())));
System.out.println("Number of rows got updated " +personJdbcDao.update(10005, "Nagpur"));

}
}



Handling with CustomRow Mapper


Userful when table and entities column names vary, the below implementation can pass on the place of BeanPropertyRowMapper.


package com.jpa.h2demo.jdbc;

import com.jpa.h2demo.entities.Person;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

public class PersonRowMapper implements RowMapper<Person>
{

@Override
public Person mapRow(ResultSet rs, int rowNum) throws SQLException
{
return new Person(rs.getInt("id"),
rs.getString("name"),
rs.getString("location"),
rs.getTimestamp("birth_date"));
}
}



JPA Introduction

java persistence API, it is a specification that sets standards for ORM frameworks, and hibernate is the implementation of JPA.


JPA CRUD operations 

Make entity class to map with database relation , provide id and entity annotations,  @GenerateValue annotation will use database sequence to insert as id. below. given code for more details 


package com.jpa.h2demo.entities;






import jakarta.persistence.*;
import java.util.Date;


@Entity()
@Table(name = "person_demo")
public class Person
{

@Override
public String toString()
{
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", location='" + location + '\'' +
", birthDate=" + birthDate +
'}';
}


public Person()
{
}

public Person(int id, String name, String location, Date birthDate)
{
this.id = id;
this.name = name;
this.location = location;
this.birthDate = birthDate;
}

public Person(String name, String location, Date birthDate)
{
this.name = name;
this.location = location;
this.birthDate = birthDate;
}

@Id
@GeneratedValue
private int id;
private String name;
private String location;

@Column(name = "birth_date")
private Date birthDate;

public int getId()
{
return id;
}

public void setId(int id)
{
this.id = id;
}

public String getName()
{
return name;
}

public void setName(String name)
{
this.name = name;
}

public String getLocation()
{
return location;
}

public void setLocation(String location)
{
this.location = location;
}

public Date getBirthDate()
{
return birthDate;
}

public void setBirthDate(Date birthDate)
{
this.birthDate = birthDate;
}
}






package com.jpa.h2demo.jpa;


import com.jpa.h2demo.entities.Person;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Repository
@Transactional
public class PersonJpaDao
{

@PersistenceContext
private EntityManager entityManager;

public List<Person> findAll()
{
return entityManager.createQuery("select p from Person p",Person.class).getResultList();
}

public Person findById(int id)
{
return entityManager.find(Person.class,id);
}


public Person insert(Person person)
{
return entityManager.merge(person );
}

public Person update(Person person)
{

return entityManager.merge(person );
}

public void delete(int id)
{
entityManager.remove(findById(id));
}
}
 
Entity Manager methods 

  • flush -- flush data to DB for saving
  • detach -- detach entity from tracking and make call flush before detach to prevent transaction commit fail
  • clear -- Entity will be not tracked for any changes made over it, flush it before calling clear 
  • Refresh -- Refresh entity with db.


@Transactional
public void playWithEmPersistMethod()
{
Problem build = Problem
.builder()
.name("Dummy local")
.solution("Dummay local")
.build();
entityManager.persist(build);
build.setName(build.getName() + " updated");// will update as inside transactions scope works only with persist
}


@Transactional
public void playWithEmFlushMethod()
{
Problem problem1 = Problem
.builder()
.name("Dummy1 local")
.solution("Dummay1 local")
.build();

entityManager.persist(problem1);
// entityManager.flush();//send to db for saving


Problem problem2 = Problem
.builder()
.name("Dummy2 local")
.solution("Dummay2 local")
.build();

entityManager.persist(problem2);
// entityManager.flush();
entityManager.detach(problem2); // detach particular entity from tracking , make sure flush that entity before detach else error
// entityManager.clear();// all entities tracking will be cleared , even data will not persist if not flushed


problem1.setName(problem1.getName() + " updated");
problem2.setName(problem2.getName() + " updated");
}



JPQL - JAVA persistent  query language 

it queries from the entity - it converted into SQL via JPA implementation  .

return entityManager.createQuery("select p from Problem p" ,Problem.class).getResultList();


// Here Problem is class name


Managing to create and update timestamps with hibernate annotation 

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;




Named Queries example 

@NamedQueries(value = {
@NamedQuery(name="get-All",query = "select p from Problem p")
})


Native Query - it same as the name suggest, useful in the scenario where want to use the database-specific feature for tunning,  like mass updates which is not good with jpa like getting rows and updating it, native query is a good candidate in these cases.


public List<Problem> findAllByNativeQuery()
{
Query nativeQuery = entityManager.createNativeQuery("select * from problem ", Problem.class);
return nativeQuery.getResultList();
}

public void findByIdNativeQuery(long id)
{
Query nativeQuery = entityManager.createNativeQuery("select * from problem where id=?", Problem.class);
nativeQuery.setParameter(1,id);
System.out.println(nativeQuery.getFirstResult());

nativeQuery = entityManager.createNativeQuery("select * from problem where id= :id", Problem.class);
nativeQuery.setParameter("id",id);
System.out.println(nativeQuery.getFirstResult());

}


@Transactional
public void updateAll()
{
Query nativeQuery = entityManager.createNativeQuery("update problem set solution =:solution");
nativeQuery.setParameter("solution","Dummy solution");
nativeQuery.executeUpdate();
}


Mappings  > OneToOne, OneToMany, ManyToOne, ManyToMany.


OneToOne

@OneToOne
private User user;


Working with oneToOne , first persist independent object then save the dependent, 
While retrieval-independent object gets automatically due to eager fetch.

To make it lazy change it to lazy fetch 

@OneToOne(fetch = FetchType.LAZY)
private User user;

Now if you try to retrieve the user object you will LazyInitilizationException, but if the whole method is inside transactional, hibernate and select the user table as well, in the earlier case session terminated after calling the find the method.



How to make bidirectional 
Add OneToOne annotation in both entities will create bidirectional which will result in duplication of fields into db, We can prevent making one entity own the relationship by adding the mapped by attribute to the other entity. this will be helpful to retrieve other entities.



Transactional [Needed when we are changing into db]

Hibernate does save the data into db after the end of the transactional scope, if anything fails inside the transactional scope everything gets a rollback, if we want to save anything in between the transactions can call the flush method,  but these changes also get rollback if at the end the transaction fails.

Does the ReadOnly method need Transactional? 

It depends upon the situation, if your entity has a relationship with other entities and has a fetch type lazy, so you need to fire a new query to find the related objects other ways to do it, by putting @Transactional over the method so that you can retrieve the other object as well.


@DirtiesContext [used to rollback the changes done inside unit test, so that other tests do not fail]

How TO fix StackOverflow error due to OneToMany relationship -https://stackoverflow.com/questions/3325387/infinite-recursion-with-jackson-json-and-hibernate-jpa-issue/18288939#18288939


OneToMany

// @JsonManagedReference
@OneToMany(mappedBy = "problem")
@ToString.Exclude
private List<Solution> solutions= new ArrayList<>();





@ManyToOne
@ToString.Exclude
private Problem problem;


here Solution class owns the relationship.
Many's side fetch strategy is lazy while one's side is eager by default.


ManyToMany

@ManyToMany(mappedBy = "problems")
@ToString.Exclude
private List<RCA> rcas = new ArrayList<>();





@ManyToMany
@ToString.Exclude
@JoinTable(name = "RCA_PROBLEM",
joinColumns = @JoinColumn(name = "RCA_ID"),
inverseJoinColumns = @JoinColumn(name = "PROBLEM_ID"))
private List<Problem> problems = new ArrayList<>();


Here new table will be created as RCA_PROBLEM which maps the problem id with rca id , and this will present at owning side of the relationship.



JPA Inheritance and Hierarchies and Mapping


Table per Hierarchy

@Entity
@Table(name = "demo_employee")
@Getter
@Setter
@NoArgsConstructor
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)// default strategy
@DiscriminatorColumn(name = "EmployeeType")
public abstract class Employee
{

@Id
@GeneratedValue
private int id;
private String name;

public Employee(String name)
{
this.name=name;
}
}






package com.jpa.h2demo.entities.inheritance;

import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.*;

import java.math.BigDecimal;


@Entity
@Table(name = "demo_full_time_employee")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class FullTimeEmployee extends Employee
{

private BigDecimal salary;

public FullTimeEmployee(BigDecimal salary, String name)
{
super(name);
this.salary = salary;
}
}






package com.jpa.h2demo.entities.inheritance;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.*;

import java.math.BigDecimal;


@Entity
@Table(name = "demo_part_time_employee")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PartTimeEmployee extends Employee
{

private BigDecimal hourlyWage;

public PartTimeEmployee( BigDecimal hourlyWage,String name)
{
super(name);
this.hourlyWage = hourlyWage;
}
}


public void insert(Employee employee)
{
entityManager.persist(employee);
}

public List<Employee> retrieveAllEmployee()
{
return entityManager.createQuery("select e from Employee e" , Employee.class).getResultList();
}





Here the problem is few columns may contain null values.


Table Per Class[per concrete class ]

The good thing with JPA is there is no change in insert and retrieve logic with this strategy there is the repetition of common columns 

Only change 
//@Inheritance(strategy = InheritanceType.SINGLE_TABLE)// default strategy
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)// default strategy
//@DiscriminatorColumn(name = "EmployeeType")
public abstract class Employee



Joined [three tables created common will go into parent and specific one will into specific tables] [good in terms of design but slow in performance]

@Setter
@NoArgsConstructor
//@Inheritance(strategy = InheritanceType.SINGLE_TABLE)// default strategy
@Inheritance(strategy = InheritanceType.JOINED)// default strategy
//@DiscriminatorColumn(name = "EmployeeType")
public abstract class Employee



No Mapping


//@Entity
@Table(name = "demo_employee")
@Getter
@Setter
@NoArgsConstructor
//@Inheritance(strategy = InheritanceType.SINGLE_TABLE)// default strategy
//@Inheritance(strategy = InheritanceType.JOINED)// default strategy
//@DiscriminatorColumn(name = "EmployeeType")
@MappedSuperclass
public abstract class Employee
{


Change into the logic of retrieving the Employees,

public List<PartTimeEmployee> retrieveAllPartTimeEmployee()
{
return entityManager.createQuery("select e from PartTimeEmployee e" , PartTimeEmployee.class).getResultList();
}

public Problem findById(long id)
{
return entityManager.find(Problem.class, id);
}

public List<Problem> findAll()
{
return entityManager.createQuery("select p from Problem p", Problem.class).getResultList();
}



Conclusion 

Table per-class and No Mapping strategy have duplicate columns which seems not design approach.

Table per hierarchy is good in terms of performance but lacks in terms of data integrity, while Joined columns are good in terms of data integrity but lack in performance.


JPAQ Complex queries Examples 


@Transactional// select * from problem where id not in (select id from problem_rca)
public List<Problem> findProblemWithoutRCA()
{
return entityManager.createQuery("select p from Problem p where p.rcas is empty ", Problem.class).getResultList();

}

@Transactional// select * FROM problem p where (select count(pr.id) from problem_rca pr where pr.id= p.id)>=2
public List<Problem> findProblemWithoutAtLeast2RCA()
{
return entityManager.createQuery("select p from Problem p where size(p.rcas) >=2 ", Problem.class).getResultList();

}
@Transactional // select * from problem p order by (select count(pr.id) from problem_rca pr where pr.id= p.id) desc
public List<Problem> findProblemsWithRcaOrderBy()
{
return entityManager.createQuery("select p from Problem p order by size(p.rcas) desc ", Problem.class).getResultList();

}

@Transactional // select * from problem p where p.name like '%some%'
public List<Problem> findProblemsWithLike()
{
return entityManager.createQuery("select p from Problem p where name like '%some%'", Problem.class).getResultList();

}
@Transactional
public void findProblemsWithJoin()
{
List<Object[]> query = entityManager.createQuery("select p ,r from Problem p join p.rcas r").getResultList();

for(Object[] query1:query)
{
System.out.println(query1[0]+" "+query1[1]);
}
}

@Transactional
public void findProblemsWithLeftJoin()
{
List<Object[]> query = entityManager.createQuery("select p ,r from Problem p left join p.rcas r").getResultList();

for(Object[] query1:query)
{
System.out.println(query1[0]+" "+query1[1]);
}
}

@Transactional
public void findProblemsWithCrossJoin()
{
List<Object[]> query = entityManager.createQuery("select p ,r from Problem p, RCA r").getResultList();

for(Object[] query1:query)
{
System.out.println(query1[0]+" "+query1[1]);
}
}




Criteria Queries 

just another way to do queries, may cover in another thread.

Transaction Management 

ACID Properties 

Automaticity - Either successfully all steps  or rollback everything gets a rollback 
Consistency - Any tractions leaves system in consistent state  
Isolation  - Every transaction should be isolated ,
Durability - Changes made by TXN should be persisted, even system fails.



Dirty Read -  Suppose there T1, and T2 are parrel transactions,  where T2 reads the values change by T1 before committing, this will leave the system in an inconsistent state if any failure happens.


Non Repeatable Read -   Reading same value again not getting same result .

select  * from product where id =10; -- T1 step-1 [ id, price - 10, 5]
update product set price= 20 where id=10; T2
select * from product where id=10; T1  step-2 [id, price , 10,20]

Phantom Read -- Changing the number of rows on the same query.


T1 step -1  >> output total 5 products 
T2 >>> inserted the 2 rows 
T1 step-2 >> output total 7 products 


Types of Isolation Level 


Isolation TypeDirty ReadRepeatable ReadPhantom Readlock
Read UncommittedNoNoNoNo
Read CommittedYesNoNoData level
Repeatable ReadYesYesNoSpecific row lock for modification
SerializationYesYesYesTable level lock which constraint



Conclusion 

Serialization guarantees all 3 problems but makes the very poor performance of database, suppose 10 thousand Transactions running parallelly.

The most commonly used one is Read Committed, It works in most of the scenarios.

Overall depending on the application, tables are specific to tables.


@Transactional -- Spring one's help into all sources like different databases,  while JPA one only solves only to a single database.

Setting isolation level at connection level by setting property value hibernate. connection.isolation=2 [for repeatable read]


Spring Data JPA --

It's an abstraction to access any kind of data, and it is jpa specific implementation, it solved the duplication of code, by just writing the interface and extending the JPARepository.


Pagination and Sort 

Sort name = Sort.by(Sort.Direction.DESC, "name").and(Sort.by(Sort.Direction.DESC,"id"));
employeeRepositorySpring.findAll(name).forEach(System.out::println);

//pagination
PageRequest of = PageRequest.of(0, 3);
Page<PartTimeEmployee> all = employeeRepositorySpring.findAll(of);
System.out.println(all.getContent());
Pageable pageable = all.nextPageable();
Page<PartTimeEmployee> all1 = employeeRepositorySpring.findAll(pageable);
System.out.println(all1);



Exposing the Repository as data rest with annotation  @RepositoryRestResource(path="nameofpath")

Hibernate and JPA Caching 


PersisteneContext->First level cache->Second Level Cache->Databse


First-level cache enables per Transaction within the boundary attached with Persistent context, while second-level cache is common for all transactions. 



Tips And Hacks 

Hard and Soft Delete 

This is an example of hard delete, which deletes the row from table.

@Transactional
public void deleteById(long id)
{
entityManager.remove(this.findById(id));
}

 
Sometimes we don't want to delete the row, instead of just marking the status as deleted.


package com.jpa.h2demo.entities.inheritance;

import jakarta.persistence.Entity;
import jakarta.persistence.PreRemove;
import jakarta.persistence.Table;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

import java.math.BigDecimal;


@Entity
@Table(name = "demo_full_time_employee")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@SQLDelete(sql = "update demo_full_time_employee set is_deleted =true where id =?")// is called on remove method call
@Where(clause = "is_deleted = false") // does not work with native need to handle by where clause , it helpfull only in jpa select query
public class FullTimeEmployee extends Employee
{

private BigDecimal salary;

public FullTimeEmployee(BigDecimal salary, String name)
{
super(name);
this.salary = salary;
}

@PreRemove // to make sure entity also reflects the is deleted true in cache
public void set()
{
isDeleted=true;
}
private boolean isDeleted;
}

here isDelete field added to achieve the same and annotation @Where and @SQLDELETE were added for the same.

in this case, just tabled entries become true on deletion, it does not work with native queries, so need to manually add the filter condition of the where clause in native query 


Similar to @PreRemove other annotation available can be used per requirement.


Embed the object as columns 


@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Address
{

private String add1;
private String add2;
}



@Embedded
private Address address;

public FullTimeEmployee(BigDecimal salary, String name,Address address)
{
super(name);
this.salary = salary;
this.address=address;
}


This will add add1, and add2 as columns into fulltimeemployee table.


Enum Usage

@Enumerated(value = EnumType.STRING)
private Rating rating;





public enum Rating
{

GOOD,
VERYGOOD,
BAD
}



We can change the enumType to ordinal in case of integer value is needed.


N+1 problem 

For one query N query gets fire ,  Like   Problem having many solution .

Solution -  Use eager annotation which will fix this but not good.

Use join.

@NamedQuery(name = "get-All", query = "select p from Problem p join fetch p.solutions s")

 Use graph 

public List<Problem> findAllWithGraph()
{

EntityGraph<Problem> entityGraph = entityManager.createEntityGraph(Problem.class);
entityGraph.addSubgraph("solutions");
return entityManager.createNamedQuery("get-All", Problem.class)
.setHint("javax.persistence.loadgraph", entityGraph)
.getResultList();
}

Use fetch join 


@Entity
@NoArgsConstructor
@AllArgsConstructor
@NamedQueries(value = {
@NamedQuery(name = "get-All", query = "select p from Problem p"),
@NamedQuery(name = "get-All-join", query = "select p from Problem p join fetch p.solutions s")
})
@Getter
@Setter
@ToString
public class Problem



Comments

Popular posts from this blog

12 Must-Know LeetCode+ Links for Coding Excellence

Stay Updated As Software Engineer