JDBCTemplate使用BeanPropertyRowMapper设置嵌套的POJO

ncl*_*ord 13 java postgresql spring jdbctemplate

给出以下示例POJO :(假设所有属性的Getters和Setter)

class User {
    String user_name;
    String display_name;
}

class Message {
    String title;
    String question;
    User user;
}
Run Code Online (Sandbox Code Playgroud)

可以轻松查询数据库(在我的情况下是postgres)并使用BeanPropertyRowMapper填充Message类列表,其中db字段与POJO中的属性匹配:(假设DB表具有POJO属性的对应字段).

NamedParameterDatbase.query("SELECT * FROM message", new BeanPropertyRowMapper(Message.class));
Run Code Online (Sandbox Code Playgroud)

我想知道 - 是否有一种方便的方法来构造单个查询和/或创建行映射器,以便在消息中填充内部"用户"POJO的属性.

也就是说,查询中每个结果行的一些语法魔术:

SELECT * FROM message, user WHERE user_id = message_id
Run Code Online (Sandbox Code Playgroud)

生成一个Message列表,其中填充了关联的User


使用案例:

最终,这些类作为一个序列化对象从Spring Controller传回,这些类是嵌套的,因此生成的JSON/XML具有不错的结构.

目前,通过执行两个查询并在循环中手动设置每个消息的用户属性来解决此问题.可用,但我想一个更优雅的方式应该是可能的.


更新:使用解决方案 -

感谢@Will Keeling使用自定义行映射器获得答案的灵感 - 我的解决方案添加了bean属性映射,以自动化字段分配.

需要注意的是构造查询以使相关的表名具有前缀(但是没有标准约定来执行此操作,因此以编程方式构建查询):

SELECT title AS "message.title", question AS "message.question", user_name AS "user.user_name", display_name AS "user.display_name" FROM message, user WHERE user_id = message_id
Run Code Online (Sandbox Code Playgroud)

然后,自定义行映射器将创建多个bean映射,并根据列的前缀设置其属性:(使用元数据获取列名称).

public Object mapRow(ResultSet rs, int i) throws SQLException {

    HashMap<String, BeanMap> beans_by_name = new HashMap();

    beans_by_name.put("message", BeanMap.create(new Message()));
    beans_by_name.put("user", BeanMap.create(new User()));

    ResultSetMetaData resultSetMetaData = rs.getMetaData();

    for (int colnum = 1; colnum <= resultSetMetaData.getColumnCount(); colnum++) {

        String table = resultSetMetaData.getColumnName(colnum).split("\\.")[0];
        String field = resultSetMetaData.getColumnName(colnum).split("\\.")[1];

        BeanMap beanMap = beans_by_name.get(table);

        if (rs.getObject(colnum) != null) {
            beanMap.put(field, rs.getObject(colnum));
        }
    }

    Message m = (Task)beans_by_name.get("message").getBean();
    m.setUser((User)beans_by_name.get("user").getBean());

    return m;
}
Run Code Online (Sandbox Code Playgroud)

同样,对于两类连接而言,这似乎有点过分,但IRL用例涉及具有数十个字段的多个表.

Wil*_*ing 14

也许你可以传入一个自定义RowMapper,它可以将聚合连接查询的每一行(在消息和用户之间)映射到Message嵌套User.像这样的东西:

List<Message> messages = jdbcTemplate.query("SELECT * FROM message m, user u WHERE u.message_id = m.message_id", new RowMapper<Message>() {
    @Override
    public Message mapRow(ResultSet rs, int rowNum) throws SQLException {
        Message message = new Message();
        message.setTitle(rs.getString(1));
        message.setQuestion(rs.getString(2));

        User user = new User();
        user.setUserName(rs.getString(3));
        user.setDisplayName(rs.getString(4));

        message.setUser(user);

        return message;
    }
});
Run Code Online (Sandbox Code Playgroud)


ncl*_*ord 10

Spring AutoGrowNestedPathsBeanMapper界面中引入了一个新属性.

只要SQL查询使用a格式化列名.分隔符(如前所述)然后行映射器将自动定位内部对象.

有了这个,我创建了一个新的通用行映射器,如下所示:

查询:

SELECT title AS "message.title", question AS "message.question", user_name AS "user.user_name", display_name AS "user.display_name" FROM message, user WHERE user_id = message_id
Run Code Online (Sandbox Code Playgroud)

ROW MAPPER:

package nested_row_mapper;

import org.springframework.beans.*;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;

public class NestedRowMapper<T> implements RowMapper<T> {

  private Class<T> mappedClass;

  public NestedRowMapper(Class<T> mappedClass) {
    this.mappedClass = mappedClass;
  }

  @Override
  public T mapRow(ResultSet rs, int rowNum) throws SQLException {

    T mappedObject = BeanUtils.instantiate(this.mappedClass);
    BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject);

    bw.setAutoGrowNestedPaths(true);

    ResultSetMetaData meta_data = rs.getMetaData();
    int columnCount = meta_data.getColumnCount();

    for (int index = 1; index <= columnCount; index++) {

      try {

        String column = JdbcUtils.lookupColumnName(meta_data, index);
        Object value = JdbcUtils.getResultSetValue(rs, index, Class.forName(meta_data.getColumnClassName(index)));

        bw.setPropertyValue(column, value);

      } catch (TypeMismatchException | NotWritablePropertyException | ClassNotFoundException e) {
         // Ignore
      }
    }

    return mappedObject;
  }
}
Run Code Online (Sandbox Code Playgroud)


ion*_*yed 10

然而,当我在Google上搜索相同的问题时,我发现了这个问题,我找到了一个可能对将来有利的其他解决方案.

遗憾的是,如果不创建客户RowMapper,就没有本地方法来实现嵌套方案.但是,我将分享一个更简单的方法来制作所述自定义RowMapper,而不是其他一些解决方案.

根据您的方案,您可以执行以下操作:

class User {
    String user_name;
    String display_name;
}

class Message {
    String title;
    String question;
    User user;
}

public class MessageRowMapper implements RowMapper<Message> {

    @Override
    public Message mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = (new BeanPropertyRowMapper<>(User.class)).mapRow(rs,rowNum);
        Message message = (new BeanPropertyRowMapper<>(Message.class)).mapRow(rs,rowNum);
        message.setUser(user);
        return message;
     }
}
Run Code Online (Sandbox Code Playgroud)

要记住的关键BeanPropertyRowMapper是你必须遵循列的命名和类成员的属性到字母,但有以下例外(请参阅Spring文档):

  • 列名称完全是别名
  • 带下划线的列名称将转换为"camel"大小写(即MY_COLUMN_WITH_UNDERSCORES == myColumnWithUnderscores)