Store and query data without writing SQL boilerplate — map entities with JPA/Hibernate, get CRUD for free from a repository, and write derived and custom queries.
Why: spring-boot-starter-data-jpa brings in Hibernate (the JPA implementation that turns objects into SQL). Note: add a database driver too — H2 is an in-memory database, perfect for learning because it needs zero setup and resets on restart.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>Why: @Entity marks a class as a database table; each field becomes a column. When @Id + @GeneratedValue: the primary key, auto-assigned by the database. Note: Spring Boot can auto-create the schema from your entities with spring.jpa.hibernate.ddl-auto=update (use migrations in production).
import jakarta.persistence.*;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private double price;
protected Book() {} // JPA needs a no-arg constructor
// getters / setters ...
}Why: extend JpaRepository and Spring generates the implementation at runtime — save, findById, findAll, delete, count, paging, all without code. Where: an interface, never a class. Note: it becomes an injectable bean automatically.
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, Long> {
// inherited for free: save, findById, findAll, deleteById, count, ...
}
@Service
public class BookService {
private final BookRepository books;
public BookService(BookRepository books) { this.books = books; }
public Book create(Book b) { return books.save(b); }
public List<Book> all() { return books.findAll(); }
}Why: name a method following Spring Data’s grammar and it writes the query for you — findBy, And, Or, Between, OrderBy, Containing. Note: the method name IS the query, so findByAuthorAndPriceLessThan generates exactly that WHERE clause.
public interface BookRepository extends JpaRepository<Book, Long> {
List<Book> findByAuthor(String author);
List<Book> findByAuthorAndPriceLessThan(String author, double max);
List<Book> findByTitleContainingIgnoreCase(String fragment);
List<Book> findTop5ByOrderByPriceDesc();
}When @Query: the derived-name grammar gets awkward, or you want an explicit query. Note: write JPQL (queries over entities, not tables) by default, or set nativeQuery = true for raw SQL. Bind parameters with :name.
public interface BookRepository extends JpaRepository<Book, Long> {
@Query("select b from Book b where b.price between :min and :max")
List<Book> inPriceRange(double min, double max);
@Query(value = "SELECT * FROM book WHERE author = :author",
nativeQuery = true)
List<Book> rawByAuthor(String author);
}Why: never load an entire table. Note: accept a Pageable in the repository and Spring adds LIMIT/OFFSET and ORDER BY for you; the returned Page carries the rows plus total count and page metadata. Build it with PageRequest.of(page, size, sort).
import org.springframework.data.domain.*;
public interface BookRepository extends JpaRepository<Book, Long> {
Page<Book> findByAuthor(String author, Pageable pageable);
}
// in a service / controller
Pageable page = PageRequest.of(0, 20, Sort.by("price").descending());
Page<Book> result = books.findByAuthor("Tolkien", page);
result.getContent(); // the 20 rows
result.getTotalElements(); // total matching count