Home > Blockchain >  Transactional Service Method that updates 2 repositories
Transactional Service Method that updates 2 repositories

Time:03-10

I want to test Transactional operation in my project. Basically, I want to roll back the userService.saveUser() operation, if an exception is thrown. I have simplified the classes, and you can find it below.

A user must live in an address. An address can have multiple users.

Address Entity

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Address {

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "STREET")
    @NotNull
    private String street;

    @ToString.Exclude
    @OneToMany(mappedBy = "address")
    private Set<User> users = new HashSet<>();
}

User Entity

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "USER")
public class User {

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "FIRSTNAME", nullable = false)
    @NotNull
    private String firstName;

    @ManyToOne(fetch = FetchType.LAZY)
    private Address address;

}

Repositories

public interface AddressRepository extends CrudRepository<Address, Long> {
}
public interface UserRepository extends CrudRepository<User, Long> {

}

UserService Class

@Service
@Slf4j
@AllArgsConstructor
public class UserService {
    
    @Autowired
    AddressRepository addressRepository;

    @Autowired
    UserRepository userRepository;

    @Transactional
    public void saveUser(String firstName, String street) {
        var address1 = Address.builder.street(street).build();
        // to make sure that I have "id" of the address when I am saving it.
        var addressSaved = addressRepository.save(address1);
        if ("f1".equals(firstName))
           throw new RuntimeException("some exception");
        var user = User.builder()
                .firstName(firstName)
                .address(addressSaved)
                .build();
        // this operation can also throw DataIntegrityViolationException
        userRepository.save(user);
    }
}

This is my test class

@SpringBootTest
class UserServiceIT {

    @Autowired
    AddressRepository addressRepository;
    @Autowired
    UserRepository userRepository;
    @Autowired
    UserService userService;

    @BeforeEach
    void beforeEach() {
        userRepository.deleteAll();
        addressRepository.deleteAll();
    }
    
    @Test
    void test_saveUser() {
        assertThrows(RuntimeException.class,() -> userService.saveUser("f1", "s1"));
        assertEquals(0, userRepository.count());
        assertEquals(0, addressRepository.count());
    }

    @Test
    void test_saveUser2() {
        // column: nullable = false will throw the exception
        assertThrows(DataIntegrityViolationException.class,() -> userService.saveUser(null, "s1"));
        assertEquals(0, userRepository.count());
        assertEquals(0, addressRepository.count());
    }
    
}

Both of the tests give assertion error on address count (Address is saved and user is not saved). I expect address to be roll backed (and not to be saved) since there is an error after saving the address, and while saving the user (some condition is violated, therefore 2 saves must be roll backed). What am I doing wrong?

application.yml for test environment

spring:
  devtools:
    restart:
      enabled: false
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=false
    driverClassName: org.h2.Driver
    username: sa
    password: 123
  h2:
    console:
      enabled: false
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    database: H2
    show-sql: false
    hibernate:
      ddl-auto: create

You can reach the whole sample project from this link: https://wetransfer.com/downloads/7cb870266e2e20f610b44d3cc9f229c220220308071438/7b88a2700076a3e53771e389c796cfe420220308071438/c777ab

CodePudding user response:

The code you posted here differs from what is actually exists in the original code you uploaded.

original code:

@Transactional
void saveUser(String firstName, String street) {
    var address = Address.builder().street(street).build();
    var addressSaved = addressRepository.save(address);

    if ("f1".equals(firstName))
        throw new RuntimeException("f1");

    var user = Person.builder()
            .firstName(firstName)
            .address(addressSaved)
            .build();
    personRepository.save(user);
}

This method actually have default access modifier so GCLIB is not able to override it and creates the needed logic. change access modifier of this method to public

  • Related