2013-05-23 63 views
12

Với ví dụ sau POJO của: (Giả sử Getters and Setters cho tất cả các thuộc tính)JdbcTemplate thiết POJO lồng nhau với BeanPropertyRowMapper

class User { 
    String user_name; 
    String display_name; 
} 

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

Người ta có thể dễ dàng truy vấn cơ sở dữ liệu (postgres trong trường hợp của tôi) và cư một danh sách các lớp tin nhắn sử dụng BeanPropertyRowMapper trong đó trường db khớp với thuộc tính trong POJO: (Giả sử các bảng DB có các trường tương ứng với các thuộc tính POJO).

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

Tôi đang tự hỏi - là có một cách thuận tiện để xây dựng một truy vấn duy nhất và/hoặc tạo ra một mapper hàng theo cách như vậy cũng để cư các thuộc tính của nội tâm 'user' POJO trong bức thư.

Đó là, một số ma thuật syntatical nơi mỗi hàng kết quả trong truy vấn:

SELECT * FROM message, user WHERE user_id = message_id 

Sản xuất một danh sách các tin nhắn với tài khoản liên quan đến dân cư


Trường hợp sử dụng:

Cuối cùng , các lớp được truyền lại như một đối tượng được tuần tự hóa từ một bộ điều khiển Spring, các lớp được lồng nhau sao cho kết quả JSON/XML có một cấu trúc phong nha.

Hiện tại, tình huống này được giải quyết bằng cách thực hiện hai truy vấn và đặt thủ công thuộc tính người dùng của mỗi thư trong một vòng lặp. Có thể sử dụng được, nhưng tôi tưởng tượng một cách thanh lịch hơn là có thể.


Cập nhật: Giải pháp sử dụng -

Kudos để @ Will Keeling để tìm cảm hứng cho câu trả lời với việc sử dụng các mapper hàng tùy chỉnh - Giải pháp của tôi cho biết thêm việc bổ sung các bản đồ tài sản đậu để tự động hóa các nhiệm vụ lĩnh vực .

Thông báo trước là cơ cấu truy vấn để các tên bảng có liên quan được bắt đầu (tuy nhiên không có ước tiêu chuẩn để làm điều này để truy vấn được xây dựng programatically):

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 

Tục hàng mapper sau đó tạo ra một số các bản đồ bean và đặt các thuộc tính của chúng dựa trên tiền tố của cột: (sử dụng dữ liệu meta để lấy tên cột).

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

Một lần nữa, điều này có vẻ như quá mức cần thiết để tham gia hai lớp nhưng trường hợp sử dụng IRL liên quan đến nhiều bảng với hàng chục trường.

+1

Đoạn mã trên bị hỏng. Có nhiều niềng răng đóng hơn là mở các niềng răng. (4 vs 3) – fivedogit

+0

@fivedogit - updated - TY- hy vọng bạn thấy nó hữu ích – nclord

Trả lời

8

Mùa xuân đã giới thiệu sản phẩm AutoGrowNestedPaths mới vào giao diện BeanMapper.

Miễn là truy vấn SQL định dạng tên cột bằng a. dấu phân cách (như trước đây) thì trình ánh xạ Row sẽ tự động nhắm mục tiêu các đối tượng bên trong.

Với điều này, tôi đã tạo ra một mapper hàng generic mới như sau:

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

Không hoạt động, cột và giá trị được điền chính xác, nhưng mappedObject được trả lại có đầy đủ giá trị null. – Accollativo

2

Tôi đã làm việc rất nhiều trên các công cụ như thế này và không thấy một cách thanh lịch để đạt được điều này mà không có một người lập bản đồ HOẶC.

Bất kỳ giải pháp đơn giản nào dựa trên sự phản chiếu sẽ dựa nhiều vào mối quan hệ 1: 1 (hoặc có thể N: 1). Hơn nữa các cột của bạn được trả lại không đủ điều kiện theo loại của chúng, do đó bạn không thể nói cột nào khớp với lớp nào.

Bạn có thể lấy đi với dữ liệu mùa xuân và QueryDSL. Tôi không đào sâu vào chúng, nhưng tôi nghĩ bạn cần một số siêu dữ liệu cho truy vấn mà sau này được sử dụng để ánh xạ lại các cột từ cơ sở dữ liệu của bạn thành một cấu trúc dữ liệu thích hợp.

Bạn cũng có thể thử hỗ trợ mới postgresql json có vẻ đầy hứa hẹn.

HTH

+0

Cảm ơn bạn đã đề xuất - Tôi sẽ cung cấp cho họ một Google và viết lại ở đây nếu chúng hữu ích. Bất kỳ thay đổi nào về việc xây dựng trên bản đồ OR? – nclord

+1

Tôi chưa áp dụng nó ... chỉ cần nói chuyện với anh chàng, người đã đóng góp cho spring-jpa (ví dụ: nếu bạn google). có ý nghĩa với tôi. –

+0

TY dành cho khách hàng tiềm năng - Tôi có thể nghiên cứu điều này trong tương lai – nclord

11

lẽ bạn có thể vượt qua trong một phong tục RowMapper có thể lập bản đồ mỗi hàng của một tổng hợp tham gia truy vấn (giữa tin nhắn và người sử dụng) đến một Message và lồng User. Một cái gì đó như thế này:

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

Cảm ơn bạn đã đề xuất - Tôi đã hy vọng sử dụng sự phản chiếu; Ví dụ được đăng là ngắn gọn - tình huống IRL có POJO với hàng chục trường có nhiều loại khác nhau. – nclord

+1

Upvoted - người khác có thể thấy đây là một ví dụ hữu ích của một người lập bản đồ hàng cho một POJO lồng nhau đơn giản. – nclord

+0

Vui lòng xem các cập nhật trong câu hỏi gốc – nclord

3

Cập nhật: 10/4/2015. Tôi thường không làm bất kỳ việc lập bản đồ hàng tuần nào nữa. Bạn có thể thực hiện biểu diễn JSON có chọn lọc thanh lịch hơn thông qua chú thích. Xem này gist.


Tôi đã dành phần tốt hơn cả ngày cố gắng tìm ra trường hợp này của đối tượng lồng nhau 3 lớp và cuối cùng cũng đóng đinh nó. Dưới đây là tình hình của tôi:

Accounts (tức là người dùng) --1tomany -> Vai trò --1tomany -> views (dùng được phép nhìn thấy)

(. Những lớp POJO được dán ở phía dưới rất)

Và tôi muốn bộ điều khiển để trả về một đối tượng như thế này:

[ { 
    "id" : 3, 
    "email" : "[email protected]", 
    "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" : "[email protected]", 
    "password" : "stereopass", 
    "org" : "Stereolab", 
    "role" : { 
    "id" : 1, 
    "name" : "USER", 
    "views" : [ "viewPublicReports", "viewProducts", "orderProducts", "viewOfferings", "viewMyData", "home", "viewMyOrders" ] 
    } 
}, { 
    "id" : 6, 
    "email" : "[email protected]", 
    "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" ] 
    } 
} ] 

một điểm quan trọng là nhận ra rằng mùa xuân không chỉ làm tất cả điều này tự động cho bạn. Nếu bạn chỉ cần hỏi nó để trở về một mục tài khoản mà không làm công việc của các đối tượng lồng nhau, bạn sẽ chỉ nhận được:

{ 
    "id" : 6, 
    "email" : "[email protected]", 
    "password" : "ukmedpass", 
    "org" : "University of Kentucky College of Medicine", 
    "role" : null 
} 

Vì vậy, đầu tiên, tạo SQL 3 bảng của bạn THAM GIA truy vấn và chắc chắn rằng bạn đang nhận được tất cả dữ liệu bạn cần. Đây là của tôi, như nó xuất hiện trong Bộ điều khiển của tôi:

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

Lưu ý rằng tôi đang tham gia 3 bảng. Bây giờ tạo một lớp RowSetExtractor để đặt các đối tượng lồng nhau với nhau. Các ví dụ trên cho thấy làm tổ 2 lớp ... cái này đi thêm một bước nữa và làm 3 cấp độ. Lưu ý rằng tôi cũng phải duy trì đối tượng lớp thứ hai trong bản đồ.

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

Và điều này mang lại kết quả mong muốn. POJO dưới đây để tham khảo. Lưu ý các khung nhìn @ElementCollection Set trong lớp Role. Đây là những gì tự động tạo bảng role_views như được tham chiếu trong truy vấn SQL. Biết rằng bảng đó tồn tại, tên của nó và tên trường của nó là rất quan trọng để nhận được truy vấn SQL ngay. Cảm thấy sai lầm khi phải biết rằng ... có vẻ như điều này sẽ tự động hơn - không phải là mùa xuân là gì? ... nhưng tôi không thể tìm ra cách tốt hơn. Bạn phải làm công việc thủ công trong trường hợp này, theo như tôi có thể nói.

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

Một chút muộn để đảng tuy nhiên Tôi tìm thấy điều này khi tôi đang googling cùng một câu hỏi và tôi tìm thấy một giải pháp khác nhau mà có thể được thuận lợi cho những người khác trong Tương lai.

Thật không may là không có cách nào có nguồn gốc để đạt được kịch bản lồng nhau mà không cần tạo RowMapper cho khách hàng. Tuy nhiên, tôi sẽ chia sẻ một cách dễ dàng hơn để làm cho RowMapper tùy chỉnh nói hơn một số giải pháp khác ở đây.

Với kịch bản của bạn, bạn có thể làm như sau:

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

Điều quan trọng cần nhớ với BeanPropertyRowMapper là bạn phải tuân theo việc đặt tên các cột của bạn và các thuộc tính của các thành viên lớp học của bạn để bức thư với ngoại lệ sau (see Spring Documentation):

  • tên cột được aliased chính xác
  • tên cột với dấu gạch dưới sẽ được chuyển đổi thành "lạc đà" trường hợp (ví dụ MY_CO. LUMN_WITH_UNDERSCORES == myColumnWithUnderscores)
+0

Cải thiện tốt @ioneyed! Tôi chỉ muốn thêm một gợi ý sau đây: - Lưu trữ 'BeanPropertyRowMapper' trong các trường cuối cùng riêng tư để có hiệu suất tốt hơn khi sử dụng lại phương thức' mapRow' chính. –