Key Takeaways
- Spring Data JPA supplies repository abstractions; Jakarta Persistence defines the standard API; Hibernate is a provider that implements it.
- Repository save() selects persist or merge using entity new-state detection. It isn't an atomic database upsert.
- Relationship ownership, cascade behavior, and fetching are separate decisions. mappedBy identifies the owning Java attribute.
- Current repository interfaces separate CRUD from paging and sorting. JpaRepository combines those capabilities with JPA-specific operations.
- @Query uses JPQL by default. Efficient data access still requires deliberate fetch plans, transaction boundaries, and SQL inspection.
Spring Data JPA reduces boilerplate in relational data access by implementing repository interfaces on top of Jakarta Persistence and a provider such as Hibernate. These Spring Data JPA interview questions focus on explaining that stack, mapping entities correctly, and understanding what your repository calls actually do.
If you need the foundation first, start with the Spring Data JPA tutorial.
Spring Data JPA, JPA, and Hibernate: What Does Each Do?
What does JPA stand for?
JPA originally stood for Java Persistence API; the specification is now called Jakarta Persistence. It standardizes persistence and object-relational mapping for Java applications. Jakarta Persistence 3.0 moved the persistence API packages from javax.persistence to jakarta.persistence.
What is the difference between Spring Data JPA and Hibernate?
Spring Data JPA is a data-access abstraction, not a JPA provider. It supplies repository infrastructure and common data-access operations. Hibernate implements Jakarta Persistence and handles entity state, relational mapping, and database interaction. Spring Boot's JPA starter includes both Spring Data JPA and Hibernate.
Application / repository interface
|
Spring Data JPA
|
Jakarta Persistence API
|
Provider, such as Hibernate
|
Relational databaseFor a longer comparison, see Spring Data JPA vs Hibernate.
What is ORM, and which frameworks provide it?
Object-relational mapping, or ORM, maps an object model and its relationships to relational database structures. Hibernate, EclipseLink, and Apache OpenJPA are examples of JPA providers. Check provider compatibility with your persistence API rather than assuming providers are interchangeable across generations.
MyBatis is a SQL mapper: you define SQL or stored-procedure mappings to Java objects. Don't classify it as another JPA provider.
Which Spring Data JPA Annotations Should You Understand?
What does @Entity do?
@Entity declares a persistent entity type. It doesn't mean that the class must map to one identically named table; defaults and mapping annotations determine its relational mapping. Annotating a class also doesn't make every instance managed: an instance must enter a persistence context, the provider's working set of managed entities.
What does @Id do, and does it generate identifiers?
@Id identifies a primary-key field or property. Identifier generation is separate: @GeneratedValue requests generated identifiers, and its default strategy is AUTO. With a numeric identifier, the provider chooses an appropriate strategy; don't assume identical sequence or identity-column behavior across databases.
The examples below place mapping annotations on fields, selecting field access. With property access, mapping annotations go on getters instead. Keep annotation placement consistent unless you explicitly configure access.
Is @Column required?
No. Omit @Column when its defaults are sufficient. It can customize a column's name, nullability, or length. The default logical column name comes from the persistent field or property, but provider naming strategies can affect the physical database name.
What Does mappedBy Mean in an Author–Books Relationship?
mappedBy belongs on the inverse side of a bidirectional relationship and names the owning side's Java field or property—not a database column. Here, Book.author owns the relationship through books.author_id; Author.books is inverse, with mappedBy = "author".
These application fragments belong in separate files in an existing Spring Boot JPA project with a configured database and entity/repository scanning.
// Application fragment: Author.java
package example.interview;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Entity
@Table(name = "authors")
public class Author {
@Id
@GeneratedValue
private Long id;
private String name; // @Column defaults are sufficient here.
@OneToMany(mappedBy = "author",
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
orphanRemoval = true)
private List<Book> books = new ArrayList<>();
protected Author() {}
public Author(String name) {
this.name = Objects.requireNonNull(name);
}
public Book addBook(String title) {
Book book = new Book(title, this);
books.add(book);
return book;
}
public String getName() { return name; }
public List<Book> getBooks() { return books; }
}// Application fragment: Book.java
package example.interview;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.Objects;
@Entity
@Table(name = "books")
public class Book {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String title;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "author_id", nullable = false)
private Author author;
protected Book() {}
Book(String title, Author author) {
this.title = Objects.requireNonNull(title);
this.author = Objects.requireNonNull(author);
}
public void renameTo(String title) {
this.title = Objects.requireNonNull(title);
}
public String getTitle() { return title; }
public Author getAuthor() { return author; }
}addBook() updates both Java sides. Updating only the inverse collection doesn't establish the owning-side reference. For another mapping walkthrough, see the Spring Data JPA one-to-many example.
How are ownership, cascade, and orphan removal different?
- Ownership: determines which side controls the stored relationship.
- Cascade: propagates selected persistence operations to associated entities. In this example, persist and merge propagate from Author to its books.
- Orphan removal: removes a managed child removed from the relationship, with deletion applied at flush.
mappedBy doesn't enable cascading. Cascades default to none, and orphan removal defaults to false. Enable orphan removal only when the child genuinely belongs to the parent's lifecycle.
CrudRepository vs JpaRepository: What Is the Current Hierarchy?
CrudRepository belongs to Spring Data, not Jakarta Persistence. Its scope includes saving, finding by ID, checking existence, counting, and deleting entities.
| Interface | Capabilities | Interview distinction |
|---|---|---|
| CrudRepository | Basic CRUD operations | Multi-result methods such as findAll() return Iterable. |
| ListCrudRepository | CRUD with list-returning multi-result methods | Extends CrudRepository. |
| PagingAndSortingRepository | findAll(Pageable) and findAll(Sort) | Doesn't itself provide CRUD operations. |
| ListPagingAndSortingRepository | Paging and list-returning sorted retrieval | Extends PagingAndSortingRepository. |
| JpaRepository | List CRUD, paging/sorting, query-by-example, and JPA-specific operations | Adds methods such as flush(), saveAndFlush(), and deleteAllInBatch(). |
Since Spring Data 3.0, sorting repositories no longer extend their CRUD counterparts. JpaRepository directly extends ListCrudRepository, ListPagingAndSortingRepository, and QueryByExampleExecutor. You don't need to extend all the interfaces yourself.
Do you need @EnableJpaRepositories?
@EnableJpaRepositories configures repository discovery. In a standard auto-configured Spring Boot application, repositories in the auto-configuration packages are discovered without it. Explicit configuration is useful for custom scan locations or multiple persistence units. Spring creates repository implementations at runtime; you usually write the interface, not its CRUD implementation.
Which Spring Data JPA version should you use?
For a Boot application, start with the version selected by Spring Boot's dependency management rather than independently upgrading the provider and Spring Data. Boot manages a coordinated dependency set. Check the Spring Data JPA project page for releases, then check your resolved dependencies and matching documentation.
How Does save() Work in Spring Data JPA?
save() uses entity new-state detection to choose EntityManager.persist() or EntityManager.merge(). The default strategy first checks a non-primitive version attribute: a null version means new. Without one, it checks the identifier: a null ID means new. An entity implementing Persistable can supply its own isNew() result.
This matters for assigned IDs: a new object with a non-null ID and no suitable version attribute is considered not new by the default strategy. A database row-existence check isn't the contract. Always use the value returned by save(), because merge can return a different managed instance.
save() is not an atomic SQL upsert. Merge copies state into a managed instance; it isn't simply an SQL UPDATE command. Also distinguish flushing from committing: saveAndFlush() synchronizes pending changes with the database, but doesn't itself commit the surrounding transaction.
The Spring Data JPA CRUD example provides more practice with repository operations.
How Do Derived Queries, JPQL, and Native SQL Differ?
A derived query expresses a predicate through the repository method name. @Query declares JPQL by default, using entity names and Java attributes. Setting nativeQuery = true switches to database SQL, using table and column names.
// Application fragment: BookRepository.java
package example.interview;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface BookRepository extends JpaRepository<Book, Long> {
// Derived query.
List<Book> findByTitle(String title);
// JPQL: Book is an entity; title is a Java attribute.
@Query("select b from Book b where b.title = :title")
List<Book> findUsingJpql(@Param("title") String title);
// Native SQL: books and title are database identifiers.
@Query(value = "select b.* from books b where b.title = :title",
nativeQuery = true)
List<Book> findUsingSql(@Param("title") String title);
// Fetch the author as part of this use case's query.
@Query("select b from Book b join fetch b.author where b.title = :title")
List<Book> findWithAuthorByTitle(@Param("title") String title);
}@Param binds method arguments to named query parameters. Prefer derived methods for simple predicates, JPQL for entity-oriented queries, and native SQL when database-specific features justify the portability and result-mapping costs. Bind input values rather than concatenating them into query text.
FetchType.LAZY vs EAGER: What Is Actually Guaranteed?
Under Jakarta Persistence 3.2, the relationship defaults are:
| Relationship | Default fetch type |
|---|---|
| @ManyToOne and @OneToOne | EAGER |
| @OneToMany and @ManyToMany | LAZY |
EAGER requires eager fetching; it doesn't require a SQL join or a single query. LAZY is a hint permitting deferred loading, and the provider may fetch eagerly instead. Lazy loading can use proxies or bytecode enhancement, depending on the mapping and provider.
Why does lazy loading cause N+1 queries?
In the classic case, one query loads N books and N additional queries load their distinct, uncached authors. Iterating through lazy associations can trigger that pattern; eager associations loaded with secondary selects can cause it too. Changing everything to EAGER isn't a fetch plan.
The repository's join fetch b.author requests the association in that query. Another option is @EntityGraph(attributePaths = "author"), which requests an association fetch plan without embedding the join in the query text. An entity graph doesn't promise a particular SQL shape—inspect what your provider executes.
With Hibernate, accessing an uninitialized lazy association after its persistence context closes can throw LazyInitializationException. Fetch the data you need within the unit of work, then map it to a response object if it must leave that boundary.
What changes when you add pagination?
A Page exposes totals and may require a count query. A Slice reports whether more results exist without calculating the overall total. Deep offset pagination can become expensive. Use stable sorting with a unique tie-breaker, and consider keyset scrolling for large sequential reads.
Don't casually combine collection fetch joins with pagination: Hibernate can fetch all matching rows and apply the limit in memory. For paged parent collections, consider paging parent IDs first and fetching the required associations separately.
Where Should Transaction Boundaries Go?
Inherited CRUD methods have transaction configuration: read operations are read-only by default, while writes use ordinary transactions. Declared query methods don't automatically inherit that configuration. Put a service-level transaction around a business operation that reads and changes data, rather than assuming separate repository calls form one transaction.
// Application fragment: BookService.java
package example.interview;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BookService {
private final BookRepository books;
public BookService(BookRepository books) {
this.books = books;
}
@Transactional
public void renameBook(Long id, String title) {
Book book = books.findById(id)
.orElseThrow(() ->
new IllegalArgumentException("Book not found: " + id));
book.renameTo(title);
}
}Here, the loaded book remains managed during the transaction. Its changed state is synchronized through dirty checking; an explicit save() isn't required for that managed update. A transaction and a persistence context aren't identical concepts: Open EntityManager in View can keep a context open beyond a repository transaction.
Common Mistakes
- Memorizing annotations without explaining the SQL and transaction implications.
- Updating an inverse collection but forgetting the owning-side reference.
- Using EAGER everywhere to hide lazy-loading errors.
- Calling a bulk delete as though it had ordinary entity lifecycle behavior.
- Returning unbounded result sets from request-facing repository operations.
For interview preparation, trace one request from service to repository to SQL. In production, inspect query counts, execution plans, and indexes with realistic data. Hibernate supports DEBUG logging for org.hibernate.SQL to show executed SQL.
FAQ
Is Spring Data JPA an implementation of JPA?
No. Spring Data JPA provides repository abstractions on top of Jakarta Persistence. A provider such as Hibernate implements the persistence specification.
What is the difference between CrudRepository and JpaRepository?
CrudRepository provides basic CRUD operations. JpaRepository combines list-returning CRUD, paging and sorting, query-by-example, and JPA-specific operations such as flush().
Does save() in Spring Data JPA perform an upsert?
No atomic database upsert is guaranteed. Spring Data JPA uses entity new-state detection to choose persist or merge; use the returned entity because merge may return a different managed instance.
Is @Query JPQL or SQL by default?
@Query uses JPQL by default, referring to entity names and Java attributes. Set nativeQuery = true to execute database SQL.
Can Spring Data JPA work with non-relational databases?
Spring Data JPA targets relational databases through Jakarta Persistence. Other Spring Data projects, including Spring Data MongoDB and Spring Data Redis, support other stores.
Further Reading
- Jakarta Persistence specification — mapping, ownership, entity state, and fetch semantics.
- Defining repository interfaces — CRUD and sorting interface relationships.
- JpaRepository API — JPA-specific methods and bulk-operation caveats.
- Persisting entities — save() and new-state detection.
- JPA query methods — derived queries, parameter binding, and entity graphs.
- Repository transactionality — defaults and service-level units of work.
- Spring Boot SQL database integration — JPA setup, scanning, and web persistence contexts.
- Hibernate ORM user guide — fetching, generated SQL, and provider behavior.
