如何与 JDBI SQL 对象 API 建立一对多关系?

2022-09-01 08:20:41

我正在使用JDBI创建一个简单的REST应用程序,其中包含dropwizard。下一步是集成与另一个资源具有一对多关系的新资源。到目前为止,我无法弄清楚如何在我的DAO中创建一个方法来检索一个对象,该对象保存另一个表中的对象列表。

POJO 表示形式如下:

用户 POJO:

public class User {

    private int id;
    private String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

帐户 POJO:

public class Account {

    private int id;
    private String name;
    private List<User> users;

    public Account(int id, String name, List<User> users) {
        this.id = id;
        this.name = name;
        this.users = users;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<User> getUsers() {
        return users;
    }

    public void setUsers(List<User> users) {
        this.users = users;
    }
}

DAO 应该看起来像这样

public interface AccountDAO {

    @Mapper(AccountMapper.class)
    @SqlQuery("SELECT Account.id, Account.name, User.name as u_name FROM Account LEFT JOIN User ON User.accountId = Account.id WHERE Account.id = :id")
    public Account getAccountById(@Bind("id") int id);

}

但是,当该方法具有单个对象作为返回值(帐户而不是List<Account>)时,似乎无法访问Mapper类中结果集的多行。我能找到的唯一接近的解决方案是在 https://groups.google.com/d/msg/jdbi/4e4EP-gVwEQ/02CRStgYGtgJ 中描述的,但是一个人也只返回一个包含单个对象的Set,这似乎不是很优雅。(并且不能被资源类正确使用。

似乎有一种方法可以在流畅的API中使用Folder2。但是我不知道如何将其与dropwizard正确集成,我宁愿坚持使用Dropwizard文档中建议的JDBI的SQL对象API。

真的没有办法在JDBI中使用SQL对象API获得一对多映射吗?这是数据库的一个基本用例,我认为我一定错过了一些东西。

非常感谢所有帮助,
蒂尔曼


答案 1

好吧,经过大量搜索,我看到了两种方法来处理这个问题:

第一种选择是检索每列的对象,并将其合并到资源的 Java 代码中(即在代码中进行联接,而不是由数据库完成)。这将导致类似

@GET
@Path("/{accountId}")
public Response getAccount(@PathParam("accountId") Integer accountId) {
    Account account = accountDao.getAccount(accountId);
    account.setUsers(userDao.getUsersForAccount(accountId));
    return Response.ok(account).build();
}

这对于较小的联接操作是可行的,但对我来说似乎不是很优雅,因为这是数据库应该做的事情。但是,我决定采用此路径,因为我的应用程序相当小,我不想编写大量映射器代码。

二个选项是编写一个映射器,该映射器检索联接查询的结果并将其映射到对象,如下所示:

public class AccountMapper implements ResultSetMapper<Account> {

    private Account account;

    // this mapping method will get called for every row in the result set
    public Account map(int index, ResultSet rs, StatementContext ctx) throws SQLException {

        // for the first row of the result set, we create the wrapper object
        if (index == 0) {
            account = new Account(rs.getInt("id"), rs.getString("name"), new LinkedList<User>());
        }

        // ...and with every line we add one of the joined users
        User user = new User(rs.getInt("u_id"), rs.getString("u_name"));
        if (user.getId() > 0) {
            account.getUsers().add(user);
        }

        return account;
    }
}

然后,DAO 接口将具有如下方法:

public interface AccountDAO {

    @Mapper(AccountMapper.class)
    @SqlQuery("SELECT Account.id, Account.name, User.id as u_id, User.name as u_name FROM Account LEFT JOIN User ON User.accountId = Account.id WHERE Account.id = :id")
    public List<Account> getAccountById(@Bind("id") int id);

}

注意:如果使用非集合返回类型,则抽象 DAO 类将静默编译,例如 .但是,即使 SQL 查询已找到多行,映射器也只会收到包含单行的结果集,映射器很乐意将其转换为具有单个用户的单个帐户。JDBI 似乎为具有非集合返回类型的查询强制实施了 。如果将具体方法声明为抽象类,则可以将其放入DAO中,因此一种选择是使用公共/受保护的方法对包装逻辑,如下所示:public Account getAccountById(...);LIMIT 1SELECT

public abstract class AccountDAO {

    @Mapper(AccountMapper.class)
    @SqlQuery("SELECT Account.id, Account.name, User.id as u_id, User.name as u_name FROM Account LEFT JOIN User ON User.accountId = Account.id WHERE Account.id = :id")
    protected abstract List<Account> _getAccountById(@Bind("id") int id);

    public Account getAccountById(int id) {
        List<Account> accountList = _getAccountById(id);
        if (accountList == null || accountList.size() < 1) {
            // Log it or report error if needed
            return null;
        }
        // The mapper will have given a reference to the same value for every entry in the list
        return accountList.get(accountList.size() - 1);
    }
}

这对我来说仍然有点麻烦和低级,因为在处理关系数据时通常有很多联接。我希望看到更好的方法,或者让JDBI使用SQL对象API支持抽象操作。


答案 2

在 JDBI v3 中,可以使用@UseRowReducer来实现此目的。行化简器在连接结果的每一行上调用,您可以将这些结果“累积”到单个对象中。在您的案例中,一个简单的实现如下所示:

public class AccountUserReducer implements LinkedHashMapRowReducer<Integer, Account> {

    @Override
    public void accumulate(final Map<Integer, Account> map, final RowView rowView) {
        final Account account = map.computeIfAbsent(rowView.getColumn("a_id", Integer.class),
            id -> rowView.getRow(Account.class));
        if (rowView.getColumn("u_id", Integer.class) != null) {
            account.addUser(rowView.getRow(User.class));
        }
    }
}

现在,您可以在返回联接的查询上应用此化简器:

@RegisterBeanMapper(value = Account.class, prefix = "a")
@RegisterBeanMapper(value = User.class, prefix = "u")
@SqlQuery("SELECT a.id a_id, a.name a_name, u.id u_id, u.name u_name FROM " +
    "Account a LEFT JOIN User u ON u.accountId = a.id WHERE " +
    "a.id = :id")
@UseRowReducer(AccountUserReducer.class)
Account getAccount(@Bind("id") int id);

请注意,您的和行/豆映射器可以保持不变;他们只是知道如何分别映射用户和帐户表的单个行。您的类将需要一个每次调用行化简器时调用的方法。UserAccountAccountaddUser()


推荐