Before comparing between JPA and JDBC, lets see the journey from JDBC to JPA. For that we will first see the non-spring JDBC, then Spring JDBC, then Hibernate and finally Spring Data JPA.
2 decades ago when we started our java programming journey we used to write pure JDBC calls to connect to database. As the time passes by, we migrated to Hibernate, and now we use JPA in the real world applications.
JDBC Vs Spring JDBC:
In the earlier days (pre-spring boot era) we used to write the JDBC code something like below:
Connection connection = datasource.getConnection(); PreparedStatement st = connection.prepareStatement( "SELECT * FROM TODO where user=?"); st.setString(1, user); ResultSet resultSet = st.executeQuery(); List<Todo> todos = new ArrayList<>(); while (resultSet.next()) { Todo todo = new Todo(resultSet.getInt("id"), resultSet.getString("user"), resultSet.getString("desc"), resultSet.getTimestamp("target_date"), resultSet.getBoolean("is_done")); todos.add(todo); } st.close(); connection.close();
And, here is the sample Spring JDBC code:
@Repository public class PersonJbdcDao { @Autowired JdbcTemplate jdbcTemplate; public List<Person> someName() { return jdbcTemplate.query("select * from person", new BeanPropertyRowMapper(Person.class)); }
So below are the benefits of using the Spring JDBC:
- Lesser code.
- Makes you to do less mistakes- you don’t have to worry for connection closing, exception handling, etc.
Lets do a demo for Spring JDBC first.
In the subsequent section we will see Hibernate and then JPA.
Spring JDBC demo:
Lets create an Spring Boot project with below dependencies from Spring Initializr:
- Spring Web
- JDBC
- H2
Since we are using H2 in-memory database, add below content in application.properties:
spring.h2.console.enabled=true spring.datasource.url=jdbc:h2:mem:testdb spring.data.jpa.repositories.bootstrap-mode=default
Run the main java class. You can see this URL in the log: /h2-console
So hit this url: http://localhost:8080/h2-console
Verify that the JDBC URL given as jdbc:h2:mem:testdb
Username: sa
Password:
Click Connect.
You will see the h2-console:
Now if you see there is no tables in the database.
Now, we will use the spring auto-configuration feature to create some table and populate some data to the database. For that, Create a file called data.sql in the /resources folder and add below contents to it:
create table person ( id integer not null, name varchar(255), birth_date timestamp, primary key(id) );
Once you restart the application you can see the Person table in the database, although you won’t see any data in it.
If you want to populate some rows to this table then you can add some insert queries to the same data.sql file like below:
create table person ( id integer not null, name varchar(255), birth_date timestamp, primary key(id) ); insert into person values(1001,'Prasanna',sysdate()); insert into person values(1002,'Padma',sysdate()); insert into person values(1003,'Sarojini',sysdate());
P.S.: Everytime you restart the application the previous data will be gone, because its an in-memory database.
Now lets write some Spring JDBC stuffs which you are looking for right now:
Create an entity class: Person.java. Make sure to have an no-arg constructor otherwise it will give run time error.
public class Person { private int id; private String name; private Date birthDate; public Person() { } public Person(int id, String name, Date birthDate) { super(); this.id = id; this.name = name; this.birthDate = 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 Date getBirthDate() { return birthDate; } public void setBirthDate(Date birthDate) { this.birthDate = birthDate; } @Override public String toString() { return "\n Person [id=" + id + ", name=" + name + ", birthDate=" + birthDate + "]"; } }
Create a Dao class, give any name to the method:
@Repository public class PersonJdbcDao { @Autowired JdbcTemplate jdbcTemplate; public List findAll() { return jdbcTemplate.query("select * from Person", new BeanPropertyRowMapper(Person.class)); } public Person findById(int id) { return jdbcTemplate.queryForObject("select * from Person where id= ?", new Object[] {id} ,new BeanPropertyRowMapper<Person>(Person.class)); } }
Here we had used query() and queryForObject() of JdbcTemplate to fetch all and individual records respectively.
BeanPropertyRowMapper is used to convert a table row into a new instance of the specified mapped target class.
In the main class:
@SpringBootApplication public class HibernateJpaApplication implements CommandLineRunner{ private Logger logger=LoggerFactory.getLogger(this.getClass()); @Autowired PersonJdbcDao dao; public static void main(String[] args) { SpringApplication.run(HibernateJpaApplication.class, args); } @Override public void run(String... args) throws Exception { //logger.info("All users ---> {}" + dao.findAll()); logger.info("User ---> {}" + dao.findById(1001)); } }
ApplicationRunner and CommandLineRunner interfaces will lets you to execute the code after the Spring Boot application is started. You can use these interfaces to perform any actions immediately after the application has started.
The above code setup will fetch records from the table, using Spring JDBC way.
Springboot Autoconfiguration:
In the above demo we had just @Autowired the JdbcTemplate, but how does Spring know about the database connection- like what and where is the database etc.
This is the magic of Spring Boot Auto Configuration- it looks for what are available in its classpath and automatically configures that.
To see the AUTO-CONFIGURATION REPORT in the log just add below line in application.properties:
logging.level.root=debug
Then you will see below lines of code in the console:
Spring Boot JPA
Now lets see the Spring Boot JPA demo.
To replace the Spring JDBC code with JPA code, below changes you need to do:
Add below dependency to the project (available in Spring Initializr):
- Spring Data JPA
By adding above dependency, these 3 dependencies are added- JPA, Hibernate and Spring Data JPA
Add below annotations to the bean:
@Entity – Its like bean++
@Id – to make it primary key
@GeneratedValue – to auto generate the id.
@Entity public class Person { @Id @GeneratedValue private int id; private String name; private String location;
And create a new repository class:
@Repository @Transactional public class PersonJpaRepository { // connect to the database @PersistenceContext EntityManager entityManager; public Person findById(int id) { return entityManager.find(Person.class, id);// JPA }
EntityManager is similar to Session in Hibernate. The way we write session.save() in Hibernate we will call entityManager.persist() in JPA.
If you are using Spring Boot 2.5.0 or Greater, add the following property to application.properties
spring.jpa.defer-datasource-initialization=true
Why do you need to make the change?
By default, data.sql scripts are now run before Hibernate is initialized. This aligns the behaviour of basic script-based initialization with that of Flyway and Liquibase. If you want to use data.sql to populate a schema created by Hibernate, set spring.jpa.defer-datasource-initialization to true. While mixing database initialization technologies is not recommended, this will also allow you to use a schema.sql script to build upon a Hibernate-created schema before itβs populated via data.sql.
Now if you run the application you will get an error saying: Person table already exists. Why so ?
Because, Schema Update which is a Hibernate feature, will automatically create a table for us for an in-memory database. Schema Update will be triggered by Springboot auto-configuration. Springboot sees @Entity annotation and do all these magic.
So, remove the create table query from data.sql file. But you have to keep the insert table query in data.sql as it is.
So, below will be the data.sql:
insert into person(id,name,birth_date) values(1001,'Prasanna',sysdate()); insert into person(id,name,birth_date) values(1002,'Padma',sysdate()); insert into person(id,name,birth_date) values(1003,'Sarojini',sysdate());
If you want to see the SQL query in the console that is fired, add this in application.properties:
spring.jpa.show-sql=true
P.S.: There is a lot of confusion between JPA and Hibernate. JPA is an specifications, its not an implementation. Hibernate is an implementation of JPA. Spring JPA is another implementation of JPA, it came to the market long after Hibernate became popular. Whenever you add spring-boot-starter-data-jpa (Spring Data JPA) artifactId from Spring Initialzr, you get both hibernate-core and spring-data-jpa artifactId in your code. In all the examples in this section we would be talking about JPA, but internally we would be doing Hibernate calls. In the logs also you can see Hibernate instances everywhere like below:
org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
Although the style of code that we had used is not specific to Hibernate, its rather like JPA style, for eg., here we used EntityManager instead of Session, PersistenceContext instead of SessionFactory.
Later part of this tutorial series we will see how to use Spring Data JPA as well.
But the question is, what is the benefit of using JPA style of code if we are using hibernate internally, why not hibernate ? The answer is, if tomorrow if we want to change our ORM to some other vendor than hibernate then it won’t have any problem.
Implementing other CRUD operations in JPA repository:
Implementing insert and update JPA repository:
For implementing insert and update to a JPA repository you need to call a single method called – merge()
Below would be the implementation:
public Person2 insertUpdate(Person2 person) { return entityManager.merge(person); }
The object that you are passing, JPA will first check whether that object exists in the DB, if it doesn’t find it it will insert, else it will update.
Alternatively, for insert only you can use persist() as well.
Implement deleteById in JPA repository:
You need to use this method: remove(). Just call your findById() inside this method.
public void deleteById(int id) { entityManager.remove(findById(id)); }
Implement findAll in JPARepository
For this you need to use HQL (Hibernate Query Language), infact JPQL(Jpa Persistent Query Language). In JPQL you will be using a NamedQuery.
public List findAll() { return entityManager.createNamedQuery("find_all").getResultList(); }
In the Person entity class you need to add below annotation at the top:
@NamedQuery(name="find_all", query="select p from Person2 p")
More about EntityManager & PersistenceContext
EntityManager is an interface to the PersistenceContext, its like Session in Hibernate. And PersistenceContext is an block of memory which keeps track of all entities, its like a SessionFactory in Hibernate.
What is the difference between @PersistenceContext and EntityManagerFactory ?
Both are the factory, in the former one the container takes care of the lifecycle of the bean and in the later one the programmer (you) takes care of the lifecycle.
@PersistenceContext(unitName ="some name") private EntityManager entityManager;
EntityManagerFactory emf = Persistence.createEntityManagerFactory("some name"); EntityManager em = emf.createEntityManager();
In the above steps we had seen that what is the usage of persist() method. Basically its for saving any data to DB.
Suppose we call the persist() method and after that you change some value in the object. Do you think the value will be updated in the DB, although you have not requested to persist the data ?
Person person=new Person("Bangalore",new Date()); entityManager.persist(person); person.setName("Some updated name");
Yes, it would be saved. This magic happened because the EntityManager. When EntityManager sees the @Transactional in the Entity class it do all things in a transaction
One can see the below 2 lines in the console log, once we execute the above code:
Hibernate: insert into person (birth_date, name, id) values (?, ?, ?) Hibernate: update person set birth_date=?, name=? where id=?
flush()
flush() will do explicit commit of the changes.
Below 2 block of code- one with and other without the flush(), will give the same result:
Person person=new Person("Bangalore",new Date()); entityManager.persist(person); person.setName("some updated name");
Person person=new Person("Bangalore",new Date()); entityManager.persist(person); person.setName("some updated name"); entityManager.flush();
Why? The first block will commit the changes once the transaction is complete. The 2nd block will also do the commit on the call of the flush() along with the completion of transaction.
detach()
When you detach some object from the EntityManager session, it wouldn’t be part of the DB changes. Before doing detach(), try to flush() other changes, else you will get runtime exception.
Lets see the below code, here we are basically doing 4 things- persisting person1 (and flushing it), updating person1 (and flushing it), persisting person2 (and flushing it), updating person2 (and flushing it). Before doing the 4th task we are doing detach(). Can you guess the result ?
Person person=new Person("Tom 1",new Date()); entityManager.persist(person); entityManager.flush(); //will save successfully person.setName("Tom 11"); entityManager.flush(); //will update successfully Person person2=new Person("Tom 2",new Date()); entityManager.persist(person2); entityManager.flush(); //will save successfully entityManager.detach(person2); //detached person2.setName("Tom 22"); //this data will be lost entityManager.flush(); //Will not save.
Of course, the 4th task (updation) will not work.
clear()
This works similar to detach(), it will remove any object from the Entity Manager object. The only difference which I have seen is it it works fine without the use of the flush().
refresh()
This will fetch data from the DB and will override the changes on the object.