在使用@DataJpaTest和H2数据库时,持久化多对多关系不起作用。

huangapple go评论120阅读模式
英文:

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为UserRole创建存储库:

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并将Role添加到User,但Role未添加到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);

    }
}

一切都正常工作!发生了什么?

英文:

I have two entities. User:

@Entity
@Table(name = &quot;user&quot;)
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = &quot;id&quot;)
private Long id;
@Column(name = &quot;username&quot;, unique = true)
private String username;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = &quot;user_role&quot;,
joinColumns = @JoinColumn(name = &quot;user_id&quot;),
inverseJoinColumns = @JoinColumn(name = &quot;role_id&quot;))
private Set&lt;Role&gt; roles = new HashSet&lt;&gt;();
public void addRole(Role role) {
this.roles.add(role);
}
public void addUser(User user) {
this.users.add(user);
}

Role:

@Entity
@Table(name = &quot;role&quot;)
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = &quot;name&quot;, unique = true)
private String name;
@ManyToMany(mappedBy = &quot;roles&quot;, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set&lt;User&gt; users = new HashSet&lt;&gt;();

I'm using Spring Data JPA and create repositories for User and Role:

public interface UserRepository extends JpaRepository&lt;User, Long&gt; {
public Optional&lt;User&gt; findByUsername(String username);
}

<!-- -->

public interface RoleRepository extends JpaRepository&lt;Role, Long&gt; {
public Optional&lt;Role&gt; findByName(String name);
public void deleteByName(String name);
}

I write a test which creates a new User and adds a Role to the User but Role doesn&#39;t get added to the User` and the test failes:

@ExtendWith(SpringExtension.class)
@DataJpaTest
class UserRepositoryTest {
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@BeforeEach
void setUp() {
Role role1 = new Role();
role1.setName(&quot;Admin&quot;);
Role role2 = new Role();
role2.setName(&quot;Teacher&quot;);
Role role3 = new Role();
role3.setName(&quot;User&quot;);
roleRepository.saveAll(Arrays.asList(role1, role2, role3));
}
@Test
@Transactional
public void createNewUserTest() {
String username = &quot;User1&quot;;
String password = &quot;password&quot;;
String firstName = &quot;name&quot;;
String lastName = &quot;last name&quot;;
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setFirstName(firstName);
user.setLastName(lastName);
Role findedUserRole = roleRepository.findByName(&quot;User&quot;).get();
Role findedAdminRole = roleRepository.findByName(&quot;Admin&quot;).get();
user.addRole(findedUserRole);
user.addRole(findedAdminRole);
userRepository.save(user);
User findedUser = userRepository.findByUsername(username).get();
Role findedRole = roleRepository.findByName(&quot;User&quot;).get();
assertEquals(firstName,findedUser.getFirstName());
assertEquals(2, findedUser.getRoles().size());
assertTrue(findedRole.getUsers().contains(findedUser));
}
}

assertTrue(findedRole.getUsers().contains(findedUser)) fails.
If using same method in application (without adding user to role) everything is ok, but in in the test it does not work.

My application:

@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(&quot;Admin&quot;);
Role role2 = new Role();
role2.setName(&quot;Teacher&quot;);
Role role3 = new Role();
role3.setName(&quot;User&quot;);
roleRepository.saveAll(Arrays.asList(role1, role2, role3));
String username = &quot;User1&quot;;
String password = &quot;password&quot;;
String firstName = &quot;name&quot;;
String lastName = &quot;last name&quot;;
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setFirstName(firstName);
user.setLastName(lastName);
Role findedUserRole = roleRepository.findByName(&quot;User&quot;).get();
Role findedAdminRole = roleRepository.findByName(&quot;Admin&quot;).get();
user.addRole(findedUserRole);
user.addRole(findedAdminRole);
userRepository.save(user);
}

}

Everything works fine!
What is going on?

答案1

得分: 3

你陷入了JPA一级缓存的陷阱。

你的完整测试在单个事务和会话中进行。

JPA的一个核心原则是,在给定类和ID的会话中,EntityManager将始终返回相同的实例。

因此,在你的测试中执行Role findedRole = roleRepository.findByName("User").get();时,实际上并没有重新加载角色。你获得的仍然是在测试设置部分创建的确切实例,该实例仍然没有用户。

修复这个问题的方法取决于你实际想要实现什么。

  1. 对于双向关联,你应始终保持它们同步,即user.addRole(..)的调用应更新作为参数传递的Role。详细信息请参见https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/。

    虽然这会使你的测试变为绿色,但实际上并不能测试数据是否已写入数据库并且是否可以按预期加载。为此,我喜欢使用以下方法之一。

  2. flushclear。在测试中注入EntityManager。在保存实体后,在其上调用flush,以便将更改写入数据库,然后调用clear以清除一级缓存。这将确保后续的加载操作实际上命中数据库并加载新实例。

  3. 显式事务。从你的测试中移除@Transactional注解。在你的测试中注入一个TransactionTemplate,并使用TransactionManager.execute在单独的事务中运行设置、保存部分以及加载和断言部分。我认为这会使测试的意图更明显。但现在你的事务实际上会提交,如果你在多个测试中重用同一个数据库,可能会引起问题。

英文:

You fell in the trap of JPAs first level cache.

Your complete test happens in a single transaction and therefore session.

One of the core principles of JPA is that within a session for a given class and id the EntityManager will always return the same instance.

So when you do a Role findedRole = roleRepository.findByName(&quot;User&quot;).get(); in your test, you are not actually reloading the role. You are getting the exact instance that you created in the setup part of the test, which still has no users.

The way to fix this depends on what you actually want to achieve.

  1. For bidirectional relationships you should always keep them in sync, i.e. the call to user.addRole(..) should update the Role passed as an argument. See https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/ for details on that.

    While this would make your test green, it doesn't actually test if the data was written to the database and can be loaded again as expected. For this I like to use one of the following approaches.

  2. flush & clear. Get the EntityManager injected in the test. After saving the entities call flush on it, in order to write the changes to the database and then clear to clear the 1st level cache. This will ensure that subsequent load operations actually hit the database and load new instances.

  3. explicite transactions. Remove the @Transactional annotations from your tests. Get a TransactionTemplate injected into your test and run setup, the saving part and the loading and assertion part in separate transactions using TransactionManager.execute. I think this makes the intent of the test more obvious. But now your transactions actually commit, which might cause problems if you are reusing the same database for several tests.

huangapple
  • 本文由 发表于 2020年10月21日 16:42:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/64459905.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定