JDBCTemplate使用BeanPropertyRowMapper设置嵌套的POJO

时间:2013-05-23 15:35:31

标签: java spring postgresql jdbctemplate

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

class User {
    String user_name;
    String display_name;
}

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

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

NamedParameterDatbase.query("SELECT * FROM message", new BeanPropertyRowMapper(Message.class));

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

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

SELECT * FROM message, user WHERE user_id = message_id

生成一个消息列表,其中包含关联的用户填充


用例:

最终,这些类作为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

然后,自定义行映射器会创建多个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;
}

同样,对于两个类连接来说,这似乎有些过分,但IRL用例涉及多个具有数十个字段的表。

5 个答案:

答案 0 :(得分:13)

也许你可以传入一个自定义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;
    }
});

答案 1 :(得分:10)

Spring在AutoGrowNestedPaths接口中引入了一个新的BeanMapper属性。

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

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

QUERY:

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

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;
  }
}

答案 2 :(得分:7)

有点迟到了,但是当我在谷歌上搜索同一个问题时我发现了这个问题,我发现了一个不同的解决方案,可能对将来的其他人有利。

不幸的是,如果不创建客户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;
     }
}

使用BeanPropertyRowMapper时要记住的关键是,您必须遵循列的命名和类成员的属性,并遵循以下例外(see Spring Documentation)

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

答案 3 :(得分:3)

更新:2015年10月4日。我通常不再进行任何这种行映射。您可以通过注释更加优雅地完成选择性JSON表示。请参阅此gist


我花了一整天的大部分时间试图弄清楚我的3层嵌套对象的情况,然后最终将其钉住。这是我的情况:

帐户(即用户)--1tomany - &gt;角色--1tomany - &gt;视图(允许用户查看)

(这些POJO类被粘贴在最底层。)

我希望控制器返回这样的对象:

[ {
  "id" : 3,
  "email" : "catchall@sdcl.org",
  "password" : "sdclpass",
  "org" : "Super-duper Candy Lab",
  "role" : {
    "id" : 2,
    "name" : "ADMIN",
    "views" : [ "viewPublicReports", "viewAllOrders", "viewProducts", "orderProducts", "viewOfferings", "viewMyData", "viewAllData", "home", "viewMyOrders", "manageUsers" ]
  }
}, {
  "id" : 5,
  "email" : "catchall@stereolab.com",
  "password" : "stereopass",
  "org" : "Stereolab",
  "role" : {
    "id" : 1,
    "name" : "USER",
    "views" : [ "viewPublicReports", "viewProducts", "orderProducts", "viewOfferings", "viewMyData", "home", "viewMyOrders" ]
  }
}, {
  "id" : 6,
  "email" : "catchall@ukmedschool.com",
  "password" : "ukmedpass",
  "org" : "University of Kentucky College of Medicine",
  "role" : {
    "id" : 2,
    "name" : "ADMIN",
    "views" : [ "viewPublicReports", "viewAllOrders", "viewProducts", "orderProducts", "viewOfferings", "viewMyData", "viewAllData", "home", "viewMyOrders", "manageUsers" ]
  }
} ]

关键是要意识到Spring并不是自动为你做这一切。如果您只是要求它返回一个Account项而不进行嵌套对象的工作,那么您只会得到:

{
  "id" : 6,
  "email" : "catchall@ukmedschool.com",
  "password" : "ukmedpass",
  "org" : "University of Kentucky College of Medicine",
  "role" : null
}

首先,创建3表SQL JOIN查询,确保您获得所需的所有数据。这是我的,因为它出现在我的控制器中:

@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@RequestMapping("/accounts")
public List<Account> getAllAccounts3() 
{
    List<Account> accounts = jdbcTemplate.query("SELECT Account.id, Account.password, Account.org, Account.email, Account.role_for_this_account, Role.id AS roleid, Role.name AS rolename, role_views.role_id, role_views.views FROM Account JOIN Role on Account.role_for_this_account=Role.id JOIN role_views on Role.id=role_views.role_id", new AccountExtractor() {});
    return accounts;
}

请注意,我正在加入3个表格。现在创建一个RowSetExtractor类,将嵌套对象放在一起。上面的例子显示了2层嵌套...这个更进了一步,做了3个级别。请注意,我也必须在地图中维护第二层对象。

public class AccountExtractor implements ResultSetExtractor<List<Account>>{

    @Override
    public List<Account> extractData(ResultSet rs) throws SQLException, DataAccessException {

        Map<Long, Account> accountmap = new HashMap<Long, Account>();
        Map<Long, Role> rolemap = new HashMap<Long, Role>();

        // loop through the JOINed resultset. If the account ID hasn't been seen before, create a new Account object. 
        // In either case, add the role to the account. Also maintain a map of Roles and add view (strings) to them when encountered.

        Set<String> views = null;
        while (rs.next()) 
        {
            Long id = rs.getLong("id");
            Account account = accountmap.get(id);
            if(account == null)
            {
                account = new Account();
                account.setId(id);
                account.setPassword(rs.getString("password"));
                account.setEmail(rs.getString("email"));
                account.setOrg(rs.getString("org"));
                accountmap.put(id, account);
            }

            Long roleid = rs.getLong("roleid");
            Role role = rolemap.get(roleid);
            if(role == null)
            {
                role = new Role();
                role.setId(rs.getLong("roleid"));
                role.setName(rs.getString("rolename"));
                views = new HashSet<String>();
                rolemap.put(roleid, role);
            }
            else
            {
                views = role.getViews();
                views.add(rs.getString("views"));
            }

            views.add(rs.getString("views"));
            role.setViews(views);
            account.setRole(role);
        }
        return new ArrayList<Account>(accountmap.values());
    }
}

这给出了所需的输出。以下POJO供参考。请注意Role类中的@ElementCollection Set视图。这是自动生成SQL查询中引用的role_views表的内容。知道该表存在,其名称和字段名称对于正确获取SQL查询至关重要。我不得不知道这似乎是错误的......似乎这应该更加自动化 - 不是Spring的用途吗?......但我无法找到更好的方法。据我所知,在这种情况下,您必须手动完成工作。

@Entity
public class Account implements Serializable {
        private static final long serialVersionUID = 1L;

        @Id 
        @GeneratedValue(strategy=GenerationType.AUTO)
        private long id;
        @Column(unique=true, nullable=false)
        private String email;
        @Column(nullable = false) 
        private String password;
        @Column(nullable = false) 
        private String org;
        private String phone;

        @ManyToOne(fetch = FetchType.EAGER, optional = false)
        @JoinColumn(name = "roleForThisAccount") // @JoinColumn means this side is the *owner* of the relationship. In general, the "many" side should be the owner, or so I read.
        private Role role;

        public Account() {}

        public Account(String email, String password, Role role, String org) 
        {
            this.email = email;
            this.password = password;
            this.org = org;
            this.role = role;
        }
        // getters and setters omitted

    }

   @Entity
   public class Role implements Serializable {

        private static final long serialVersionUID = 1L;

        @Id 
        @GeneratedValue(strategy=GenerationType.AUTO)   
        private long id; // required

        @Column(nullable = false) 
        @Pattern(regexp="(ADMIN|USER)")
        private String name; // required

        @Column
        @ElementCollection(targetClass=String.class)
        private Set<String> views;

        @OneToMany(mappedBy="role")
        private List<Account> accountsWithThisRole;

        public Role() {}

        // constructor with required fields
        public Role(String name)
        {
            this.name = name;
            views = new HashSet<String>();
            // both USER and ADMIN
            views.add("home");
            views.add("viewOfferings");
            views.add("viewPublicReports");
            views.add("viewProducts");
            views.add("orderProducts");
            views.add("viewMyOrders");
            views.add("viewMyData");
            // ADMIN ONLY
            if(name.equals("ADMIN"))
            {
                views.add("viewAllOrders");
                views.add("viewAllData");
                views.add("manageUsers");
            }
        }

        public long getId() { return this.id;}
        public void setId(long id) { this.id = id; };

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

        public Set<String> getViews() { return this.views; }
        public void setViews(Set<String> views) { this.views = views; };
    }

答案 4 :(得分:2)

我在这样的事情上做了很多工作,如果没有OR映射器,我就看不到一种优雅的方法来实现这一点。

任何基于反射的简单解决方案都会严重依赖于1:1(或N:1)的关系。此外,您返回的列不符合其类型,因此您无法说明哪些列与哪个类匹配。

您可以使用 spring-data和QueryDSL 。我没有深入研究它们,但我认为您需要一些查询的元数据,以后用于将数据库中的列映射回适当的数据结构。

您也可以尝试新的 postgresql json 支持,看起来很有希望。

HTH