JDBCTemplate使用BeanPropertyRowMapper设置嵌套的POJO

给出以下示例POJO :(假设所有属性的Getters和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 

生成一个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 

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

 public Object mapRow(ResultSet rs, int i) throws SQLException { HashMap 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用例涉及具有数十个字段的多个表。

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

 List messages = jdbcTemplate.query("SELECT * FROM message m, user u WHERE u.message_id = m.message_id", new RowMapper() { @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; } }); 

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

只要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 

行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 implements RowMapper { private Class mappedClass; public NestedRowMapper(Class 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; } } 

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

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

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

 class User { String user_name; String display_name; } class Message { String title; String question; User user; } public class MessageRowMapper implements RowMapper { @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要记住的关键是,必须遵循列的命名和类成员的属性,并遵循以下exception(请参阅Spring文档) :

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

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


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

帐户(即用户)–1tomany – >角色–1tomany – >视图(允许用户查看)

(这些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 getAllAccounts3() { List 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>{ @Override public List extractData(ResultSet rs) throws SQLException, DataAccessException { Map accountmap = new HashMap(); Map rolemap = new HashMap(); // 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 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(); 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(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 views; @OneToMany(mappedBy="role") private List accountsWithThisRole; public Role() {} // constructor with required fields public Role(String name) { this.name = name; views = new HashSet(); // 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 getViews() { return this.views; } public void setViews(Set views) { this.views = views; }; } 

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

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

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

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

HTH