次のPOJOの例を考えます:(すべてのプロパティのゲッターとセッターを想定)
class User {
String user_name;
String display_name;
}
class Message {
String title;
String question;
User user;
}
データベース(私の場合はpostgres)を簡単に照会し、BeanPropertyRowMapperを使用してメッセージクラスのリストにデータを入力できます。ここで、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がまともな構造になるようにします。
現時点では、この状況は2つのクエリを実行し、ループ内の各メッセージのユーザープロパティを手動で設定することで解決されています。使用可能ですが、もっとエレガントな方法が可能になると思います。
更新:使用したソリューション-
カスタムの行マッパーを使用して回答のインスピレーションを得るために@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;
}
繰り返しになりますが、これは2つのクラスの結合には過剰に見えるかもしれませんが、IRLの使用例には、数十のフィールドを持つ複数のテーブルが含まれます。
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;
}
}
おそらく、(メッセージとユーザー間の)集約結合クエリの各行を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;
}
});
パーティーには少し遅れましたが、同じ質問をグーグルで検索しているときにこれを見つけ、将来他の人に好都合な別の解決策を見つけました。
残念ながら、顧客の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ドキュメントを参照) :
更新:2015年10月4日。通常、この行マッピングはこれ以上行いません。アノテーションを使用すると、選択的JSON表現をよりエレガントに実行できます。これを見てください 要旨 。
3層のネストされたオブジェクトの場合について、これを理解するために丸一日の大部分を費やし、ついにそれを釘付けにしました。これが私の状況です:
アカウント(つまり、ユーザー)--1tomany->役割--1tomany->ビュー(ユーザーは表示を許可されています)
(これらのPOJOクラスは一番下に貼り付けられます。)
そして、私はコントローラがこのようなオブジェクトを返すことを望みました:
[ {
"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" ]
}
} ]
重要な点は、Springがこれらすべてを自動的に行うわけではないことを理解することです。ネストされたオブジェクトの処理を行わずに、単にAccountアイテムを返すように要求すると、次の結果が得られます。
{
"id" : 6,
"email" : "[email protected]",
"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つのレベルを実行します。 2番目のレイヤーのオブジェクトもマップに保持する必要があることに注意してください。
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クエリを正しく行うために重要です。それを知る必要があるのは間違っていると感じます...これはもっと自動魔法のように思えます-それはスプリングの目的ではありませんか?...しかし、私はより良い方法を理解できませんでした。この場合、私が知る限り、手動で作業を行う必要があります。
@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; };
}
私はこのようなものに多くのことをしました、そしてORマッパーなしでこれを達成するためのエレガントな方法は見当たりません。
リフレクションに基づく単純なソリューションは、1:1(またはおそらくN:1)の関係に大きく依存します。さらに、返される列はその型によって限定されないため、どの列がどのクラスに一致するかはわかりません。
spring-data and QueryDSLを使用して回避できます。私はそれらを詳しく調べませんでしたが、後でデータベースの列を適切なデータ構造にマップするために使用されるクエリのメタデータが必要だと思います。
有望に見える新しいpostgresql jsonサポートを試すこともできます。
HTH