Spring
04 / 05

Spring Data JPA

Spring: Spring Data JPA

Entities & Repositories

// Entity
@Entity
@Table(name = "users", indexes = {
    @Index(columnList = "email", unique = true)
})
@EntityListeners(AuditingEntityListener.class)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    @Enumerated(EnumType.STRING)
    private UserStatus status = UserStatus.ACTIVE;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<Post> posts = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "organization_id")
    private Organization organization;
}

// Repository — Spring generates implementation at runtime
public interface UserRepository extends JpaRepository<User, Long> {

    // Derived queries — Spring generates SQL from method name
    Optional<User> findByEmail(String email);
    List<User> findByStatusAndOrganizationId(UserStatus status, Long orgId);
    boolean existsByEmail(String email);
    long countByStatus(UserStatus status);
    void deleteByEmail(String email);

    // Custom query
    @Query("SELECT u FROM User u WHERE u.name ILIKE %:term% OR u.email ILIKE %:term%")
    Page<User> search(@Param("term") String term, Pageable pageable);

    // Native SQL
    @Query(value = "SELECT * FROM users WHERE created_at > :since", nativeQuery = true)
    List<User> findRecentUsers(@Param("since") LocalDateTime since);

    // Projection — return subset of fields
    List<UserSummary> findByStatus(UserStatus status);
}

Queries & Pagination

// Service layer — use repositories
@Service
@Transactional
public class UserService {

    public Page<UserDto> getAll(int page, int size, String search) {
        var pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        var userPage = search != null
            ? userRepository.search(search, pageable)
            : userRepository.findAll(pageable);
        return userPage.map(userMapper::toDto);
    }

    // Specification (dynamic filtering)
    public List<User> filter(UserFilter filter) {
        return userRepository.findAll(
            Specification.where(UserSpec.hasStatus(filter.status()))
                .and(UserSpec.inOrganization(filter.orgId()))
                .and(UserSpec.createdAfter(filter.since()))
        );
    }
}

// Specification — composable query predicates
public class UserSpec {
    public static Specification<User> hasStatus(UserStatus status) {
        return (root, query, cb) ->
            status == null ? null : cb.equal(root.get("status"), status);
    }

    public static Specification<User> createdAfter(LocalDate date) {
        return (root, query, cb) ->
            date == null ? null : cb.greaterThan(root.get("createdAt"), date.atStartOfDay());
    }
}

// Avoid N+1 — use JOIN FETCH for associations
@Query("SELECT u FROM User u LEFT JOIN FETCH u.posts WHERE u.id = :id")
Optional<User> findByIdWithPosts(@Param("id") Long id);

// Or use @EntityGraph
@EntityGraph(attributePaths = {"posts", "organization"})
Optional<User> findWithDetailsById(Long id);

Transactions & Migrations

// @Transactional — Spring manages transaction boundaries
@Service
@Transactional(readOnly = true)   // default: read-only (optimizes SELECT)
public class UserService {

    @Transactional                // override: writable
    public User create(CreateUserRequest req) {
        var user = new User(req.name(), req.email());
        userRepository.save(user);
        emailService.sendWelcome(user);  // if this throws, user creation rolls back
        return user;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void auditLog(String action) { }  // always runs in own transaction

    @Transactional(noRollbackFor = EmailException.class)
    public void createWithoutRollingBackOnEmail(CreateUserRequest req) { }
}

// Flyway (recommended) or Liquibase — database migrations
// Add flyway-core dependency; create src/main/resources/db/migration/
// V1__init_schema.sql, V2__add_users_table.sql, V3__add_posts.sql

# application.yml
spring:
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free