Spring Data JPA mappe le résultat de la requête native vers POJO non-entité

91

J'ai une méthode de référentiel Spring Data avec une requête native

@Query(value = "SELECT g.*, gm.* FROM group g LEFT JOIN group_members gm ON g.group_id = gm.group_id and gm.user_id = :userId WHERE g.group_id = :groupId", nativeQuery = true)
    GroupDetails getGroupDetails(@Param("userId") Integer userId, @Param("groupId") Integer groupId);

et je voudrais mapper le résultat à POJO non-entité GroupDetails.

Est-ce possible et si oui, pourriez-vous donner un exemple?

alexanoïde
la source

Réponses:

64

En supposant que GroupDetails correspond à la réponse d'orid, avez-vous essayé JPA 2.1 @ConstructorResult ?

@SqlResultSetMapping(
    name="groupDetailsMapping",
    classes={
        @ConstructorResult(
            targetClass=GroupDetails.class,
            columns={
                @ColumnResult(name="GROUP_ID"),
                @ColumnResult(name="USER_ID")
            }
        )
    }
)

@NamedNativeQuery(name="getGroupDetails", query="SELECT g.*, gm.* FROM group g LEFT JOIN group_members gm ON g.group_id = gm.group_id and gm.user_id = :userId WHERE g.group_id = :groupId", resultSetMapping="groupDetailsMapping")

et utilisez ce qui suit dans l'interface du référentiel:

GroupDetails getGroupDetails(@Param("userId") Integer userId, @Param("groupId") Integer groupId);

Selon la documentation Spring Data JPA , Spring essaiera d'abord de trouver la requête nommée correspondant au nom de votre méthode - donc en utilisant @NamedNativeQuery, @SqlResultSetMappinget @ConstructorResultvous devriez être en mesure d'atteindre ce comportement

Daimon
la source
15
Pour que les données Spring puissent correspondre à NamedNativeQuery, le nom de classe de l'entité de domaine suivi d'un point doit être préfixé au nom de NamedNativeQuery. Donc, le nom doit être (en supposant que l'entité de domaine est Group) 'Group.getGroupDetails'.
Grant Lay
@GrantLay pouvez-vous jeter un oeil à cette question: stackoverflow.com/q/44871757/7491770 J'ai exactement ce genre de problème.
ram
Comment retournerai-je une liste de ces objets?
Nikhil Sahu
1
Pour que cela fonctionne, doit être GroupDetailsmarqué avec @Entity? Si possible, pouvez-vous indiquer sur quelle classe l'annotation @NamedNativeQuerydoit être appliquée?
Manu
3
@SqlResultSetMappinget les @NamedNativeQueryannotations doivent être présentes sur l'entité utilisée dans votre référentiel Spring Data (par exemple, car public interface CustomRepository extends CrudRepository<CustomEntity, Long>c'est la CustomEntityclasse)
Tomasz W
112

Je pense que le moyen le plus simple de le faire est d'utiliser ce qu'on appelle la projection. Il peut mapper les résultats de la requête aux interfaces. L'utilisation SqlResultSetMappingest peu pratique et rend votre code moche :).

Un exemple tiré du code source JPA de Spring Data:

public interface UserRepository extends JpaRepository<User, Integer> {

   @Query(value = "SELECT firstname, lastname FROM SD_User WHERE id = ?1", nativeQuery = true)
   NameOnly findByNativeQuery(Integer id);

   public static interface NameOnly {

     String getFirstname();

     String getLastname();

  }
}

Vous pouvez également utiliser cette méthode pour obtenir une liste de projections.

Consultez cette entrée de documentation JPA Spring Data pour plus d'informations sur les projections.

Note 1:

N'oubliez pas que votre Userentité est définie comme normale - les champs de l'interface projetée doivent correspondre aux champs de cette entité. Sinon, le mappage de champ peut être interrompu ( getFirstname()peut renvoyer la valeur du nom de famille et cetera).

Note 2:

Si vous utilisez la SELECT table.column ...notation, définissez toujours des alias correspondant aux noms de l'entité. Par exemple, ce code ne fonctionnera pas correctement (la projection renverra des valeurs nulles pour chaque getter):

@Query(value = "SELECT user.firstname, user.lastname FROM SD_User user WHERE id = ?1", nativeQuery = true)
NameOnly findByNativeQuery(Integer id);

Mais cela fonctionne bien:

@Query(value = "SELECT user.firstname AS firstname, user.lastname AS lastname FROM SD_User user WHERE id = ?1", nativeQuery = true)
NameOnly findByNativeQuery(Integer id);

En cas de requêtes plus complexes, je préfère utiliser JdbcTemplateun référentiel personnalisé à la place.

Michał Stochmal
la source
C'est une solution plus propre. J'avais vérifié mais les performances sont bien pires que l'utilisation de SqlResultSetMapping (c'est plus lent d'environ 30-40% :()
kidnan1991
fonctionne bien! rendez l'interface publique si vous souhaitez l'utiliser ailleurs
tibi
Ne fonctionne pas si vous souhaitez extraire un champ de type XML (clob). Toute suggestion?
Ashish
@Ashish Je préfère utiliser JdbcTemplate( docs.spring.io/spring-framework/docs/current/javadoc-api/org/… ) à la place. Vous pouvez utiliser la getClobméthode sur resultSetpour récupérer clob InputStream. Pour un exemple: rs.getClob("xml_column").getCharacterStream().
Michał Stochmal
Que faire si j'utilise SELECT * dans la requête et que la requête est native?
Salman Kazmi
17

Je pense que l'approche de Michal est meilleure. Mais, il existe un autre moyen d'obtenir le résultat de la requête native.

@Query(value = "SELECT g.*, gm.* FROM group g LEFT JOIN group_members gm ON g.group_id = gm.group_id and gm.user_id = :userId WHERE g.group_id = :groupId", nativeQuery = true)
String[][] getGroupDetails(@Param("userId") Integer userId, @Param("groupId") Integer groupId);

Maintenant, vous pouvez convertir ce tableau de chaînes 2D en l'entité souhaitée.

Ashish
la source
2
simple et élégant
john
9

Vous pouvez écrire votre requête native ou non native comme vous le souhaitez, et vous pouvez encapsuler les résultats de requête JPQL avec des instances de classes de résultats personnalisées. Créez un DTO avec les mêmes noms de colonnes renvoyées dans la requête et créez un constructeur d'arguments avec la même séquence et les mêmes noms que ceux renvoyés par la requête. Ensuite, utilisez la méthode suivante pour interroger la base de données.

@Query("SELECT NEW example.CountryAndCapital(c.name, c.capital.name) FROM Country AS c")

Créer DTO:

package example;

public class CountryAndCapital {
    public String countryName;
    public String capitalName;

    public CountryAndCapital(String countryName, String capitalName) {
        this.countryName = countryName;
        this.capitalName = capitalName;
    }
}
Waqas Memon
la source
correction: les mêmes noms ne sont pas obligatoires ... juste la même séquence de paramètres dans le constructeur et le jeu de résultats renvoyé.
Waqas Memon
Cela ne fonctionne que si Country est votre classe d'entité Java. Ce ne sera pas le cas si Country n'est pas votre classe d'entité Java.
Yeshwant KAKAD
1
Vous dites que cela devrait également fonctionner avec les requêtes natives? Pourriez-vous en donner un exemple?
Richard Tingle le
OP demande une requête native, mais l'exemple donné est un exemple non natif
CLS
0

Vous pouvez faire quelque chose comme

@NamedQuery(name="IssueDescriptor.findByIssueDescriptorId" ,

    query=" select new com.test.live.dto.IssuesDto (idc.id, dep.department, iss.issueName, 
               cat.issueCategory, idc.issueDescriptor, idc.description) 
            from Department dep 
            inner join dep.issues iss 
            inner join iss.category cat 
            inner join cat.issueDescriptor idc 
            where idc.id in(?1)")

Et il doit y avoir un constructeur comme

public IssuesDto(long id, String department, String issueName, String issueCategory, String issueDescriptor,
            String description) {
        super();
        this.id = id;
        this.department = department;
        this.issueName = issueName;
        this.issueCategory = issueCategory;
        this.issueDescriptor = issueDescriptor;
        this.description = description;
    }
Chandan Gawri
la source
13
La question concerne les requêtes natives, pas les requêtes écrites en HQL.
DBK
-5

Dans mon ordinateur, je reçois ce code fonctionne, c'est un peu différent de la réponse de Daimon.

@SqlResultSetMapping(
    name="groupDetailsMapping",
    classes={
        @ConstructorResult(
            targetClass=GroupDetails.class,
            columns={
                @ColumnResult(name="GROUP_ID",type=Integer.class),
                @ColumnResult(name="USER_ID",type=Integer.class)
            }
        )
    }
)

@NamedNativeQuery(name="User.getGroupDetails", query="SELECT g.*, gm.* FROM group g LEFT JOIN group_members gm ON g.group_id = gm.group_id and gm.user_id = :userId WHERE g.group_id = :groupId", resultSetMapping="groupDetailsMapping")

Jiangke
la source