Query#getResultList() 始终 returns 特定实体的 1 大小列表

Query#getResultList() always returns a 1 sized list for a specific entity

这是我的测试方法:

@RunWithApplicationComposer(mode = ExtensionMode.PER_ALL)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MyTest{

    @PersistenceContext
    EntityManager entityManager;

    @Resource
    private UserTransaction userTransaction;

    private DB db;

    private void startDb() throws ManagedProcessException {
        DBConfigurationBuilder config = DBConfigurationBuilder.newBuilder();
        config.setPort(0); // random port
        db = DB.newEmbeddedDB(config.build());
        db.start();
        db.createDB("testdb", "root", "root");
        db.source("schemaonly.sql", "root", "root", "testdb");
    }

    //resources.xml equivalent
    @Configuration
    public Properties config() throws ManagedProcessException {
        startDb();
        Properties p = new Properties();
        p.put("db", "new://Resource?type=DataSource");
        p.put("db.JdbcDriver", "com.mysql.cj.jdbc.Driver");
        p.put("db.JdbcUrl", "jdbc:mysql://localhost:"+db.getConfiguration().getPort()+"/testdb?user=root&password=root");
        return p;
    }
    
    //persistence.xml equivalent
    @Module
    public PersistenceUnit setupPU(){
        PersistenceUnit pu = new PersistenceUnit("testPU");
        pu.setProvider(HibernatePersistenceProvider.class);
        pu.setJtaDataSource("db");
        pu.setProperty("tomee.jpa.factory.lazy", "true");
        pu.setProperty("hibernate.format_sql" ,"true");
        pu.setProperty("hibernate.use_sql_comments" ,"true");
        pu.setProperty("hibernate.dialect" ,"org.hibernate.dialect.MySQL8Dialect");
        pu.setProperty("hibernate.physical_naming_strategy",
                "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy");
        return pu;
    }

    @Test
    void fetchPost() throws Exception{
        userTransaction.begin();

        User user = new User();
        user.setUsername("username");
        user.setAdmin(false);
        user.setEmail("email");
        user.setPassword("password".getBytes(StandardCharsets.UTF_8));
        user.setSalt("salt".getBytes(StandardCharsets.UTF_8));
        entityManager.persist(user);

        Section section = new Section();
        section.setName("section");
        entityManager.persist(section);

        IntStream.range(1, 10).mapToObj(n -> {
            Post post = new Post();
            post.setContent("content" + n);
            post.setTitle("title" + n);
            post.setType(Post.Type.TEXT);
            post.setAuthor(user);
            post.setSection(section);
            return post;
        }).forEach(entityManager::persist);

        List<Post> from_post = entityManager.createQuery("select p from Post p", Post.class).getResultList();
        Assertions.assertEquals(9,from_post.size());
        userTransaction.commit();
    }
}  

我正在使用 MariaDB4j 以便为我的测试获取一个临时的嵌入式数据库。无需在测试之间清除数据库,因为我在此 class(用于演示目的)中只进行一项测试。

我还在使用 OpenEJB 的应用程序编辑器,以便在我的测试中 运行 Java EE 容器。

测试从一个空架构开始,该架构填充了 1 个用户、1 个部分和 9 个帖子。测试检查检索到的帖子是否真的是 9,否则测试失败。

问题是我得到以下结果:

org.opentest4j.AssertionFailedError: 
Expected :9
Actual   :1  

这是我到目前为止尝试过的:

  1. 有 2 个独立的事务:一个持续存在,一个检索。我得到相同的结果

  2. 断言 select count(p) from Post p 的结果值为 9:测试通过。

  3. persist 操作后立即调用本机查询(在事务内部和外部都尝试过):

     entityManager.unwrap(Session.class).doWork(connection -> {
                 ResultSet resultSet = connection.createStatement().executeQuery("SELECT * From post");
                 while(resultSet.next()){
                     System.out.println(resultSet.getString("content"));
                 }
         }  );
    

    我得到content1 content2 content3 ...

  4. 正在获取另一个实体:

     @Test
     void fetchUsers() throws Exception {
         userTransaction.begin();
         IntStream.range(1,10).mapToObj(n -> {
             User user = new User();
             user.setUsername("username" + n);
             user.setAdmin(false);
             user.setEmail("email" + n);
             user.setPassword("password".getBytes(StandardCharsets.UTF_8));
             user.setSalt("salt".getBytes(StandardCharsets.UTF_8));
             return user;
         }).forEach(entityManager::persist);
         userTransaction.commit();
    
         userTransaction.begin();
         Assertions.assertEquals(9,entityManager.createQuery("from User",User.class).getResultList().size());
         userTransaction.commit();
     }  
    

    测试莫名其妙地通过了。我确保在任何测试之前都有一个干净的数据库。

  5. 在持久化操作后立即调用 flush()clear()。没有任何变化

  6. 我执行em.createQuery("select p from Post p", Post.class).getResultList()。它打印 9 个标题

我就是无法理解它。可能是什么原因(可能还有解决方案)?


Post

@Entity
@DynamicUpdate
public class Post implements Serializable {
    public enum Type {TEXT, IMG}

    @Getter @Setter
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @Setter @Getter
    @Column(length = 255, nullable = false)
    protected String title;

    @Getter @Setter
    @Column(columnDefinition = "TEXT", nullable = false)
    protected String content;

    @Setter @Getter
    @Column(nullable = false) @Enumerated(EnumType.STRING)
    protected Type type;

    @Getter
    @Column(nullable = false, updatable = false, insertable = false)
    protected Instant creationDate; //generato da sql

    @Getter
    @Column(name = "votes", nullable = false, insertable = false, updatable = false)
    protected Integer votesCount;

    @Getter
    @Formula("(select count(id) from Comment c where c.post_id = id group by c.post_id)") //native sql ew
    protected Integer commentCount;

    @Getter @Setter
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    protected Section section;

    @Getter @Setter
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    protected User author;

    @OneToMany(mappedBy="post")
    @MapKeyJoinColumn(name="user_id", updatable = false, insertable = false)
    protected Map<User, PostVote> votes = new HashMap<>();
    public PostVote getVote(User user){
        return votes.get(user);
    }

    public Post(){}

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Post)) return false;
        Post post = (Post) o;
        return id != null && id.equals(post.id);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
}  

用户

@Entity
public class User implements Serializable {

    @Getter @Setter
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @Getter @Setter
    @NaturalId(mutable = false) @Column(length = 30, unique = true, nullable = false)
    protected String username;

    @Getter @Setter
    @Column(length = 16, nullable = false)
    protected byte[] password;

    @Getter @Setter
    @Column(length = 16, nullable = false)
    protected byte[] salt;

    @Getter @Setter
    @Column(length = 255, unique = true, nullable = false)
    protected String email;

    @Getter @Setter
    @Column(length = 255)
    protected String description;

    @Getter @Setter
    @Column(length = 4096)
    protected String picture;

    @Getter
    @Column(insertable = false, updatable = false, nullable = false)
    protected Instant creationDate;

    @Getter @Setter
    @Column(nullable = false)
    protected Boolean admin;

    @OneToMany(mappedBy = "user")
    @OrderBy("endTime desc")
    protected List<Ban> bans = new ArrayList<>();
    public List<Ban> getBans(){
        return Collections.unmodifiableList(bans);
    }

    public User(){}

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return id.equals(user.id);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
}

部分

@Entity
public class Section implements Serializable {

    @Getter @Setter
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @Getter @Setter
    @Column(length = 255)
    protected String description;


    @Getter @Setter
    @Column(length = 50, nullable = false, unique = true) @NaturalId(mutable = true)
    protected String name;

    @Getter @Setter
    @Column(length = 4096)
    protected String picture;

    @Getter @Setter
    @Column(length = 4096)
    protected String banner;

    @OneToMany(mappedBy="section")
    @MapKeyJoinColumn(name="user_id", updatable = false, insertable = false)
    @LazyCollection(LazyCollectionOption.EXTRA)
    protected Map<User, Follow> follows;
    public Follow getFollow(User user){
        return follows.get(user);
    }
    public int getFollowCount(){
        return follows.size();
    }

    public Section(){}

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Section)) return false;
        Section section = (Section) o;
        return id.equals(section.id);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
}  

环境

问题出在 Post#commentCount 字段中。它是通过带有此本机子查询的 @Formula 注释错误计算的:

SELECT count(id) FROM Comment c WHERE c.post_id = id group by c.post_id  

其中 count(id) 等同于 count(post.id)。生成的查询如下:

 select
    post0_.id as id1_4_,
    post0_.author_id as author_i7_4_,
    post0_.content as content2_4_,
    post0_.creation_date as creation3_4_,
    post0_.section_id as section_8_4_,
    post0_.title as title4_4_,
    post0_.type as type5_4_,
    post0_.votes as votes6_4_,
    (select
        count(post0_.id) 
    from
        Comment c 
    where
        c.post_id = post0_.id 
    group by
        c.post_id) as formula1_ 
from
    post post0_  

我只需要将 count(id) 替换为 count(c.id)