JDBCTemplate set nested POJO with BeanPropertyRowMapper
étant donné L'exemple suivant POJO's: (assumez Getters et Setters pour toutes les propriétés)
class User {
String user_name;
String display_name;
}
class Message {
String title;
String question;
User user;
}
on peut facilement interroger une base de données (postgres dans mon cas) et peupler une liste de classes de messages en utilisant un BeanPropertyRowMapper où le champ db correspondait à la propriété dans le POJO: (supposons que les tables DB ont des champs correspondants aux propriétés POJO).
NamedParameterDatbase.query("SELECT * FROM message", new BeanPropertyRowMapper(Message.class));
je me demande - y a-t-il un moyen pratique de construire une seule requête et / ou de créer une ligne mapper de cette manière, les propriétés du POJO interne de l '"utilisateur" sont également insérées dans le message.
C'est-à-syntatical magique où chaque ligne de résultat de la requête:
SELECT * FROM message, user WHERE user_id = message_id
produire une liste de messages avec l'utilisateur associé
De Cas D'Utilisation:
en fin de compte, les classes sont passées en arrière comme un objet sérialisé d'un contrôleur de ressort, les classes sont imbriquées de sorte que le JSON / XML résultant a un structure.
à l'heure actuelle, cette situation est résolue en exécutant deux requêtes et en définissant manuellement la propriété utilisateur de chaque message dans une boucle. Utilisable, mais j'imagine une façon plus élégante devrait être possible.
Mise À Jour: Solution Utilisée -
Bravo à @Will Keeling pour l'inspiration pour la réponse à l'aide de l'utilisation de la ligne personnalisée mapper - ma solution ajoute l'ajout de cartes de propriété de haricots afin d'automatiser le domaine affectation.
Le problème, c'est la structuration de la requête, de sorte que les noms de tables sont préfixés (cependant il n'y a pas de norme de la convention pour ce faire, la requête est construite par programmation):
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
La ligne personnalisée mappeur crée alors plusieurs haricot de cartes et de jeux de leurs propriétés basé sur le préfixe de la colonne: (à l'aide de méta-données pour obtenir le nom de la colonne).
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;
}
encore une fois, cela peut sembler exagéré pour une jonction de deux classes, mais le cas de L'IRL utiliser implique plusieurs tables avec des dizaines de champs.
5 réponses
peut-être pourriez-vous passer dans une coutume RowMapper
qui pourrait associer chaque ligne d'une requête de jointure agrégée (entre message et utilisateur) à un Message
et imbriquées User
. Quelque chose comme ceci:
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;
}
});
Le printemps a introduit un nouveau AutoGrowNestedPaths
propriété dans le BeanMapper
interface.
aussi longtemps que la requête SQL formate les noms de colonne avec un . séparateur (comme avant) alors le mapper de ligne ciblera automatiquement les objets intérieurs.
avec ceci, j'ai créé un nouveau mapper de ligne générique comme suit:
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;
}
}
un peu en retard à la partie cependant j'ai trouvé ceci quand je googlais la même question et j'ai trouvé une solution différente qui pourrait être favorable pour d'autres dans le futur.
malheureusement, il n'y a pas une façon native de réaliser le scénario imbriqué sans faire un RowMapper client. Cependant, je vais partager un moyen plus facile de faire ledit RowMapper personnalisé que certaines des autres solutions ici.
compte tenu de votre scénario, vous pouvez effectuer les opérations suivantes:
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;
}
}
La essentiel à retenir avec BeanPropertyRowMapper
est ce que vous avez à suivre le nommage de vos colonnes et les propriétés de vos membres de la classe à la lettre, avec les exceptions suivantes (voir la Documentation du printemps):
- les noms des colonnes sont aliasés exactement
- les noms de colonnes avec des soulignements seront convertis en cas" camel " (c.-à-d. MY_COLUMN_WITH_UNDERSCORES = = myColumnWithUnderscores)
mise à Jour: 10/4/2015. En général, je ne fais pas de cette rowmapping plus. Vous pouvez accomplir la représentation JSON sélective beaucoup plus élégamment via des annotations. Voir cette gist.
j'ai passé la majeure partie d'une journée entière à essayer de comprendre cela pour mon cas d'objets emboîtés en 3 couches et je l'ai finalement cloué. Voici ma situation:
comptes (c.-à-d. utilisateurs) --1tomany--> rôles --1tomany--> vues (l'utilisateur est autorisé à voir)
(ces classes de POJO sont collées tout en bas.)
Et je voulais que le contrôleur de renvoyer un objet comme ceci:
[ {
"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" ]
}
} ]
un point clé est de réaliser que le ressort ne fait pas tout cela automatiquement pour vous. Si vous lui demandez simplement de retourner un élément de compte sans faire le travail des objets imbriqués, vous obtiendrez simplement:
{
"id" : 6,
"email" : "catchall@ukmedschool.com",
"password" : "ukmedpass",
"org" : "University of Kentucky College of Medicine",
"role" : null
}
donc, tout d'abord, créez votre requête SQL de 3 tables et assurez-vous que vous obtenez toutes les données dont vous avez besoin. Voici le mien, tel qu'il apparaît dans mon contrôleur:
@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;
}
notez que je joins 3 tables. Créez maintenant une classe RowSetExtractor pour assembler les objets imbriqués. Les exemples ci-dessus montrent l'imbrication en deux couches... celui-ci va un peu plus loin et fait 3 niveaux. Notez que je dois aussi maintenir l'objet de la deuxième couche dans une carte.
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());
}
}
et cela donne la sortie désirée. Pojo ci-dessous pour référence. Notez que @ElementCollection définit les vues dans la classe de rôle. C'est ce qui génère automatiquement la table role_views référencée dans la requête SQL. Sachant que la table existe, son nom et ses noms de champs sont cruciaux pour obtenir la requête SQL correcte. Il se sent mal d'avoir à le savoir... il semble que cela devrait être plus automagique -- n'est-ce pas à cela que sert le printemps?... mais je ne pouvais trouver un meilleur moyen. Vous devez faire le travail manuellement dans cette affaire, autant que je puisse dire.
@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; };
}
j'ai beaucoup travaillé sur des choses comme celles-ci et je ne vois pas de façon élégante d'y arriver sans un OR mapper.
toute solution simple basée sur la réflexion reposerait fortement sur la relation 1:1 (ou peut-être N:1). En outre vos colonnes retournées ne sont pas qualifiées par leur type, de sorte que vous ne pouvez pas dire quelles colonnes correspondent à quelle classe.
Vous pouvez sortir avec printemps-données et QueryDSL. Je n'ai pas creusé dedans, mais je pense que vous avez besoin de quelques méta-données pour la requête qui est utilisé plus tard pour mapper les colonnes de votre base de données dans une structure de données appropriée.
vous pouvez aussi essayer le nouveau postgresql json soutien qui semble prometteur.
HTH