保持与@DataJpaTest 和 h2 数据库不工作的多对多关系
persist many to many relationship with @DataJpaTest and h2 database not working
我有两个实体。 User
:
@Entity
@Table(name = "user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "username", unique = true)
private String username;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
public void addRole(Role role) {
this.roles.add(role);
}
public void addUser(User user) {
this.users.add(user);
}
Role
:
@Entity
@Table(name = "role")
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", unique = true)
private String name;
@ManyToMany(mappedBy = "roles", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<User> users = new HashSet<>();
我正在使用 Spring Data JPA 并为 User
和 Role
创建存储库:
public interface UserRepository extends JpaRepository<User, Long> {
public Optional<User> findByUsername(String username);
}
public interface RoleRepository extends JpaRepository<Role, Long> {
public Optional<Role> findByName(String name);
public void deleteByName(String name);
}
我编写了一个测试,它创建了一个新的 User
并向 User
添加了一个 Role
,但是 Role doesn't get added to the
User` 和测试失败了:
@ExtendWith(SpringExtension.class)
@DataJpaTest
class UserRepositoryTest {
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@BeforeEach
void setUp() {
Role role1 = new Role();
role1.setName("Admin");
Role role2 = new Role();
role2.setName("Teacher");
Role role3 = new Role();
role3.setName("User");
roleRepository.saveAll(Arrays.asList(role1, role2, role3));
}
@Test
@Transactional
public void createNewUserTest() {
String username = "User1";
String password = "password";
String firstName = "name";
String lastName = "last name";
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setFirstName(firstName);
user.setLastName(lastName);
Role findedUserRole = roleRepository.findByName("User").get();
Role findedAdminRole = roleRepository.findByName("Admin").get();
user.addRole(findedUserRole);
user.addRole(findedAdminRole);
userRepository.save(user);
User findedUser = userRepository.findByUsername(username).get();
Role findedRole = roleRepository.findByName("User").get();
assertEquals(firstName,findedUser.getFirstName());
assertEquals(2, findedUser.getRoles().size());
assertTrue(findedRole.getUsers().contains(findedUser));
}
}
assertTrue(findedRole.getUsers().contains(findedUser))
失败。
如果在应用程序中使用相同的方法(不将用户添加到角色)一切正常,但在测试中它不起作用。
我的申请:
@Component
public class InitializeData implements CommandLineRunner {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Transactional
@Override
public void run(String... args) throws Exception {
Role role1 = new Role();
role1.setName("Admin");
Role role2 = new Role();
role2.setName("Teacher");
Role role3 = new Role();
role3.setName("User");
roleRepository.saveAll(Arrays.asList(role1, role2, role3));
String username = "User1";
String password = "password";
String firstName = "name";
String lastName = "last name";
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setFirstName(firstName);
user.setLastName(lastName);
Role findedUserRole = roleRepository.findByName("User").get();
Role findedAdminRole = roleRepository.findByName("Admin").get();
user.addRole(findedUserRole);
user.addRole(findedAdminRole);
userRepository.save(user);
}
}
一切正常!
怎么回事?
你中了JPA一级缓存的陷阱
您的完整测试发生在单个事务和会话中。
JPA 的核心原则之一是在给定 class 和 id 的会话中,EntityManager
将始终 return 相同的实例。
因此,当您在测试中执行 Role findedRole = roleRepository.findByName("User").get();
时,实际上并没有重新加载角色。您将获得您在测试的设置部分创建的确切实例,该实例仍然没有用户。
解决此问题的方法取决于您实际想要实现的目标。
对于双向关系,您应该始终保持它们同步,即对 user.addRole(..)
的调用应该更新作为参数传递的 Role
。有关详细信息,请参阅 https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/。
虽然这会使您的测试通过,但它实际上并没有测试数据是否已写入数据库并且可以按预期再次加载。为此,我喜欢使用以下方法之一。
flush
& clear
。获取测试中注入的 EntityManager
。保存实体后调用 flush
on it, in order to write the changes to the database and then clear
清除一级缓存。这将确保后续加载操作实际命中数据库并加载新实例。
显式交易。从测试中删除 @Transactional
注释。使用 TransactionManager.execute
在单独的事务中将 TransactionTemplate
注入到您的测试和 运行 设置、保存部分以及加载和断言部分。我认为这使测试的意图更加明显。但是现在您的事务实际上已提交,如果您为多个测试重复使用同一个数据库,这可能会导致问题。
我有两个实体。 User
:
@Entity
@Table(name = "user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "username", unique = true)
private String username;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
public void addRole(Role role) {
this.roles.add(role);
}
public void addUser(User user) {
this.users.add(user);
}
Role
:
@Entity
@Table(name = "role")
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", unique = true)
private String name;
@ManyToMany(mappedBy = "roles", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<User> users = new HashSet<>();
我正在使用 Spring Data JPA 并为 User
和 Role
创建存储库:
public interface UserRepository extends JpaRepository<User, Long> {
public Optional<User> findByUsername(String username);
}
public interface RoleRepository extends JpaRepository<Role, Long> {
public Optional<Role> findByName(String name);
public void deleteByName(String name);
}
我编写了一个测试,它创建了一个新的 User
并向 User
添加了一个 Role
,但是 Role doesn't get added to the
User` 和测试失败了:
@ExtendWith(SpringExtension.class)
@DataJpaTest
class UserRepositoryTest {
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@BeforeEach
void setUp() {
Role role1 = new Role();
role1.setName("Admin");
Role role2 = new Role();
role2.setName("Teacher");
Role role3 = new Role();
role3.setName("User");
roleRepository.saveAll(Arrays.asList(role1, role2, role3));
}
@Test
@Transactional
public void createNewUserTest() {
String username = "User1";
String password = "password";
String firstName = "name";
String lastName = "last name";
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setFirstName(firstName);
user.setLastName(lastName);
Role findedUserRole = roleRepository.findByName("User").get();
Role findedAdminRole = roleRepository.findByName("Admin").get();
user.addRole(findedUserRole);
user.addRole(findedAdminRole);
userRepository.save(user);
User findedUser = userRepository.findByUsername(username).get();
Role findedRole = roleRepository.findByName("User").get();
assertEquals(firstName,findedUser.getFirstName());
assertEquals(2, findedUser.getRoles().size());
assertTrue(findedRole.getUsers().contains(findedUser));
}
}
assertTrue(findedRole.getUsers().contains(findedUser))
失败。
如果在应用程序中使用相同的方法(不将用户添加到角色)一切正常,但在测试中它不起作用。
我的申请:
@Component
public class InitializeData implements CommandLineRunner {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Transactional
@Override
public void run(String... args) throws Exception {
Role role1 = new Role();
role1.setName("Admin");
Role role2 = new Role();
role2.setName("Teacher");
Role role3 = new Role();
role3.setName("User");
roleRepository.saveAll(Arrays.asList(role1, role2, role3));
String username = "User1";
String password = "password";
String firstName = "name";
String lastName = "last name";
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setFirstName(firstName);
user.setLastName(lastName);
Role findedUserRole = roleRepository.findByName("User").get();
Role findedAdminRole = roleRepository.findByName("Admin").get();
user.addRole(findedUserRole);
user.addRole(findedAdminRole);
userRepository.save(user);
}
}
一切正常! 怎么回事?
你中了JPA一级缓存的陷阱
您的完整测试发生在单个事务和会话中。
JPA 的核心原则之一是在给定 class 和 id 的会话中,EntityManager
将始终 return 相同的实例。
因此,当您在测试中执行 Role findedRole = roleRepository.findByName("User").get();
时,实际上并没有重新加载角色。您将获得您在测试的设置部分创建的确切实例,该实例仍然没有用户。
解决此问题的方法取决于您实际想要实现的目标。
对于双向关系,您应该始终保持它们同步,即对
user.addRole(..)
的调用应该更新作为参数传递的Role
。有关详细信息,请参阅 https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/。虽然这会使您的测试通过,但它实际上并没有测试数据是否已写入数据库并且可以按预期再次加载。为此,我喜欢使用以下方法之一。
flush
&clear
。获取测试中注入的EntityManager
。保存实体后调用flush
on it, in order to write the changes to the database and thenclear
清除一级缓存。这将确保后续加载操作实际命中数据库并加载新实例。显式交易。从测试中删除
@Transactional
注释。使用TransactionManager.execute
在单独的事务中将TransactionTemplate
注入到您的测试和 运行 设置、保存部分以及加载和断言部分。我认为这使测试的意图更加明显。但是现在您的事务实际上已提交,如果您为多个测试重复使用同一个数据库,这可能会导致问题。