diff --git a/persistence-modules/spring-data-jpa-annotations/README.md b/persistence-modules/spring-data-jpa-annotations/README.md
new file mode 100644
index 0000000000..5f1c8dbc27
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/README.md
@@ -0,0 +1,23 @@
+## Spring Data JPA - Annotations
+
+This module contains articles about annotations used in Spring Data JPA
+
+### Relevant articles
+
+- [Spring Data Annotations](https://www.baeldung.com/spring-data-annotations)
+- [DDD Aggregates and @DomainEvents](https://www.baeldung.com/spring-data-ddd)
+- [JPA @Embedded And @Embeddable](https://www.baeldung.com/jpa-embedded-embeddable)
+- [Spring Data JPA @Modifying Annotation](https://www.baeldung.com/spring-data-jpa-modifying-annotation)
+- [Spring JPA @Embedded and @EmbeddedId](https://www.baeldung.com/spring-jpa-embedded-method-parameters)
+- [Programmatic Transaction Management in Spring](https://www.baeldung.com/spring-programmatic-transaction-management)
+- [JPA Entity Lifecycle Events](https://www.baeldung.com/jpa-entity-lifecycle-events)
+
+### Eclipse Config
+After importing the project into Eclipse, you may see the following error:
+"No persistence xml file found in project"
+
+This can be ignored:
+- Project -> Properties -> Java Persistance -> JPA -> Error/Warnings -> Select Ignore on "No persistence xml file found in project"
+Or:
+- Eclipse -> Preferences - Validation - disable the "Build" execution of the JPA Validator
+
diff --git a/persistence-modules/spring-data-jpa-annotations/pom.xml b/persistence-modules/spring-data-jpa-annotations/pom.xml
new file mode 100644
index 0000000000..67b788c404
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/pom.xml
@@ -0,0 +1,77 @@
+
+
+ 4.0.0
+ spring-data-jpa-annotations
+ spring-data-jpa-annotations
+
+
+ com.baeldung
+ parent-boot-2
+ 0.0.1-SNAPSHOT
+ ../../parent-boot-2
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.hibernate
+ hibernate-ehcache
+
+
+ org.hibernate
+ hibernate-envers
+
+
+
+ com.h2database
+ h2
+
+
+
+
+ org.testcontainers
+ postgresql
+ ${testcontainers.postgresql.version}
+ test
+
+
+
+ org.postgresql
+ postgresql
+
+
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+
+
+ com.baeldung.boot.Application
+ 1.10.6
+ 42.2.5
+ 21.0
+
+
+
\ No newline at end of file
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/Application.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/Application.java
new file mode 100644
index 0000000000..3ea3d113da
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/Application.java
@@ -0,0 +1,13 @@
+package com.baeldung;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate.java
new file mode 100644
index 0000000000..e435f4c85c
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate.java
@@ -0,0 +1,46 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Transient;
+
+import org.springframework.context.ApplicationEventPublisher;
+
+@Entity
+class Aggregate {
+ @Transient
+ private ApplicationEventPublisher eventPublisher;
+ @Id
+ private long id;
+
+ private Aggregate() {
+ }
+
+ Aggregate(long id, ApplicationEventPublisher eventPublisher) {
+ this.id = id;
+ this.eventPublisher = eventPublisher;
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "DomainEntity [id=" + id + "]";
+ }
+
+ void domainOperation() {
+ // some business logic
+ if (eventPublisher != null) {
+ eventPublisher.publishEvent(new DomainEvent());
+ }
+ }
+
+ long getId() {
+ return id;
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate2.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate2.java
new file mode 100644
index 0000000000..08f61db812
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate2.java
@@ -0,0 +1,44 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.Transient;
+
+import org.springframework.data.domain.AfterDomainEventPublication;
+import org.springframework.data.domain.DomainEvents;
+
+@Entity
+public class Aggregate2 {
+ @Transient
+ private final Collection domainEvents;
+ @Id
+ @GeneratedValue
+ private long id;
+
+ public Aggregate2() {
+ domainEvents = new ArrayList<>();
+ }
+
+ @AfterDomainEventPublication
+ public void clearEvents() {
+ domainEvents.clear();
+ }
+
+ public void domainOperation() {
+ // some domain operation
+ domainEvents.add(new DomainEvent());
+ }
+
+ @DomainEvents
+ public Collection events() {
+ return domainEvents;
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate2Repository.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate2Repository.java
new file mode 100644
index 0000000000..7f09c410fc
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate2Repository.java
@@ -0,0 +1,10 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+import org.springframework.data.repository.CrudRepository;
+
+public interface Aggregate2Repository extends CrudRepository {
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate3.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate3.java
new file mode 100644
index 0000000000..f664322a59
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate3.java
@@ -0,0 +1,23 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+
+import org.springframework.data.domain.AbstractAggregateRoot;
+
+@Entity
+public class Aggregate3 extends AbstractAggregateRoot {
+ @Id
+ @GeneratedValue
+ private long id;
+
+ public void domainOperation() {
+ // some domain operation
+ registerEvent(new DomainEvent());
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate3Repository.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate3Repository.java
new file mode 100644
index 0000000000..93f50bb5cf
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/Aggregate3Repository.java
@@ -0,0 +1,14 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * @author goobar
+ *
+ */
+public interface Aggregate3Repository extends CrudRepository {
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/AggregateRepository.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/AggregateRepository.java
new file mode 100644
index 0000000000..1c2d3884bf
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/AggregateRepository.java
@@ -0,0 +1,10 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+import org.springframework.data.repository.CrudRepository;
+
+public interface AggregateRepository extends CrudRepository {
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/DddConfig.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/DddConfig.java
new file mode 100644
index 0000000000..34cf21463b
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/DddConfig.java
@@ -0,0 +1,15 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.context.annotation.PropertySource;
+
+@SpringBootConfiguration
+@EnableAutoConfiguration
+@PropertySource("classpath:/ddd.properties")
+public class DddConfig {
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/DomainEvent.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/DomainEvent.java
new file mode 100644
index 0000000000..e7626e742d
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/DomainEvent.java
@@ -0,0 +1,8 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+class DomainEvent {
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/DomainService.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/DomainService.java
new file mode 100644
index 0000000000..80aa5ca6cb
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/boot/ddd/event/DomainService.java
@@ -0,0 +1,31 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+import javax.transaction.Transactional;
+
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DomainService {
+ private final ApplicationEventPublisher eventPublisher;
+ private final AggregateRepository repository;
+
+ public DomainService(AggregateRepository repository, ApplicationEventPublisher eventPublisher) {
+ this.repository = repository;
+ this.eventPublisher = eventPublisher;
+ }
+
+ @Transactional
+ public void serviceDomainOperation(long entityId) {
+ repository.findById(entityId)
+ .ifPresent(entity -> {
+ entity.domainOperation();
+ repository.save(entity);
+ eventPublisher.publishEvent(new DomainEvent());
+ });
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/BookApplication.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/BookApplication.java
new file mode 100644
index 0000000000..52f06058aa
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/BookApplication.java
@@ -0,0 +1,12 @@
+package com.baeldung.composite;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class BookApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(BookApplication.class);
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/entity/Book.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/entity/Book.java
new file mode 100644
index 0000000000..e4f1727654
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/entity/Book.java
@@ -0,0 +1,47 @@
+package com.baeldung.composite.entity;
+
+import javax.persistence.EmbeddedId;
+import javax.persistence.Entity;
+
+@Entity
+public class Book {
+
+ @EmbeddedId
+ private BookId id;
+ private String genre;
+ private Integer price;
+
+ public Book() {
+ }
+
+ public Book(String author, String name, String genre, Integer price) {
+ BookId id = new BookId(author, name);
+ this.id = id;
+ this.genre = genre;
+ this.price = price;
+ }
+
+ public BookId getId() {
+ return id;
+ }
+
+ public void setId(BookId id) {
+ this.id = id;
+ }
+
+ public String getGenre() {
+ return genre;
+ }
+
+ public void setGenre(String genre) {
+ this.genre = genre;
+ }
+
+ public Integer getPrice() {
+ return price;
+ }
+
+ public void setPrice(Integer price) {
+ this.price = price;
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/entity/BookId.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/entity/BookId.java
new file mode 100644
index 0000000000..1524452412
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/entity/BookId.java
@@ -0,0 +1,51 @@
+package com.baeldung.composite.entity;
+
+import javax.persistence.Embeddable;
+import java.io.Serializable;
+import java.util.Objects;
+
+@Embeddable
+public class BookId implements Serializable {
+
+ private String author;
+ private String name;
+
+ public BookId() {
+ }
+
+ public BookId(String author, String name) {
+ this.author = author;
+ this.name = name;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ BookId bookId = (BookId) o;
+ return Objects.equals(author, bookId.author) && Objects.equals(name, bookId.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(author, name);
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/repository/BookRepository.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/repository/BookRepository.java
new file mode 100644
index 0000000000..d5993eaf79
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/composite/repository/BookRepository.java
@@ -0,0 +1,18 @@
+package com.baeldung.composite.repository;
+
+import com.baeldung.composite.entity.Book;
+import com.baeldung.composite.entity.BookId;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface BookRepository extends JpaRepository {
+
+ List findByIdName(String name);
+
+ List findByIdAuthor(String author);
+
+ List findByGenre(String genre);
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/embeddable/model/Company.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/embeddable/model/Company.java
new file mode 100644
index 0000000000..203cff1e35
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/embeddable/model/Company.java
@@ -0,0 +1,71 @@
+package com.baeldung.embeddable.model;
+
+import javax.persistence.AttributeOverride;
+import javax.persistence.AttributeOverrides;
+import javax.persistence.Column;
+import javax.persistence.Embedded;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+
+@Entity
+public class Company {
+
+ @Id
+ @GeneratedValue
+ private Integer id;
+
+ private String name;
+
+ private String address;
+
+ private String phone;
+
+ @Embedded
+ @AttributeOverrides(value = {
+ @AttributeOverride( name = "firstName", column = @Column(name = "contact_first_name")),
+ @AttributeOverride( name = "lastName", column = @Column(name = "contact_last_name")),
+ @AttributeOverride( name = "phone", column = @Column(name = "contact_phone"))
+ })
+ private ContactPerson contactPerson;
+
+ public Integer getId() {
+ return id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public ContactPerson getContactPerson() {
+ return contactPerson;
+ }
+
+ public void setContactPerson(ContactPerson contactPerson) {
+ this.contactPerson = contactPerson;
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/embeddable/model/ContactPerson.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/embeddable/model/ContactPerson.java
new file mode 100644
index 0000000000..561da80878
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/embeddable/model/ContactPerson.java
@@ -0,0 +1,38 @@
+package com.baeldung.embeddable.model;
+
+import javax.persistence.Embeddable;
+
+@Embeddable
+public class ContactPerson {
+
+ private String firstName;
+
+ private String lastName;
+
+ private String phone;
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/embeddable/repositories/CompanyRepository.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/embeddable/repositories/CompanyRepository.java
new file mode 100644
index 0000000000..f456b15652
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/embeddable/repositories/CompanyRepository.java
@@ -0,0 +1,18 @@
+package com.baeldung.embeddable.repositories;
+
+import com.baeldung.embeddable.model.Company;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.List;
+
+public interface CompanyRepository extends JpaRepository {
+
+ List findByContactPersonFirstName(String firstName);
+
+ @Query("SELECT C FROM Company C WHERE C.contactPerson.firstName = ?1")
+ List findByContactPersonFirstNameWithJPQL(String firstName);
+
+ @Query(value = "SELECT * FROM company WHERE contact_first_name = ?1", nativeQuery = true)
+ List findByContactPersonFirstNameWithNativeQuery(String firstName);
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/SpringBootLifecycleEventApplication.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/SpringBootLifecycleEventApplication.java
new file mode 100644
index 0000000000..fbc861c5fe
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/SpringBootLifecycleEventApplication.java
@@ -0,0 +1,11 @@
+package com.baeldung.lifecycleevents;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SpringBootLifecycleEventApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootLifecycleEventApplication.class, args);
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/model/AuditTrailListener.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/model/AuditTrailListener.java
new file mode 100644
index 0000000000..26ebff42e4
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/model/AuditTrailListener.java
@@ -0,0 +1,39 @@
+package com.baeldung.lifecycleevents.model;
+
+import javax.persistence.PostLoad;
+import javax.persistence.PostPersist;
+import javax.persistence.PostRemove;
+import javax.persistence.PostUpdate;
+import javax.persistence.PrePersist;
+import javax.persistence.PreRemove;
+import javax.persistence.PreUpdate;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public class AuditTrailListener {
+ private static Log log = LogFactory.getLog(AuditTrailListener.class);
+
+ @PrePersist
+ @PreUpdate
+ @PreRemove
+ private void beforeAnyUpdate(User user) {
+ if (user.getId() == 0) {
+ log.info("[USER AUDIT] About to add a user");
+ } else {
+ log.info("[USER AUDIT] About to update/delete user: " + user.getId());
+ }
+ }
+
+ @PostPersist
+ @PostUpdate
+ @PostRemove
+ private void afterAnyUpdate(User user) {
+ log.info("[USER AUDIT] add/update/delete complete for user: " + user.getId());
+ }
+
+ @PostLoad
+ private void afterLoad(User user) {
+ log.info("[USER AUDIT] user loaded from database: " + user.getId());
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/model/User.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/model/User.java
new file mode 100644
index 0000000000..a080cb3bf2
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/model/User.java
@@ -0,0 +1,104 @@
+package com.baeldung.lifecycleevents.model;
+
+import javax.persistence.Entity;
+import javax.persistence.EntityListeners;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.PostLoad;
+import javax.persistence.PostPersist;
+import javax.persistence.PostRemove;
+import javax.persistence.PostUpdate;
+import javax.persistence.PrePersist;
+import javax.persistence.PreRemove;
+import javax.persistence.PreUpdate;
+import javax.persistence.Transient;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+@Entity
+@EntityListeners(AuditTrailListener.class)
+public class User {
+ private static Log log = LogFactory.getLog(User.class);
+
+ @Id
+ @GeneratedValue
+ private int id;
+
+ private String userName;
+ private String firstName;
+ private String lastName;
+ @Transient
+ private String fullName;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getFullName() {
+ return fullName;
+ }
+
+ @PrePersist
+ public void logNewUserAttempt() {
+ log.info("Attempting to add new user with username: " + userName);
+ }
+
+ @PostPersist
+ public void logNewUserAdded() {
+ log.info("Added user '" + userName + "' with ID: " + id);
+ }
+
+ @PreRemove
+ public void logUserRemovalAttempt() {
+ log.info("Attempting to delete user: " + userName);
+ }
+
+ @PostRemove
+ public void logUserRemoval() {
+ log.info("Deleted user: " + userName);
+ }
+
+ @PreUpdate
+ public void logUserUpdateAttempt() {
+ log.info("Attempting to update user: " + userName);
+ }
+
+ @PostUpdate
+ public void logUserUpdate() {
+ log.info("Updated user: " + userName);
+ }
+
+ @PostLoad
+ public void logUserLoad() {
+ fullName = firstName + " " + lastName;
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/repository/UserRepository.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/repository/UserRepository.java
new file mode 100644
index 0000000000..af14117ebb
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/lifecycleevents/repository/UserRepository.java
@@ -0,0 +1,9 @@
+package com.baeldung.lifecycleevents.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import com.baeldung.lifecycleevents.model.User;
+
+public interface UserRepository extends JpaRepository {
+ public User findByUserName(String userName);
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/tx/TxApplication.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/tx/TxApplication.java
new file mode 100644
index 0000000000..4c982c91e9
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/tx/TxApplication.java
@@ -0,0 +1,13 @@
+package com.baeldung.tx;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class TxApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(TxApplication.class, args);
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/tx/model/Payment.java b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/tx/model/Payment.java
new file mode 100644
index 0000000000..921a1e9275
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/java/com/baeldung/tx/model/Payment.java
@@ -0,0 +1,55 @@
+package com.baeldung.tx.model;
+
+import javax.persistence.*;
+
+@Entity
+public class Payment {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ private Long amount;
+
+ @Column(unique = true)
+ private String referenceNumber;
+
+ @Enumerated(EnumType.STRING)
+ private State state;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getAmount() {
+ return amount;
+ }
+
+ public void setAmount(Long amount) {
+ this.amount = amount;
+ }
+
+ public String getReferenceNumber() {
+ return referenceNumber;
+ }
+
+ public void setReferenceNumber(String referenceNumber) {
+ this.referenceNumber = referenceNumber;
+ }
+
+ public State getState() {
+ return state;
+ }
+
+ public void setState(State state) {
+ this.state = state;
+ }
+
+ public enum State {
+ STARTED, FAILED, SUCCESSFUL
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/resources/application.properties b/persistence-modules/spring-data-jpa-annotations/src/main/resources/application.properties
new file mode 100644
index 0000000000..f127dd5e50
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/resources/application.properties
@@ -0,0 +1,6 @@
+spring.main.allow-bean-definition-overriding=true
+
+spring.jpa.properties.hibernate.jdbc.batch_size=4
+spring.jpa.properties.hibernate.order_inserts=true
+spring.jpa.properties.hibernate.order_updates=true
+spring.jpa.properties.hibernate.generate_statistics=true
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/resources/ddd.properties b/persistence-modules/spring-data-jpa-annotations/src/main/resources/ddd.properties
new file mode 100644
index 0000000000..e5126b694b
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/resources/ddd.properties
@@ -0,0 +1 @@
+spring.datasource.initialization-mode=never
\ No newline at end of file
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/resources/logback.xml b/persistence-modules/spring-data-jpa-annotations/src/main/resources/logback.xml
new file mode 100644
index 0000000000..7d900d8ea8
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/resources/logback.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/persistence-modules/spring-data-jpa-annotations/src/main/resources/persistence.properties b/persistence-modules/spring-data-jpa-annotations/src/main/resources/persistence.properties
new file mode 100644
index 0000000000..6bc83edf34
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/main/resources/persistence.properties
@@ -0,0 +1,14 @@
+# jdbc.X
+jdbc.driverClassName=org.h2.Driver
+jdbc.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS USERS
+jdbc.user=sa
+jdbc.pass=sa
+
+# hibernate.X
+hibernate.dialect=org.hibernate.dialect.H2Dialect
+hibernate.show_sql=true
+hibernate.hbm2ddl.auto=create-drop
+hibernate.cache.use_second_level_cache=true
+hibernate.cache.use_query_cache=true
+hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory
+
diff --git a/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/Aggregate2EventsIntegrationTest.java b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/Aggregate2EventsIntegrationTest.java
new file mode 100644
index 0000000000..e76b932cb9
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/Aggregate2EventsIntegrationTest.java
@@ -0,0 +1,72 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import com.baeldung.boot.ddd.event.Aggregate2;
+import com.baeldung.boot.ddd.event.Aggregate2Repository;
+import com.baeldung.boot.ddd.event.DomainEvent;
+
+@SpringJUnitConfig
+@SpringBootTest
+class Aggregate2EventsIntegrationTest {
+ @MockBean
+ private TestEventHandler eventHandler;
+ @Autowired
+ private Aggregate2Repository repository;
+
+ // @formatter:off
+ @DisplayName("given aggregate with @AfterDomainEventPublication,"
+ + " when do domain operation and save twice,"
+ + " then an event is published only for the first time")
+ // @formatter:on
+ @Test
+ void afterDomainEvents() {
+ // given
+ Aggregate2 aggregate = new Aggregate2();
+
+ // when
+ aggregate.domainOperation();
+ repository.save(aggregate);
+ repository.save(aggregate);
+
+ // then
+ verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ repository.deleteAll();
+ }
+
+ // @formatter:off
+ @DisplayName("given aggregate with @DomainEvents,"
+ + " when do domain operation and save,"
+ + " then an event is published")
+ // @formatter:on
+ @Test
+ void domainEvents() {
+ // given
+ Aggregate2 aggregate = new Aggregate2();
+
+ // when
+ aggregate.domainOperation();
+ repository.save(aggregate);
+
+ // then
+ verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/Aggregate3EventsIntegrationTest.java b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/Aggregate3EventsIntegrationTest.java
new file mode 100644
index 0000000000..4193e932ee
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/Aggregate3EventsIntegrationTest.java
@@ -0,0 +1,67 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import com.baeldung.boot.ddd.event.Aggregate3;
+import com.baeldung.boot.ddd.event.Aggregate3Repository;
+import com.baeldung.boot.ddd.event.DomainEvent;
+
+@SpringJUnitConfig
+@SpringBootTest
+class Aggregate3EventsIntegrationTest {
+
+ @MockBean
+ private TestEventHandler eventHandler;
+ @Autowired
+ private Aggregate3Repository repository;
+
+ // @formatter:off
+ @DisplayName("given aggregate extending AbstractAggregateRoot,"
+ + " when do domain operation and save twice,"
+ + " then an event is published only for the first time")
+ // @formatter:on
+ @Test
+ void afterDomainEvents() {
+ // given
+ Aggregate3 aggregate = new Aggregate3();
+
+ // when
+ aggregate.domainOperation();
+ repository.save(aggregate);
+ repository.save(aggregate);
+
+ // then
+ verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
+ }
+
+ // @formatter:off
+ @DisplayName("given aggregate extending AbstractAggregateRoot,"
+ + " when do domain operation and save,"
+ + " then an event is published")
+ // @formatter:on
+ @Test
+ void domainEvents() {
+ // given
+ Aggregate3 aggregate = new Aggregate3();
+
+ // when
+ aggregate.domainOperation();
+ repository.save(aggregate);
+
+ // then
+ verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/AggregateEventsIntegrationTest.java b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/AggregateEventsIntegrationTest.java
new file mode 100644
index 0000000000..ac607063b2
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/AggregateEventsIntegrationTest.java
@@ -0,0 +1,87 @@
+package com.baeldung.boot.ddd.event;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import com.baeldung.boot.ddd.event.Aggregate;
+import com.baeldung.boot.ddd.event.AggregateRepository;
+import com.baeldung.boot.ddd.event.DomainEvent;
+import com.baeldung.boot.ddd.event.DomainService;
+
+@SpringJUnitConfig
+@SpringBootTest
+class AggregateEventsIntegrationTest {
+
+ @Autowired
+ private DomainService domainService;
+
+ @MockBean
+ private TestEventHandler eventHandler;
+ @Autowired
+ private ApplicationEventPublisher eventPublisher;
+ @Autowired
+ private AggregateRepository repository;
+
+ // @formatter:off
+ @DisplayName("given existing aggregate,"
+ + " when do domain operation directly on aggregate,"
+ + " then domain event is NOT published")
+ // @formatter:on
+ @Test
+ void aggregateEventsTest() {
+ Aggregate existingDomainEntity = new Aggregate(0, eventPublisher);
+ repository.save(existingDomainEntity);
+
+ // when
+ repository.findById(existingDomainEntity.getId())
+ .get()
+ .domainOperation();
+
+ // then
+ verifyZeroInteractions(eventHandler);
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ repository.deleteAll();
+ }
+
+ // @formatter:off
+ @DisplayName("given existing aggregate,"
+ + " when do domain operation on service,"
+ + " then domain event is published")
+ // @formatter:on
+ @Test
+ void serviceEventsTest() {
+ Aggregate existingDomainEntity = new Aggregate(1, eventPublisher);
+ repository.save(existingDomainEntity);
+
+ // when
+ domainService.serviceDomainOperation(existingDomainEntity.getId());
+
+ // then
+ verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
+ }
+
+ @TestConfiguration
+ public static class TestConfig {
+ @Bean
+ public DomainService domainService(AggregateRepository repository, ApplicationEventPublisher eventPublisher) {
+ return new DomainService(repository, eventPublisher);
+ }
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/TestEventHandler.java b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/TestEventHandler.java
new file mode 100644
index 0000000000..0f499834eb
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/boot/ddd/event/TestEventHandler.java
@@ -0,0 +1,14 @@
+/**
+ *
+ */
+package com.baeldung.boot.ddd.event;
+
+import org.springframework.transaction.event.TransactionalEventListener;
+
+import com.baeldung.boot.ddd.event.DomainEvent;
+
+interface TestEventHandler {
+ @TransactionalEventListener
+ void handleEvent(DomainEvent event);
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/composite/repository/BookRepositoryIntegrationTest.java b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/composite/repository/BookRepositoryIntegrationTest.java
new file mode 100644
index 0000000000..9d25acbd96
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/composite/repository/BookRepositoryIntegrationTest.java
@@ -0,0 +1,64 @@
+package com.baeldung.composite.repository;
+
+import com.baeldung.composite.BookApplication;
+import com.baeldung.composite.entity.Book;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = BookApplication.class)
+public class BookRepositoryIntegrationTest {
+
+ public static final String JAVA_101 = "Java101";
+ public static final String JANE = "Jane";
+ public static final String TECH = "Tech";
+ @Autowired
+ BookRepository repository;
+
+ @Before
+ public void setUp() {
+ Book book1 = new Book("John", JAVA_101, TECH, 20);
+ Book book2 = new Book(JANE, JAVA_101, "Arch", 25);
+ Book book3 = new Book(JANE, "Scala101", TECH, 23);
+
+ repository.saveAll(Arrays.asList(book1, book2, book3));
+ }
+
+ @After
+ public void tearDown() {
+ repository.deleteAll();
+ }
+
+ @Test
+ public void testFindByName() {
+ List books = repository.findByIdName(JAVA_101);
+
+ assertEquals(2, books.size());
+ }
+
+ @Test
+ public void testFindByAuthor() {
+ List books = repository.findByIdAuthor(JANE);
+
+ assertEquals(2, books.size());
+ }
+
+ @Test
+ public void testFindByGenre() {
+ List books = repository.findByGenre(TECH);
+
+ assertEquals(2, books.size());
+ }
+}
\ No newline at end of file
diff --git a/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/embeddable/EmbeddableIntegrationTest.java b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/embeddable/EmbeddableIntegrationTest.java
new file mode 100644
index 0000000000..b4c365a2d9
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/embeddable/EmbeddableIntegrationTest.java
@@ -0,0 +1,125 @@
+package com.baeldung.embeddable;
+
+import com.baeldung.Application;
+import com.baeldung.embeddable.model.Company;
+import com.baeldung.embeddable.model.ContactPerson;
+import com.baeldung.embeddable.repositories.CompanyRepository;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = {Application.class})
+public class EmbeddableIntegrationTest {
+
+ @Autowired
+ private CompanyRepository companyRepository;
+
+ @Test
+ @Transactional
+ public void whenInsertingCompany_thenEmbeddedContactPersonDetailsAreMapped() {
+ ContactPerson contactPerson = new ContactPerson();
+ contactPerson.setFirstName("First");
+ contactPerson.setLastName("Last");
+ contactPerson.setPhone("123-456-789");
+
+ Company company = new Company();
+ company.setName("Company");
+ company.setAddress("1st street");
+ company.setPhone("987-654-321");
+ company.setContactPerson(contactPerson);
+
+ companyRepository.save(company);
+
+ Company result = companyRepository.getOne(company.getId());
+
+ assertEquals("Company", result.getName());
+ assertEquals("1st street", result.getAddress());
+ assertEquals("987-654-321", result.getPhone());
+ assertEquals("First", result.getContactPerson().getFirstName());
+ assertEquals("Last", result.getContactPerson().getLastName());
+ assertEquals("123-456-789", result.getContactPerson().getPhone());
+ }
+
+ @Test
+ @Transactional
+ public void whenFindingCompanyByContactPersonAttribute_thenCompanyIsReturnedProperly() {
+ ContactPerson contactPerson = new ContactPerson();
+ contactPerson.setFirstName("Name");
+ contactPerson.setLastName("Last");
+ contactPerson.setPhone("123-456-789");
+
+ Company company = new Company();
+ company.setName("Company");
+ company.setAddress("1st street");
+ company.setPhone("987-654-321");
+ company.setContactPerson(contactPerson);
+
+ companyRepository.save(company);
+
+ List result = companyRepository.findByContactPersonFirstName("Name");
+
+ assertEquals(1, result.size());
+
+ result = companyRepository.findByContactPersonFirstName("FirstName");
+
+ assertEquals(0, result.size());
+ }
+
+ @Test
+ @Transactional
+ public void whenFindingCompanyByContactPersonAttributeWithJPQL_thenCompanyIsReturnedProperly() {
+ ContactPerson contactPerson = new ContactPerson();
+ contactPerson.setFirstName("@QueryName");
+ contactPerson.setLastName("Last");
+ contactPerson.setPhone("123-456-789");
+
+ Company company = new Company();
+ company.setName("Company");
+ company.setAddress("1st street");
+ company.setPhone("987-654-321");
+ company.setContactPerson(contactPerson);
+
+ companyRepository.save(company);
+
+ List result = companyRepository.findByContactPersonFirstNameWithJPQL("@QueryName");
+
+ assertEquals(1, result.size());
+
+ result = companyRepository.findByContactPersonFirstNameWithJPQL("FirstName");
+
+ assertEquals(0, result.size());
+ }
+
+ @Test
+ @Transactional
+ public void whenFindingCompanyByContactPersonAttributeWithNativeQuery_thenCompanyIsReturnedProperly() {
+ ContactPerson contactPerson = new ContactPerson();
+ contactPerson.setFirstName("NativeQueryName");
+ contactPerson.setLastName("Last");
+ contactPerson.setPhone("123-456-789");
+
+ Company company = new Company();
+ company.setName("Company");
+ company.setAddress("1st street");
+ company.setPhone("987-654-321");
+ company.setContactPerson(contactPerson);
+
+ companyRepository.save(company);
+
+ List result = companyRepository.findByContactPersonFirstNameWithNativeQuery("NativeQueryName");
+
+ assertEquals(1, result.size());
+
+ result = companyRepository.findByContactPersonFirstNameWithNativeQuery("FirstName");
+
+ assertEquals(0, result.size());
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/tx/ManualTransactionIntegrationTest.java b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/tx/ManualTransactionIntegrationTest.java
new file mode 100644
index 0000000000..01551348c9
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/test/java/com/baeldung/tx/ManualTransactionIntegrationTest.java
@@ -0,0 +1,152 @@
+package com.baeldung.tx;
+
+import com.baeldung.tx.model.Payment;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+import org.springframework.transaction.support.TransactionCallbackWithoutResult;
+import org.springframework.transaction.support.TransactionTemplate;
+
+import javax.persistence.EntityManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.transaction.annotation.Propagation.NOT_SUPPORTED;
+
+@DataJpaTest
+@ActiveProfiles("test")
+@Transactional(propagation = NOT_SUPPORTED)
+class ManualTransactionIntegrationTest {
+
+ @Autowired
+ private PlatformTransactionManager transactionManager;
+
+ @Autowired
+ private EntityManager entityManager;
+
+ private TransactionTemplate transactionTemplate;
+
+ @BeforeEach
+ void setUp() {
+ transactionTemplate = new TransactionTemplate(transactionManager);
+ }
+
+ @AfterEach
+ void flushDb() {
+ transactionTemplate.execute(status -> entityManager
+ .createQuery("delete from Payment")
+ .executeUpdate());
+ }
+
+ @Test
+ void givenAPayment_WhenNotDuplicate_ThenShouldCommit() {
+ Long id = transactionTemplate.execute(status -> {
+ Payment payment = new Payment();
+ payment.setAmount(1000L);
+ payment.setReferenceNumber("Ref-1");
+ payment.setState(Payment.State.SUCCESSFUL);
+
+ entityManager.persist(payment);
+
+ return payment.getId();
+ });
+
+ Payment payment = entityManager.find(Payment.class, id);
+ assertThat(payment).isNotNull();
+ }
+
+ @Test
+ void givenAPayment_WhenMarkAsRollback_ThenShouldRollback() {
+ transactionTemplate.execute(status -> {
+ Payment payment = new Payment();
+ payment.setAmount(1000L);
+ payment.setReferenceNumber("Ref-1");
+ payment.setState(Payment.State.SUCCESSFUL);
+
+ entityManager.persist(payment);
+ status.setRollbackOnly();
+
+ return payment.getId();
+ });
+
+ assertThat(entityManager
+ .createQuery("select p from Payment p", Payment.class)
+ .getResultList()).isEmpty();
+ }
+
+ @Test
+ void givenTwoPayments_WhenRefIsDuplicate_ThenShouldRollback() {
+ try {
+ transactionTemplate.execute(s -> {
+ Payment first = new Payment();
+ first.setAmount(1000L);
+ first.setReferenceNumber("Ref-1");
+ first.setState(Payment.State.SUCCESSFUL);
+
+ Payment second = new Payment();
+ second.setAmount(2000L);
+ second.setReferenceNumber("Ref-1");
+ second.setState(Payment.State.SUCCESSFUL);
+
+ entityManager.persist(first);
+ entityManager.persist(second);
+
+ return "Ref-1";
+ });
+ } catch (Exception ignored) {
+ }
+
+ assertThat(entityManager
+ .createQuery("select p from Payment p", Payment.class)
+ .getResultList()).isEmpty();
+ }
+
+ @Test
+ void givenAPayment_WhenNotExpectingAnyResult_ThenShouldCommit() {
+ transactionTemplate.execute(new TransactionCallbackWithoutResult() {
+ @Override
+ protected void doInTransactionWithoutResult(TransactionStatus status) {
+ Payment payment = new Payment();
+ payment.setReferenceNumber("Ref-1");
+ payment.setState(Payment.State.SUCCESSFUL);
+
+ entityManager.persist(payment);
+ }
+ });
+
+ assertThat(entityManager
+ .createQuery("select p from Payment p", Payment.class)
+ .getResultList()).hasSize(1);
+ }
+
+ @Test
+ void givenAPayment_WhenUsingTxManager_ThenShouldCommit() {
+ DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
+ definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
+ definition.setTimeout(3);
+
+ TransactionStatus status = transactionManager.getTransaction(definition);
+ try {
+ Payment payment = new Payment();
+ payment.setReferenceNumber("Ref-1");
+ payment.setState(Payment.State.SUCCESSFUL);
+
+ entityManager.persist(payment);
+ transactionManager.commit(status);
+ } catch (Exception ex) {
+ transactionManager.rollback(status);
+ }
+
+ assertThat(entityManager
+ .createQuery("select p from Payment p", Payment.class)
+ .getResultList()).hasSize(1);
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-annotations/src/test/java/lifecycleevents/UserRepositoryIntegrationTest.java b/persistence-modules/spring-data-jpa-annotations/src/test/java/lifecycleevents/UserRepositoryIntegrationTest.java
new file mode 100644
index 0000000000..078f437474
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-annotations/src/test/java/lifecycleevents/UserRepositoryIntegrationTest.java
@@ -0,0 +1,80 @@
+package lifecycleevents;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import com.baeldung.lifecycleevents.SpringBootLifecycleEventApplication;
+import com.baeldung.lifecycleevents.model.User;
+import com.baeldung.lifecycleevents.repository.UserRepository;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = SpringBootLifecycleEventApplication.class)
+public class UserRepositoryIntegrationTest {
+
+ @Autowired
+ private UserRepository userRepository;
+
+ @Before
+ public void setup() {
+ User user = new User();
+ user.setFirstName("Jane");
+ user.setLastName("Smith");
+ user.setUserName("jsmith123");
+ userRepository.save(user);
+ }
+
+ @After
+ public void cleanup() {
+ userRepository.deleteAll();
+ }
+
+ @Test
+ public void whenNewUserProvided_userIsAdded() {
+ User user = new User();
+ user.setFirstName("John");
+ user.setLastName("Doe");
+ user.setUserName("jdoe123");
+ user = userRepository.save(user);
+ assertTrue(user.getId() > 0);
+ }
+
+ @Test
+ public void whenUserNameProvided_userIsLoaded() {
+ User user = userRepository.findByUserName("jsmith123");
+ assertNotNull(user);
+ assertEquals("jsmith123", user.getUserName());
+ }
+
+ @Test
+ public void whenExistingUserProvided_userIsUpdated() {
+ User user = userRepository.findByUserName("jsmith123");
+ user.setFirstName("Joe");
+ user = userRepository.save(user);
+ assertEquals("Joe", user.getFirstName());
+ }
+
+ @Test
+ public void whenExistingUserDeleted_userIsDeleted() {
+ User user = userRepository.findByUserName("jsmith123");
+ userRepository.delete(user);
+ user = userRepository.findByUserName("jsmith123");
+ assertNull(user);
+ }
+
+ @Test
+ public void whenExistingUserLoaded_fullNameIsAvailable() {
+ String expectedFullName = "Jane Smith";
+ User user = userRepository.findByUserName("jsmith123");
+ assertEquals(expectedFullName, user.getFullName());
+ }
+}