在 Dropwizard 中使用 JDBi3 映射到具有多对多关系的 DTO 类 的问题

Problem mapping to DTO classes with Many-To-Many relations using JDBi3 in Dropwizard


我在 DAO 接口中使用 PostgreSQL 查询通过 JDBi3 映射检索数据时遇到问题。 在我的 Dropwizard 应用程序中,我有 Book DTO class,它与 Author 和 Category DTO classes 具有多对多关系,并且在将查询行映射到 BookDTO class 时遇到问题。以下是 DTO classes 的代码片段:

class BookDTO {
  private Long bookId;
  // other fields are left for code brevity 
  private List<Long> authors; 
  private List<Long> categories;

  // empty constructor + constructor with all fields excluding Lists + getters + setters
}

class AuthorDTO {
  private Long authorId;
  // other fields are left for code brevity 
  private List<Long> books; 

  // empty constructor + constructor with all fields excluding List + getters + setters
}

class CategoryDTO {
   private Long categoryId;
   // other fields are left for code brevity 
   private List<Long> books;

   // empty constructor + constructor with all fields excluding List + getters + setters
}


...并且由于我使用 JDBi3 DAO 接口执行 CRUD 操作,这就是我查询 数据库中所有书籍的方法 看起来像:

    @Transaction
    @UseRowMapper(BookDTOACMapper.class)
    @SqlQuery("SELECT book.book_id AS b_id, book.title, book.price, book.amount, book.is_deleted, author.author_id AS aut_id, category.category_id AS cat_id FROM book " +
            "LEFT JOIN author_book ON book.book_id = author_book.book_id " +
            "LEFT JOIN author ON author_book.author_id = author.author_id " +
            "LEFT JOIN category_book ON book.book_id = category_book.book_id " +
            "LEFT JOIN category ON category_book.category_id = category.category_id ORDER BY b_id ASC, aut_id ASC, cat_id ASC")
    List<BookDTO> getAllBooks();


...这是 BookDTOACMapper class 的 map 方法,看起来像:

public class BookDTOACMapper implements RowMapper<BookDTO> {

        @Override
        public BookDTO map(ResultSet rs, StatementContext ctx) throws SQLException {      
            final long bookId = rs.getLong("b_id");
            // normally retrieving values by using appropriate rs.getXXX() methods

            Set<Long> authorIds = new HashSet<>();
            Set<Long> categoryIds = new HashSet<>();
            long authorId = rs.getLong("aut_id");
            if (authorId > 0) {
                authorIds.add(authorId);
            }
            long categoryId = rs.getLong("cat_id");
            if (categoryId > 0) {
                categoryIds.add(categoryId);
            }    

            while (rs.next()) {
                if (rs.getLong("b_id") != bookId) {
                    break;
                } else {
                    authorId = rs.getLong("aut_id");
                    if (authorId > 0) { authorIds.add(authorId); }

                    categoryId = rs.getLong("cat_id");
                    if (categoryId > 0) { categoryIds.add(categoryId); }
                }
            }
            final List<Long> authorIdsList = new ArrayList<>(authorIds);
            final List<Long> categoryIdsList = new ArrayList<>(categoryIds);

            return new BookDTO(bookId, title, price, amount, is_deleted, authorIdsList, categoryIdsList);
        }
}

我遇到的问题是调用我的 GET 方法时(在资源 class 中定义,它从 BookDAO class 调用 getAllBooks() 方法)displays inconsistent results while the query itself returns proper results.

我在 Whosebug、官方 JDBi3 文档 API 和 Google 上找到的许多问题正在考虑 一对多 关系并使用 @UseRowReducer 注释,其中包含实现 LinkedHashMapRowReducer<TypeOfEntityIdentifier, EntityName> 的 class 但对于这种情况,我找不到实现它的方法。欢迎任何 example/suggestion。 :) 提前谢谢你。

所用工具的版本:
Dropwizard 框架 1.3.8
PostgreSQL 11.7
Java8

评论太长了:

  1. 这基本上是一个调试问题。为什么?

while (rs.next()) { if (rs.getLong("b_id") != bookId) { break; } else {

while 之后的第一个 if 正在吃掉当前行之后的行(调用行映射器时为当前行的行)。您正在跳过 bookId、authorId 等的处理(将数据放入 Java 对象中)。这就是为什么您得到

inconsistent results while the query itself returns proper results.

  1. 因此您需要重新审视您处理数据的方式。我看到两条路径:

    • 重新访问处理循环的逻辑以在停止给定 bookId 的处理时存储数据。可以使用可滚动的 ResultSets 实现此目的 - 即请求可滚动的 ResultSet 并在 brake; 调用 rs.previous() 之前。在下一次调用行映射器时,处理将从结果集中的正确行开始。
    • 利用 SQL/PostgreSQL 的力量并正确地做到这一点:https://dba.stackexchange.com/questions/173831/convert-right-side-of-join-of-many-to-many-into-array 聚合和调整数据库中的数据。数据库是完成这项工作的最佳工具。

也请花点时间查看 https://dba.stackexchange.com/users/3684/erwin-brandstetter 的其他答案。他们在 SQL 和 PostgreSQL.

方面提供了宝贵的见解

As zloster mentioned in his answer I've chosen 2nd option (by this answer 用于多对多关系),这是使用在 List<BookDTO> getAllBooks(); 方法上方编辑我的 PostgreSQL 查询 @SqlQuery 注释。查询现在在 SELECT 语句中使用 array_agg 聚合函数将我的结果分组到一个数组中,现在看起来像这样:

    @UseRowMapper(BookDTOACMapper.class)
    @SqlQuery("SELECT b.book_id AS b_id, b.title, b.price, b.amount, b.is_deleted, ARRAY_AGG(aut.author_id) as aut_ids, ARRAY_AGG(cat.category_id) as cat_ids " +
            "FROM book b " +
            "LEFT JOIN author_book ON author_book.book_id = b.book_id " +
            "LEFT JOIN author aut ON aut.author_id = author_book.author_id " +
            "LEFT JOIN category_book ON category_book.book_id = b.book_id " +
            "LEFT JOIN category cat ON cat.category_id = category_book.category_id " +
            "GROUP BY b_id " +
            "ORDER BY b_id ASC")
    List<BookDTO> getAllBooks();

因此 BookDTOACMapper class 的 map(..) 方法必须进行编辑,现在看起来像这样:

  @Override
  public BookDTO map(ResultSet rs, StatementContext ctx) throws SQLException {
    final long bookId = rs.getLong("b_id");
    String title = rs.getString("title");
    double price = rs.getDouble("price");
    int amount = rs.getInt("amount");
    boolean is_deleted = rs.getBoolean("is_deleted");

    Set<Long> authorIds = new HashSet<>();
    Set<Long> categoryIds = new HashSet<>();

    /* rs.getArray() retrives java.sql.Array and after it getArray gets
       invoked which returns array of Object(s) which are being casted 
       into array of Long elements */
    Long[] autIds = (Long[]) (rs.getArray("aut_ids").getArray());
    Long[] catIds = (Long[]) (rs.getArray("cat_ids").getArray());

    Collections.addAll(authorIds, autIds);
    Collections.addAll(categoryIds, catIds);

    final List<Long> authorIdsList = new ArrayList<>(authorIds);
    final List<Long> categoryIdsList = new ArrayList<>(categoryIds);

    return new BookDTO(bookId, title, price, amount, is_deleted, authorIdsList, categoryIdsList);
  }

现在所有结果都是一致的,这里是 pgAdmin4 中的 screenshot 查询。