Étant donné l'exemple suivant de POJO: (Supposons que les 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 remplir une liste de classes Message 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 - existe-t-il un moyen pratique de construire une seule requête et/ou de créer un mappeur de lignes de manière à renseigner également les propriétés du POJO "utilisateur" interne dans le message.
Autrement dit, une certaine magie syntatique où chaque ligne de résultat dans la requête:
SELECT * FROM message, user WHERE user_id = message_id
Produire une liste de messages avec l'utilisateur associé renseigné
Cas d'utilisation:
En fin de compte, les classes sont retransmises en tant qu'objets sérialisés à partir d'un contrôleur Spring, les classes sont imbriquées de sorte que le JSON/XML résultant ait une structure décente.
Pour le moment, 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 qu'une manière plus élégante devrait être possible.
Mise à jour: solution utilisée -
Félicitations à @Will Keeling pour trouver l'inspiration pour la réponse avec l'utilisation du mappeur de lignes personnalisé - Ma solution ajoute l'ajout de cartes de propriétés de bean afin d'automatiser les affectations de champs.
La mise en garde structure la requête de manière à ce que les noms de table pertinents soient préfixés (mais il n'y a pas de convention standard pour ce faire, la requête est donc construite par programme):
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
Le mappeur de lignes personnalisé crée ensuite plusieurs mappages de beans et définit leurs propriétés en fonction du préfixe de la colonne: (en utilisant des métadonné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 jointure à deux classes, mais le cas d'utilisation IRL implique plusieurs tables avec des dizaines de champs.
Spring a introduit une nouvelle propriété AutoGrowNestedPaths
dans l'interface BeanMapper
.
Tant que la requête SQL formate les noms de colonne avec un. séparateur (comme précédemment), le mappeur de lignes ciblera automatiquement les objets internes.
Avec cela, j'ai créé un nouveau mappeur de lignes générique comme suit:
REQUETE:
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 DE RANGÉE:
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;
}
}
Vous pourriez peut-être passer un RowMapper
personnalisé qui pourrait mapper chaque ligne d'une requête de jointure agrégée (entre le message et l'utilisateur) à un Message
et un User
imbriqué. Quelque chose comme ça:
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;
}
});
Un peu tard pour la fête, j'ai trouvé cela quand je cherchais la même question sur Google et j'ai trouvé une solution différente qui pourrait être favorable à d'autres à l'avenir.
Malheureusement, il n'existe pas de méthode native pour réaliser le scénario imbriqué sans créer un client RowMapper. Cependant, je partagerai un moyen plus facile de créer 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 chose clé à retenir avec BeanPropertyRowMapper
est que vous devez suivre la dénomination de vos colonnes et les propriétés de vos membres de classe à la lettre avec les exceptions suivantes (voir la documentation de Spring) :
Mise à jour: 10/4/2015 . En général, je ne fais plus aucun de ces tracés. Vous pouvez effectuer une représentation JSON sélective de manière beaucoup plus élégante via des annotations. Voir ceci Gist .
J'ai passé la majeure partie d'une journée entière à essayer de comprendre cela pour mon cas d'objets imbriqués à 3 couches et je l'ai finalement cloué. Voici ma situation:
Comptes (c'est-à-dire utilisateurs) --1tomany -> Rôles --1tomany -> vues (l'utilisateur est autorisé à voir)
(Ces classes POJO sont collées tout en bas.)
Et je voulais que le contrôleur retourne un objet comme celui-ci:
[ {
"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" ]
}
} ]
Un point clé est de réaliser que Spring ne fait pas tout cela automatiquement pour vous. Si vous lui demandez simplement de renvoyer un élément de compte sans effectuer le travail d'objets imbriqués, vous obtiendrez simplement:
{
"id" : 6,
"email" : "[email protected]",
"password" : "ukmedpass",
"org" : "University of Kentucky College of Medicine",
"role" : null
}
Donc, tout d'abord, créez votre requête SQL JOIN à 3 tables et assurez-vous d'obtenir 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 JOIGNE 3 tables. Créez maintenant une classe RowSetExtractor pour assembler les objets imbriqués. Les exemples ci-dessus montrent l'imbrication à 2 couches ... celle-ci va plus loin et fait 3 niveaux. Notez que je dois également conserver l'objet de 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 souhaitée. POJO ci-dessous pour référence. Notez les vues @ElementCollection Set dans la classe Role. C'est ce qui génère automatiquement la table role_views comme référencé dans la requête SQL. Sachant que cette table existe, son nom et ses noms de champs sont cruciaux pour obtenir la bonne requête SQL. Il ne faut pas savoir que ... il semble que cela devrait être plus automagique - n'est-ce pas à ça que sert Spring? ... mais je ne pouvais pas trouver de meilleur moyen. Vous devez faire le travail manuellement dans ce cas, pour autant que je sache.
@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 trucs comme ça et je ne vois pas une manière élégante d'y parvenir sans un mappeur OR.
Toute solution simple basée sur la réflexion reposerait fortement sur la relation 1: 1 (ou peut-être N: 1). De plus, vos colonnes retournées ne sont pas qualifiées par leur type, vous ne pouvez donc pas dire quelles colonnes correspondent à quelle classe.
Vous pouvez vous en tirer avec spring-data et QueryDSL. Je ne les ai pas creusés, mais je pense que vous avez besoin de métadonnées pour la requête qui est ensuite utilisée pour mapper les colonnes de votre base de données dans une structure de données appropriée.
Vous pouvez également essayer le nouveau support postgresql json qui semble prometteur.
HTH