Spring Data JPA mapeia o resultado da consulta nativa para POJO de não entidade

97

Eu tenho um método de repositório Spring Data com uma consulta nativa

@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);

e gostaria de mapear o resultado para POJO de não entidade GroupDetails.

É possível e, em caso afirmativo, poderia dar um exemplo?

alexanoide
fonte

Respostas:

66

Assumindo GroupDetails como na resposta de orid, você tentou 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")

e use o seguinte na interface do repositório:

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

De acordo com a documentação do Spring Data JPA , o spring tentará primeiro encontrar a consulta nomeada que corresponda ao nome do seu método - então, usando @NamedNativeQuery, @SqlResultSetMappinge @ConstructorResultvocê deve ser capaz de atingir esse comportamento

Daimon
fonte
15
Para que os dados de primavera possam corresponder a NamedNativeQuery, o nome da classe da entidade de domínio seguido por um ponto precisa ser prefixado ao nome de NamedNativeQuery. Portanto, o nome deve ser (assumindo que a entidade de domínio é Grupo) 'Group.getGroupDetails'.
Grant Lay
@GrantLay você pode dar uma olhada nesta pergunta: stackoverflow.com/q/44871757/7491770 Eu tenho exatamente esse tipo de problema.
Ram
Como vou retornar uma lista de tais objetos?
Nikhil Sahu
1
Para fazê-lo funcionar, deve ser GroupDetailsmarcado com @Entity? Se possível, você pode dizer em qual classe a anotação @NamedNativeQuerydeve ser aplicada?
Manu,
3
@SqlResultSetMappinge as @NamedNativeQueryanotações devem estar presentes na entidade usada em seu repositório Spring Data (por exemplo, porque public interface CustomRepository extends CrudRepository<CustomEntity, Long>é a CustomEntityclasse)
Tomasz W
116

Acho que a maneira mais fácil de fazer isso é usar a chamada projeção. Ele pode mapear os resultados da consulta para interfaces. Usar SqlResultSetMappingé inconveniente e torna seu código feio :).

Um exemplo direto do código-fonte JPA da 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();

  }
}

Você também pode usar este método para obter uma lista de projeções.

Verifique esta entrada de documentos JPA de dados de primavera para obter mais informações sobre as projeções.

Nota 1:

Lembre-se de ter sua Userentidade definida como normal - os campos da interface projetada devem coincidir com os campos desta entidade. Caso contrário, o mapeamento de campo pode ser interrompido ( getFirstname()pode retornar o valor do sobrenome etc.).

Nota 2:

Se você usar a SELECT table.column ...notação, sempre defina aliases que correspondam aos nomes da entidade. Por exemplo, este código não funcionará corretamente (a projeção retornará nulos para cada getter):

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

Mas isso funciona bem:

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

No caso de consultas mais complexas, prefiro usar o JdbcTemplaterepositório personalizado.

Michał Stochmal
fonte
É uma solução mais limpa. Eu verifiquei, mas o desempenho é muito pior do que usar SqlResultSetMapping (é mais lento cerca de 30-40% :()
kidnan1991
funciona bem! tornar a interface pública se quiser usá-la em outro lugar
tibi
Não funciona se você deseja extrair o campo do tipo XML (clob). Alguma sugestão?
Ashish,
@Ashish Prefiro usar JdbcTemplate( docs.spring.io/spring-framework/docs/current/javadoc-api/org/… ). Você pode usar o getClobmétodo on a resultSetfim de buscar clob InputStream. Para um exemplo: rs.getClob("xml_column").getCharacterStream().
Michał Stochmal
E se eu usar SELECT * na consulta e a consulta for nativa?
Salman Kazmi
17

Acho que a abordagem de Michal é melhor. Porém, há mais uma maneira de obter o resultado da consulta nativa.

@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);

Agora, você pode converter esta matriz de string 2D na entidade desejada.

Ashish
fonte
2
simples e elegante
john
como você faz a conversão de um array 2D para uma classe?
Jorge Jiménez
10

Você pode escrever sua consulta nativa ou não nativa da maneira que desejar e pode agrupar os resultados da consulta JPQL com instâncias de classes de resultados personalizadas. Crie um DTO com os mesmos nomes das colunas retornadas na consulta e crie um construtor de todos os argumentos com a mesma sequência e nomes retornados pela consulta. Em seguida, use a seguinte maneira para consultar o banco de dados.

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

Criar 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
fonte
correção: os mesmos nomes não são obrigatórios ... apenas a mesma sequência de parâmetros no construtor e o conjunto de resultados retornado.
Waqas Memon de
Isso funciona apenas se Country for sua classe de entidade java. Isso não acontecerá se Country não for sua classe de entidade java.
Yeshwant KAKAD
1
Você diz que isso também deve funcionar com consultas nativas? Você poderia dar um exemplo disso?
Richard Tingle
OP pede uma consulta nativa, mas o exemplo dado é um não nativo
CLS
0

Use o método padrão na interface e obtenha o EntityManager para ter a oportunidade de definir o ResultTransformer, então você pode retornar o POJO puro, assim:

final String sql = "SELECT g.*, gm.* FROM group g LEFT JOIN group_members gm ON g.group_id = gm.group_id and gm.user_id = ? WHERE g.group_id = ?";
default GroupDetails getGroupDetails(Integer userId, Integer groupId) {
    return BaseRepository.getInstance().uniqueResult(sql, GroupDetails.class, userId, groupId);
}

E o BaseRepository.java é assim:

@PersistenceContext
public EntityManager em;

public <T> T uniqueResult(String sql, Class<T> dto, Object... params) {
    Session session = em.unwrap(Session.class);
    NativeQuery q = session.createSQLQuery(sql);
    if(params!=null){
        for(int i=0,len=params.length;i<len;i++){
            Object param=params[i];
            q.setParameter(i+1, param);
        }
    }
    q.setResultTransformer(Transformers.aliasToBean(dto));
    return (T) q.uniqueResult();
}

Esta solução não afeta nenhum outro método no arquivo de interface do repositório.

Ifankai
fonte
-2

Você pode fazer algo como

@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)")

E deve haver um construtor como

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
fonte
15
A questão é sobre consultas nativas, não consultas escritas em HQL.
DBK
-5

No meu computador, eu consigo que esse código funcione. É um pouco diferente da resposta 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
fonte