19

给定以下示例 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 控制器传回,这些类是嵌套的,因此生成的 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 用例涉及具有数十个字段的多个表。

4

6 回答 6

19

也许您可以传入一个自定义RowMapper,该自定义可以将聚合连接查询(在消息和用户之间)的每一行映射到 aMessage和 nested 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;
    }
});
于 2013-05-24T08:27:47.340 回答
17

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

不幸的是,如果不制作客户 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是您必须遵循列的命名和类成员的属性,但以下例外(请参阅 Spring 文档)

  • 列名完全别名
  • 带有下划线的列名将被转换为“驼峰式”大小写(即 MY_COLUMN_WITH_UNDERSCORES == myColumnWithUnderscores)
于 2017-03-24T01:57:00.357 回答
12

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

只要 SQL 查询使用 . 分隔符(和以前一样)然后行映射器将自动定位内部对象。

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

询问:

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

行映射器:

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;
  }
}
于 2015-07-23T15:38:32.657 回答
3

更新: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<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; };
    }
于 2015-04-09T13:34:16.377 回答
2

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

任何基于反射的简单解决方案都将严重依赖 1:1(或者可能是 N:1)关系。此外,您返回的列未按其类型限定,因此您无法说出哪些列与哪个类匹配。

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

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

于 2013-05-23T22:06:13.843 回答
0

NestedRowMapper 为我工作,重要的部分是让 SQL 正确。Message 属性中不应包含类名,因此查询应如下所示:

询问:

SELECT title AS "title", question AS "question", user_name AS "user.user_name", display_name AS "user.display_name" FROM message, user WHERE user_id = message_id
于 2021-09-21T14:10:36.250 回答