Modern Persistence with Spring Data JPA
While Hibernate is an excellent JPA implementation, modern applications—especially those built with Spring Boot—use Spring Data JPA to significantly simplify the data access layer. Spring Data JPA acts as an abstraction layer on top of a JPA provider like Hibernate, reducing boilerplate code to a minimum.
This guide provides a complete, runnable example of a Spring Boot application using Spring Data JPA with an SQLite database.
Why Spring Data JPA?
- Eliminates Boilerplate: You no longer need to write DAO/repository implementations. Simply define an interface, and Spring Data JPA generates the implementation at runtime.
- Derived Queries: Create complex queries just by defining a method signature in your repository interface (e.g.,
findBySubjectAndSentDateAfter(...)). - Simple Configuration: Database connections and JPA settings are managed centrally in
application.properties. - Easy Integration: Seamlessly integrates with the rest of the Spring ecosystem.
Complete Runnable Example with SQLite
This example can be created as a new Maven project in Eclipse. Just copy the files into the correct locations.
1. Project Structure
A typical Maven project structure would look like this:
spring-data-jpa-sqlite/
├── pom.xml
└── src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── jpa/
│ │ ├── Application.java
│ │ ├── Email.java
│ │ ├── EmailRepository.java
│ │ └── dialect/
│ │ └── SQLiteDialect.java
│ └── resources/
│ └── application.properties
└── test/
2. pom.xml
This file defines all the necessary dependencies, including the Spring Boot starter for JPA and the JDBC driver for SQLite.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Spring Boot Parent provides dependency management -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version> <!-- Or any recent stable version -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>spring-data-jpa-sqlite</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-data-jpa-sqlite</name>
<description>Demo project for Spring Boot with JPA and SQLite</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<!-- Core dependency for Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- JDBC Driver for SQLite -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.39.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3. src/main/resources/application.properties
This file configures the application, including the database connection. The database file (mydatabase.db) will be created automatically in the project's root directory.
# SQLite Datasource Configuration
spring.datasource.url=jdbc:sqlite:mydatabase.db
spring.datasource.driver-class-name=org.sqlite.JDBC
# Specify the custom dialect for SQLite
spring.jpa.database-platform=com.example.jpa.dialect.SQLiteDialect
# Show SQL statements in the console for debugging
spring.jpa.show-sql=true
# Hibernate ddl-auto (create, create-drop, validate, update)
# 'create-drop' is useful for testing as the schema is dropped on shutdown.
spring.jpa.hibernate.ddl-auto=create-drop
4. Custom Hibernate Dialect for SQLite
Hibernate does not have a built-in dialect for SQLite. You must provide one. Create this class exactly as shown below.
File: src/main/java/com/example/jpa/dialect/SQLiteDialect.java
package com.example.jpa.dialect;
import java.sql.Types;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.identity.IdentityColumnSupport;
/**
* Custom Hibernate Dialect for SQLite to ensure compatibility.
*/
public class SQLiteDialect extends Dialect {
public SQLiteDialect() {
registerColumnType(Types.BIT, "integer");
registerColumnType(Types.TINYINT, "tinyint");
registerColumnType(Types.SMALLINT, "smallint");
registerColumnType(Types.INTEGER, "integer");
registerColumnType(Types.BIGINT, "bigint");
registerColumnType(Types.FLOAT, "float");
registerColumnType(Types.REAL, "real");
registerColumnType(Types.DOUBLE, "double");
registerColumnType(Types.NUMERIC, "numeric");
registerColumnType(Types.DECIMAL, "decimal");
registerColumnType(Types.CHAR, "char");
registerColumnType(Types.VARCHAR, "varchar");
registerColumnType(Types.LONGVARCHAR, "longvarchar");
registerColumnType(Types.DATE, "date");
registerColumnType(Types.TIME, "time");
registerColumnType(Types.TIMESTAMP, "timestamp");
registerColumnType(Types.BINARY, "blob");
registerColumnType(Types.VARBINARY, "blob");
registerColumnType(Types.LONGVARBINARY, "blob");
registerColumnType(Types.BLOB, "blob");
registerColumnType(Types.CLOB, "clob");
registerColumnType(Types.BOOLEAN, "integer");
}
@Override
public IdentityColumnSupport getIdentityColumnSupport() {
return new SQLiteIdentityColumnSupport();
}
@Override
public boolean hasAlterTable() {
return false;
}
@Override
public boolean dropConstraints() {
return false;
}
@Override
public String getDropForeignKeyString() {
return "";
}
@Override
public String getAddForeignKeyConstraintString(String constraintName, String[] foreignKey, String referencedTable,
String[] primaryKey, boolean referencesPrimaryKey) {
return "";
}
@Override
public String getAddPrimaryKeyConstraintString(String constraintName) {
return "";
}
}
Note: A helper class SQLiteIdentityColumnSupport is needed by the dialect.
package com.example.jpa.dialect;
import org.hibernate.dialect.identity.IdentityColumnSupportImpl;
public class SQLiteIdentityColumnSupport extends IdentityColumnSupportImpl {
@Override
public boolean supportsIdentityColumns() {
return true;
}
@Override
public String getIdentitySelectString(String table, String column, int type) {
return "select last_insert_rowid()";
}
@Override
public String getIdentityColumnString(int type) {
return "integer";
}
}
5. JPA Entity
This is the Email object that will be mapped to a database table.
File: src/main/java/com/example/jpa/Email.java
package com.example.jpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Date;
@Entity
public class Email {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String subject;
private String body;
private Date sentDate;
// Constructors
public Email() {}
public Email(String subject, String body, Date sentDate) {
this.subject = subject;
this.body = body;
this.sentDate = sentDate;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getSubject() { return subject; }
public void setSubject(String subject) { this.subject = subject; }
public String getBody() { return body; }
public void setBody(String body) { this.body = body; }
public Date getSentDate() { return sentDate; }
public void setSentDate(Date sentDate) { this.sentDate = sentDate; }
@Override
public String toString() {
return "Email{" +
"id=" + id +
", subject='" + subject + "'" +
", body='" + body + "'" +
", sentDate=" + sentDate +
'}';
}
}
6. Spring Data JPA Repository
This is the magic of Spring Data JPA. Simply define an interface that extends JpaRepository. You get full CRUD functionality for free.
File: src/main/java/com/example/jpa/EmailRepository.java
package com.example.jpa;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface EmailRepository extends JpaRepository<Email, Long> {
// Example of a derived query: Spring Data JPA will automatically
// implement this method based on its name.
List<Email> findBySubject(String subject);
}
7. Main Application and Demo Runner
The main class to start the Spring Boot application. It includes a CommandLineRunner which is a simple way to execute code on startup. We use it here to save and retrieve data to demonstrate that everything is working.
File: src/main/java/com/example/jpa/Application.java
package com.example.jpa;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.util.Date;
import java.util.List;
@SpringBootApplication
public class Application {
private static final Logger log = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public CommandLineRunner demo(EmailRepository repository) {
return (args) -> {
// 1. Save a couple of emails
log.info("Saving a few emails...");
repository.save(new Email("Project Update", "The project is on track.", new Date()));
repository.save(new Email("Team Lunch", "Reminder: team lunch tomorrow at 12pm.", new Date()));
repository.save(new Email("Project Update", "A second project update.", new Date()));
// 2. Fetch all emails
log.info("Emails found with findAll():");
log.info("-------------------------------");
for (Email email : repository.findAll()) {
log.info(email.toString());
}
log.info("");
// 3. Fetch an individual email by ID
Email email = repository.findById(1L).get();
log.info("Email found with findById(1L):");
log.info("--------------------------------");
log.info(email.toString());
log.info("");
// 4. Fetch emails by subject using our derived query
log.info("Email found with findBySubject('Project Update'):");
log.info("--------------------------------------------");
List<Email> projectUpdates = repository.findBySubject("Project Update");
projectUpdates.forEach(update -> log.info(update.toString()));
log.info("");
};
}
}
After setting up these files, you can run the Application.java class as a Java Application in Eclipse, and you will see the log output in the console demonstrating the database interactions.