这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions persistence-modules/spring-boot-persistence-5/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
<instancio.version>5.2.1</instancio.version>
<spring-boot.version>3.4.1</spring-boot.version>
<db2.version>12.1.0.0</db2.version>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@startuml Nested Transactions in publishArticle_v2

participant "Client" as C
participant "Blog" as B
participant "ArticleRepo" as AR
participant "AuditService" as AS
database "Database" as DB

C -> B: publishArticle_v2()
activate B
note right of B: @Transactional
B -> DB: Begin TX1
activate DB #LightBlue

B -> AR: save()
activate AR
AR -> DB: INSERT article
AR --> B:
deactivate AR

B -> AS: saveAudit()
activate AS
note right of AS: @Transactional\n(REQUIRES_NEW)
AS -> DB: Begin TX2
activate DB #LightGreen

AS -> DB: INSERT audit

AS -> DB: Commit TX2
deactivate DB
AS --> B:
deactivate AS

B -> DB: Commit / Rollback TX1
deactivate DB
B --> C:
deactivate B

note over DB
TX1: Article save
TX2: Audit save (independent, always commits)
end note

@enduml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@startuml Sequential Transactions in publishArticle_v3

participant "Client" as C
participant "Blog" as B
participant "TransactionTemplate" as TT
participant "ArticleRepo" as AR
participant "AuditRepo" as AuR
database "Database" as DB

C -> B: publishArticle_v3()
activate B

B -> TT: execute()
activate TT
TT -> DB: Begin TX1
activate DB #LightBlue

TT -> AR: save()
activate AR
AR -> DB: INSERT article
AR --> TT
deactivate AR

TT -> DB: Commit / Rollback TX1
deactivate DB
TT --> B
deactivate TT


B -> DB: Begin TX2
activate DB #LightGreen

B -> AuR: save()
activate AuR
AuR -> DB: INSERT audit (FAILURE)
AuR --> B:
deactivate AuR

B -> DB: Commit TX2
deactivate DB

B --> C:
deactivate B

note over DB
TX1 completes (commit or rollback) BEFORE TX2 begins
TX2: Independent transaction for FAILURE audit
end note

@enduml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.baeldung.rollbackonly;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EnableJpaRepositories
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.baeldung.rollbackonly;

import java.util.Optional;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import com.baeldung.rollbackonly.article.Article;
import com.baeldung.rollbackonly.article.ArticleRepo;
import com.baeldung.rollbackonly.audit.Audit;
import com.baeldung.rollbackonly.audit.AuditRepo;
import com.baeldung.rollbackonly.audit.AuditService;

@Component
public class Blog {

private final ArticleRepo articleRepo;
private final AuditRepo auditRepo;
private final AuditService auditService;
private final TransactionTemplate transactionTemplate;

Blog(ArticleRepo articleRepo, AuditRepo auditRepo, AuditService auditService, TransactionTemplate transactionTemplate) {
this.articleRepo = articleRepo;
this.auditRepo = auditRepo;
this.auditService = auditService;
this.transactionTemplate = transactionTemplate;
}

@Transactional
public Optional<Long> publishArticle(Article article) {
try {
article = articleRepo.save(article);
auditRepo.save(new Audit("SAVE_ARTICLE", "SUCCESS", "saved: " + article.getTitle()));
return Optional.of(article.getId());

} catch (Exception e) {
String errMsg = "failed to save: %s, err: %s".formatted(article.getTitle(), e.getMessage());
auditRepo.save(new Audit("SAVE_ARTICLE", "FAILURE", errMsg));
return Optional.empty();
}
}

@Transactional
public Optional<Long> publishArticle_v2(Article article) {
try {
article = articleRepo.save(article);
auditService.saveAudit("SAVE_ARTICLE", "SUCCESS", "saved: " + article.getTitle());
return Optional.of(article.getId());

} catch (Exception e) {
auditService.saveAudit("SAVE_ARTICLE", "FAILURE", "failed to save: " + article.getTitle());
return Optional.empty();
}
}

public Optional<Long> publishArticle_v3(final Article article) {
try {
Article savedArticle = transactionTemplate.execute(txStatus -> {
Article saved = articleRepo.save(article);
auditRepo.save(new Audit("SAVE_ARTICLE", "SUCCESS", "saved: " + article.getTitle()));
return saved;
});
return Optional.of(savedArticle.getId());

} catch (Exception e) {
auditRepo.save(
new Audit("SAVE_ARTICLE", "FAILURE", "failed to save: " + article.getTitle()));
return Optional.empty();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.baeldung.rollbackonly.article;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Entity
@NoArgsConstructor
public class Article {

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

@Column(nullable = false)
private String title;

@Column(nullable = false)
private String author;

public Article(String title, String author) {
this.title = title;
this.author = author;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.baeldung.rollbackonly.article;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;


@Repository
public interface ArticleRepo extends JpaRepository<Article, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.baeldung.rollbackonly.audit;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@Entity
public class Audit {

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

private String operation;

private String status;

private String description;

private LocalDateTime timestamp;

public Audit(String operation, String status, String description) {
this.operation = operation;
this.status = status;
this.description = description;
this.timestamp = LocalDateTime.now();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.baeldung.rollbackonly.audit;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AuditRepo extends JpaRepository<Audit, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.baeldung.rollbackonly.audit;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
public class AuditService {

private final AuditRepo auditRepo;

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAudit(String action, String status, String message) {
auditRepo.save(new Audit(action, status, message));
}

public AuditService(AuditRepo auditRepo) {
this.auditRepo = auditRepo;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.baeldung.rollbackonly;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.UnexpectedRollbackException;

import com.baeldung.rollbackonly.article.Article;
import com.baeldung.rollbackonly.article.ArticleRepo;
import com.baeldung.rollbackonly.audit.AuditRepo;

@SpringBootTest(classes = { Application.class })
class BlogIntegrationTest {

@Autowired
private Blog articleService;

@Autowired
private ArticleRepo articleRepo;

@Autowired
private AuditRepo auditRepo;

@BeforeEach
void afterEach() {
articleRepo.deleteAll();
auditRepo.deleteAll();
}

@Test
void whenPublishingAnArticle_thenAlsoSaveSuccessAudit() {
articleService.publishArticle(new Article("Test Article", "John Doe"));

assertThat(articleRepo.findAll())
.extracting("title")
.containsExactly("Test Article");

assertThat(auditRepo.findAll())
.extracting("description")
.containsExactly("saved: Test Article");
}

@Test
void whenPublishingAnInvalidArticle_thenThrowsUnexpectedRollbackException() {
assertThatThrownBy(() -> articleService.publishArticle(
new Article("Test Article", null)))
.isInstanceOf(UnexpectedRollbackException.class)
.hasMessageContaining("marked as rollback-only");

assertThat(auditRepo.findAll())
.isEmpty();
}

@Test
void whenPublishingAnInvalidArticle_thenSavesFailureToAudit() {
assertThatThrownBy(() -> articleService.publishArticle_v2(
new Article("Test Article", null)))
.isInstanceOf(Exception.class);

assertThat(auditRepo.findAll())
.extracting("description")
.containsExactly("failed to save: Test Article");
}

@Test
void whenPublishingAnInvalidArticle_thenRecoverFromError_andSavesFailureToAudit() {
Optional<Long> id = articleService.publishArticle_v3(
new Article("Test Article", null));

assertThat(id)
.isEmpty();

assertThat(auditRepo.findAll())
.extracting("description")
.containsExactly("failed to save: Test Article");
}
}