Spring Data JPA removes a lot of boilerplate code by providing abstractions on top of JPA and Hibernate.
It is recommended to annotate all tables and columns with names for better readability and maintainability.
@Column(name = "example")
@Table(name = "example")- PostgreSQL →
SEQUENCE+@SequenceGenerator - MySQL / MariaDB →
IDENTITY
Choose UUIDs if:
- You’re working with distributed systems or microservices.
- Security and global uniqueness are crucial.
- Your application requires frequent data synchronization or merging.
Choose Incremental Integers if:
- Your application uses a single database.
- You prioritize readability, simplicity, and high performance.
- You have moderate security requirements.
- Very similar to the
AUTOstrategy - Generated values are unique only within a given type hierarchy (simpy table)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;❌ No batch inserts (Worse performance with many inserts)
Batch insert:
A database technique for inserting multiple rows using fewer, larger operations instead of individual inserts.
This significantly improves performance by reducing network overhead and transaction costs.
- Has batch insert - IDs are allocated in blocks to reduce database round trips
- Requires the sequence to exist in the database
- UUIDs cannot be used with sequences
⚠️ Using a single shared sequence is acceptable only for small projects with no long-term growth expectations.
✅ For scalable systems, create a separate sequence per entity.
@Entity
@SequenceGenerator(
name = "student_seq", // Alias used by @GeneratedValue
sequenceName = "student_seq", // Actual database sequence name
allocationSize = 50 // Fetch IDs in blocks of 50
)
public class Student {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "student_seq"
)
private long id;
}✔️ Best performance on PostgreSQL ✔️ Highly scalable
| Environment | allocationSize |
|---|---|
| Dev | 1 |
| Test | 10 |
| Prod | 50–100 |
| High-load | 100–1000 |
Using @Table(uniqueConstraints = ...) improves readability and maintainability.
@Table(
name = "student",
uniqueConstraints = @UniqueConstraint(
columnNames = {"email_address", "phone_number"}
)
)This is equivalent to:
@Column(unique = true)Use embeddables when you want to group fields into a reusable component without creating a separate table or entity.
@Embeddable
public class Guardian {
private String name;
private String email;
private String mobile;
}@Entity
public class Student {
@Embedded
private Guardian guardian;
}- Fields from the embedded class are stored in the same table as the owning entity
- Improves domain modeling and code reuse
- The owning side contains the foreign key (FK) and
@JoinColumnannotation - The other side is only a mirror of the relationship (
mappedBy) and not define the FK - Relationships are optional by default, which means you can create a
Coursewithout aCourseMaterial
To make the relationship mandatory:
@OneToOne(optional = false)@Entity
@Table(name = "course_material")
public class CourseMaterial {
@Id
@SequenceGenerator(...)
@GeneratedValue(...)
private Long id;
@OneToOne(cascade = CascadeType.ALL) // Cascading
@JoinColumn(
name = "course_id", // FK column name
referencedColumnName = "courseId" // Target column (by default targets PK)
)
private Course course;
}@OneToOne(cascade = CascadeType.ALL)Cascading allows you to save multiple entities with a single save operation.
Use it when the foreign key references a column other than the primary key.
@ManyToOne
@JoinColumn(
name = "user_email",
referencedColumnName = "email"
)
private User user;@Entity
@Table(name = "course")
public class Course {
@Id
@SequenceGenerator(...)
@GeneratedValue(...)
private Long courseId;
@OneToOne(mappedBy = "course")
private CourseMaterial courseMaterial;
}mappedBy = "course" tells JPA:
"This side is NOT the owner of the relationship. The relationship is managed by the
coursefield inCourseMaterial."
Apparently it's always better to make @ManyToOne instead @OneToMany when possible
- Simpler mapping
- Better performance
@Entity
@Table(name = "course")
public class Course {
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(
name = "teacher_id",
referencedColumnName = "teacher_id"
)
private Teacher teacher;
}- Using
@OneToManyonTeacherresults in a FK column in theCoursetable - You will not see courses stored in the
Teachertable, instead, theCoursetable contains a reference toTeacher
@Entity
@Table(name = "teacher")
public class Teacher {
@Id
@Column(name = "teacher_id")
private Long teacherId;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(
name = "teacher_id_name_for_course", // FK column name in Course table
referencedColumnName = "teacher_id" // PK column in Teacher table
)
private List<Course> courseList;
}- Requires a join table to store the relationship
- The join table maps IDs from both sides of the association
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(
name = "student_course_map",
joinColumns = @JoinColumn(
name = "course_id", // FK for this entity
referencedColumnName = "courseId"
),
inverseJoinColumns = @JoinColumn(
name = "student_id", // FK for the related entity (Student)
referencedColumnName = "studentId"
)
)
private List<Student> students;public void addStudent(Student student) {
if (studentList == null) {
studentList = new ArrayList<>();
}
studentList.add(student);
}FetchType.EAGER– related entity is fetched immediately using a joinFetchType.LAZY– related entity is loaded only when accessed
@OneToOne(fetch = FetchType.LAZY)EAGER fetching — it may cause performance issues and unexpected joins.
Pageable firstPageWithThreeRecords = PageRequest.of(pageNumber: 0, pageSize: 3);
List<Course> courseList = courseRepository
.findAll(firstPageWithThreeRecords)
.getContent();
System.out.println("Courses = " + courseList);
long totalElements = courseRepository
.findAll(firstPageWithThreeRecords)
.getTotalElements();
System.out.println("totalElements = " + totalElements);
long totalPages = courseRepository
.findAll(firstPageWithThreeRecords)
.getTotalPages();
// totalPages depends on page size and total record count
System.out.println("totalPages = " + totalPages);
Pageable sortByTitle = PageRequest.of(0, 2, Sort.by("title"));
Pageable sortByCreditDesc = PageRequest.of(
0, 2, Sort.by("credit").descending()
);
Pageable sortByTitleAndCreditDesc = PageRequest.of(
0, 2,
Sort.by("title").descending().and(Sort.by("credit"))
);
List<Course> courses = courseRepository
.findAll(sortByTitleAndCreditDesc)
.getContent();
System.out.println(courses);Page<Course> findByTitleContaining(String title, Pageable pageable);Pageable sortByTitle = PageRequest.of(0, 2, Sort.by("title"));
System.out.println(
courseRepository
.findByTitleContaining("kurs", sortByTitle)
.getContent()
);By extending Spring Data JPA repository interfaces, you get CRUD operations and pagination support out-of-the-box.
You can also define custom queries using method names or the @Query annotation.
JPA Query Method Reference
- Spring Data generates queries based on method names automatically.
Optional<User> findByUsername(String username);
Optional<List<Student>> findByFirstName(String name); // Find records matching the exact name
List<Student> findByGuardianName(String name); // Find records in @Embedded class
boolean existsByUsername(String username);- Use JPQL (class-based query language)
@Query("SELECT f FROM Foo f WHERE LOWER(f.name) = LOWER(:name)")
Foo retrieveByName(@Param("name") String name);
@Query("SELECT s.firstName FROM Student s WHERE s.emailId = ?1") // get only firstName
String getStudentFirstNameByEmailAddress(String emailId);Notes:
Studentrefers to the entity class, not the table?1,?2refer to method parameters by index:namerefer to@Paramvariable
- Use actual database table and column names
WARNING
JPA Auditing @CreatedDate @ModificationDate isn't working for native queries
@Query(
value = "SELECT * FROM student s WHERE s.email_address = ?1",
nativeQuery = true
)
Student getStudentByEmailAddressNative(String emailId);@Query(
value = "SELECT * FROM student s WHERE s.email_address = :emailId",
nativeQuery = true
)
Student getStudentByEmailAddressNativeNamedParam(@Param("emailId") String emailId);-
@Queryis treated asSELECTwithout@Modifying -
Transactions ensure all-or-nothing behavior (rollback on exception
-
Inserts/updates/deletes must be transactional and annotated with
@Modifying -
Repository methods do not have transactions by default
Let's assume the exception is thrown after succeeding 1) and before executing 2).
Now we would have some kind of inconsistency because A lost 100$ while B got nothing.
Transactions means all or nothing.
If there is an exception thrown somewhere in the method, changes are not persisted in the database.
Rollback happens.
@Transactional(readOnly = true)
interface UserRepository extends JpaRepository<User, Long> {
List<User> findByLastname(String lastname);
@Modifying
@Transactional
@Query("DELETE FROM User u WHERE u.active = false")
void deleteInactiveUsers();
@Modifying
@Transactional // by default readOnly = false
@Query(
value = "UPDATE student SET first_name = :firstName WHERE email_address = :emailId",
nativeQuery = true
)
int updateStudentNameByEmailId(String firstName, String emailId);
}readOnly = truefor queries that only read data@Modifyingoverrides this for update/delete operations
- It's for repository testing which won't impact real database.
- Focused on the repository layer without loading the full application context
- Uses in-memory database (H2) by default
@DataJpaTest
public class StudentRepositoryTest {
@Autowired
StudentRepository studentRepository;
@Test
@DisplayName("Find student by id")
void givenStudentID_whenQuery_thenGetStudentObject() {
Student student = studentRepository.getReferenceById(1L);
}
}Dependencies:
spring-boot-starter-data-jpa-testspring-boot-starter-testH2 database
- Executes code after application context is loaded
- Useful for populating test/development data
@Component
@RequiredArgsConstructor
public class StudentDataLoader implements CommandLineRunner {
private final StudentRepository studentRepository;
@Override
public void run(String... args) throws Exception {
studentRepository.save(new Student("Adam", "Grant", "Email"));
studentRepository.save(new Student("Edward", "Grant", "EmailFajny"));
}
}