JDBCTemplate a défini un POJO nested avec BeanPropertyRowMapper

À l’aide des exemples de POJO suivants: (supposons des accesseurs et des setters pour toutes les propriétés)

class User { Ssortingng user_name; Ssortingng display_name; } class Message { Ssortingng title; Ssortingng question; User user; } 

On peut facilement interroger une firebase database (postgres dans mon cas) et renseigner une liste de classes Message à l’aide d’un BeanPropertyRowMapper où le champ de firebase database correspond à la propriété de POJO: (Supposons que les tables de firebase database possèdent des champs correspondants aux propriétés de POJO).

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

Je me pose la question suivante: existe-t-il un moyen pratique de créer une requête unique 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?

C’est-à-dire, un peu de 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 Message avec l’utilisateur associé renseigné


Cas d’utilisation:

En fin de compte, les classes sont renvoyées sous forme d’object sérialisé à partir d’un contrôleur Spring. Les classes sont nestedes de sorte que le format 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 son inspiration pour la réponse à l’utilisation du mappeur de lignes personnalisé – Ma solution ajoute l’ajout de mappes de propriétés de beans 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écédés d’un préfixe (toutefois, il n’existe aucune convention standard en ce sens, la requête est générée 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 mappes 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 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; } 

Là encore, cela peut sembler excessif pour une jointure à deux classes, mais le cas d’utilisation IRL implique plusieurs tables avec des dizaines de champs.

Vous pourriez peut-être transmettre un RowMapper personnalisé qui pourrait mapper chaque ligne d’une requête de jointure globale (entre message et utilisateur) à un Message et à un User nested. Quelque chose comme ça:

 List messages = jdbcTemplate.query("SELECT * FROM message m, user u WHERE u.message_id = m.message_id", new RowMapper() { @Override public Message mapRow(ResultSet rs, int rowNum) throws SQLException { Message message = new Message(); message.setTitle(rs.getSsortingng(1)); message.setQuestion(rs.getSsortingng(2)); User user = new User(); user.setUserName(rs.getSsortingng(3)); user.setDisplayName(rs.getSsortingng(4)); message.setUser(user); return message; } }); 

Spring a introduit une nouvelle propriété AutoGrowNestedPaths dans l’interface BeanMapper .

Tant que la requête SQL formate les noms de colonne avec a. séparateur (comme précédemment), le mappeur de lignes ciblera automatiquement les objects internes.

Avec cela, j’ai créé un nouveau mappeur de lignes générique comme suit:

QUESTION:

 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 implements RowMapper { private Class mappedClass; public NestedRowMapper(Class 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 tard pour le parti, mais j’ai trouvé cela lorsque je cherchais la même question sur Google et j’ai trouvé une solution différente qui pourrait être favorable pour d’autres à l’avenir.

Malheureusement, il n’existe pas de méthode native pour réaliser le scénario nested sans créer un client RowMapper. Cependant, je vais partager un moyen plus simple de créer ledit RowMapper personnalisé que certaines des autres solutions proposées ici.

En fonction de votre scénario, vous pouvez effectuer les opérations suivantes:

 class User { Ssortingng user_name; Ssortingng display_name; } class Message { Ssortingng title; Ssortingng question; User user; } public class MessageRowMapper implements RowMapper { @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; } } 

L’élément clé à retenir avec BeanPropertyRowMapper est que vous devez suivre le nom de vos colonnes et les propriétés de vos membres de classe à la lettre avec les exceptions suivantes (voir la documentation Spring) :

  • les noms de colonnes sont aliasés exactement
  • les noms de colonne avec des traits de soulignement seront convertis en “camel” (par exemple, MY_COLUMN_WITH_UNDERSCORES == myColumnWithUnderscores)

Mise à jour: 10/4/2015 . Généralement, je ne fais plus de ce mapping de lignes. Vous pouvez obtenir une représentation JSON sélective de manière beaucoup plus élégante via des annotations. Voir cette essence .


J’ai passé la plus grande partie de la journée à essayer de résoudre ce problème pour mon cas d’objects nesteds à 3 couches et je l’ai enfin bien compris. Voici ma situation:

Comptes (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 object 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 élément clé est de réaliser que Spring ne fait pas que tout cela automatiquement pour vous. Si vous lui demandez simplement de renvoyer un élément de compte sans effectuer le travail des objects nesteds, vous obtiendrez simplement:

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

Commencez donc par créer 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 getAllAccounts3() { List 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 rassembler les objects nesteds. Les exemples ci-dessus montrent une imbrication à 2 couches … celle-ci va plus loin et fait 3 niveaux. Notez que je dois également conserver l’object de la deuxième couche dans une carte.

 public class AccountExtractor implements ResultSetExtractor>{ @Override public List extractData(ResultSet rs) throws SQLException, DataAccessException { Map accountmap = new HashMap(); Map rolemap = new HashMap(); // 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 (ssortingngs) to them when encountered. Set 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.getSsortingng("password")); account.setEmail(rs.getSsortingng("email")); account.setOrg(rs.getSsortingng("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.getSsortingng("rolename")); views = new HashSet(); rolemap.put(roleid, role); } else { views = role.getViews(); views.add(rs.getSsortingng("views")); } views.add(rs.getSsortingng("views")); role.setViews(views); account.setRole(role); } return new ArrayList(accountmap.values()); } } 

Et cela donne la sortie souhaitée. POJOs 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 telle que référencée dans la requête SQL. Sachant que cette table existe, son nom et ses noms de champs sont cruciaux pour obtenir la requête SQL correcte. Cela fait mal de devoir savoir que … il semble que cela devrait être plus automagique – n’est-ce pas à quoi sert le spring? … mais je ne pouvais pas trouver un meilleur moyen. Autant que je sache, vous devez effectuer le travail manuellement dans ce cas.

 @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 Ssortingng email; @Column(nullable = false) private Ssortingng password; @Column(nullable = false) private Ssortingng org; private Ssortingng 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(Ssortingng email, Ssortingng password, Role role, Ssortingng 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 Ssortingng name; // required @Column @ElementCollection(targetClass=Ssortingng.class) private Set views; @OneToMany(mappedBy="role") private List accountsWithThisRole; public Role() {} // constructor with required fields public Role(Ssortingng name) { this.name = name; views = new HashSet(); // 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 Ssortingng getName() { return this.name; } public void setName(Ssortingng name) { this.name = name; } public Set getViews() { return this.views; } public void setViews(Set views) { this.views = views; }; } 

J’ai beaucoup travaillé sur ce genre de choses et je ne vois pas de façon élégante de le faire sans mappeur OU.

Toute solution simple basée sur la reflection reposerait fortement sur la relation 1: 1 (ou peut-être N: 1). De plus, vos colonnes renvoyé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 analysées, mais je pense que vous avez besoin de méta-données pour la requête, qui sont ensuite utilisées pour mapper les colonnes de votre firebase database dans une structure de données appropriée.

Vous pouvez également essayer le nouveau support postgresql json qui semble prometteur.

HTH