Java usando filtragem em diferentes modelos antes e depois da projeção

8

Considere o seguinte modelo JAVA para o hibernate :

@Entity
@Table
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long id;

    @Column
    public String firstName;

    @Column
    public String lastName;

    @Column
    public Boolean active;
}

e o seguinte modelo para serialização da API (usando o controlador de descanso de inicialização por mola ):

public class PersonVO {
    public Long id;
    public String fullName;
}

O que eu quero é:

  • Aplique alguma filtragem na Pessoa (definida estaticamente)
  • Aplique alguma filtragem no PersonVO (obtenha @RequestParam)

Em C # .NET eu poderia fazer como:

IQueryable<Person> personsQuery = entityFrameworkDbContext.Persons;
// FIRST POINT - Here i could make some predefined filtering like 'only active', 'from the same city'... at the database model
personsQueryWithPreDefinedFilters = personsQuery.Where(person => person.active == true);


IQueryable<PersonVO> personsProjectedToVO = personsQueryWithPreDefinedFilters.Select(person => new PersonVO()
{
    id = person.id,
    fullName = person.firstName + " " + person.lastName
});
// SECOND POINT - At this point i could add more filtering based at PersonVO model
if (!String.IsNullOrWhiteSpace(fullNameRequestParameter)) {
    personsProjectedToVO = personsProjectedToVO.Where(personVO => personVO.FullName == fullNameRequestParameter);
}

// The generated SQL at database is with both where (before and after projection)
List<PersonVO> personsToReturn = personsProjectedToVO.ToList();

O que eu recebi em Java é:

CriteriaBuilder cb = this.entityManager.getCriteriaBuilder();
CriteriaQuery<PersonVO> cq = cb.createQuery(PersonVO.class);
Root<Person> root = cq.from(Person.class);
// FIRST POINT - Here i could make some predefined filtering like 'only active', 'from the same city'... at the database model
cq.where(cb.equal(root.get(Person_.active), true));         

Expression<String> fullName = cb.concat(root.get(Person_.firstName), root.get(Person_.lastName));
cq.select(cb.construct(
        PersonVO.class,
        root.get(Person_.id),
        fullName
        ));
// SECOND POINT - At this point i could add more filtering based at PersonVO model??? HOW???
if (fullNameRequestParameter != null) {
    cq.where(cb.equal(fullName, fullNameRequestParameter));
// i only could use based at the fullName expression used, but could i make a Predicate based only on PersonVO model without knowing or having the expression?
}

Eu quero ter separado a "projeção para o modelo VO" da "expressão onde" aplicada a ele, mas aplicada indiretamente se usada uma coluna projetada (como fullName).

Isso é possível em Java? Usando o que? Critério? Querydsl? Corrente? (não adira necessariamente à amostra do java)

jvitor83
fonte
1
Usando Streams, você poderia ter feito algo como - personList.stream().filter(p -> p.active).map(p -> new PersonV0(p.id, p.firstName + " " + p.lastName)).filter(pv -> pv.fullName.equals(fullNameRequestParameter)).collect(Collectors.toList());onde o Predicateusado no ping filterposterior mapse baseiaPersonV0
Naman
Mas para fluxos, toda a "consulta" será resolvida no banco de dados que gera o sql (usando o hibernate) ou funciona apenas com objetos na memória?
jvitor83 12/03
O exemplo acima funcionaria apenas em objetos de memória. É apenas uma dica de como você pode lidar com o código em Java e não como deve implementá-lo com o hibernate na imagem (é por isso que um comentário e não uma resposta)
Naman
1
Entendi! Obrigado pelo comentário @Naman! Vejo que este ORM speedment.com/stream pode permitir o uso stream()para consultar o banco de dados. Eu acho que isso pode responder parcialmente à minha pergunta. Mas vou mantê-lo aberto para ver se alguém pode responder isso com um exemplo concreto (de preferência usando o hibernate como orm).
jvitor83 12/03
Você tem certeza de que o Entity Framework executa o filtro no FullName via SQL (e não na memória)?
Olivier

Respostas:

5

A API de critérios JPA não possui essa funcionalidade. Além disso, não é fácil ler 😊

API de critérios JPA

Na API de critérios, você precisa reutilizar o Expression.

O código de trabalho fica assim:

public List<PersonVO> findActivePersonByFullName(String fullName) {
  CriteriaBuilder cb = entityManager.getCriteriaBuilder();
  CriteriaQuery<PersonVO> cq = cb.createQuery(PersonVO.class);
  Root<Person> root = cq.from(Person.class);

  List<Predicate> predicates = new ArrayList<>();
  predicates.add(cb.equal(root.get("active"), true));

  Expression<String> fullNameExp = 
      cb.concat(cb.concat(root.get("firstName"), " "), root.get("lastName"));

  cq.select(cb.construct(
      PersonVO.class,
      root.get("id"),
      fullNameExp
  ));

  if (fullName != null) {
    predicates.add(cb.equal(fullNameExp, fullName));
  }

  cq.where(predicates.toArray(new Predicate[0]));

  return entityManager.createQuery(cq).getResultList();
}

O código SQL gerado:

select
    person0_.id as col_0_0_,
    ((person0_.first_name||' ')||person0_.last_name) as col_1_0_ 
from
    person person0_ 
where
    person0_.active=? 
    and (
        (
            person0_.first_name||?
        )||person0_.last_name
    )=?

API de Critérios JPA e @org.hibernate.annotations.Formula

O Hibernate possui uma anotação org.hibernate.annotations.Formulaque pode simplificar um pouco o código.

Adicione à entidade um campo computado anotado com @Formula("first_name || ' ' || last_name"):

@Entity
public class Person {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  public Long id;

  @Column
  public String firstName;

  @Column
  public String lastName;

  @Column
  public boolean active;

  @Formula("first_name || ' ' || last_name")
  private String fullName;

  //...getters and setters
}

E na consulta da API JPA Criteria, faça referência ao campo fullName:

public List<PersonVO> findActivePersonByFullName(String fullName) {
  CriteriaBuilder cb = entityManager.getCriteriaBuilder();
  CriteriaQuery<PersonVO> cq = cb.createQuery(PersonVO.class);
  Root<Person> root = cq.from(Person.class);

  List<Predicate> predicates = new ArrayList<>();
  predicates.add(cb.equal(root.get("active"), true));

  cq.select(cb.construct(
      PersonVO.class,
      root.get("id"),
      root.get("fullName")
  ));

  if (fullName != null) {
    predicates.add(cb.equal(root.get("fullName"), fullName));
  }

  cq.where(predicates.toArray(new Predicate[0]));

  return entityManager.createQuery(cq).getResultList();
}

E o SQL gerado:

select
    person0_.id as col_0_0_,
    person0_.first_name || ' ' || person0_.last_name as col_1_0_ 
from
    person person0_ 
where
    person0_.active=? 
    and person0_.first_name || ' ' || person0_.last_name=?

API de critérios de hibernação

A API Hibernate Criteria (descontinuada desde o Hibernate 5.2 em favor da API JPA Criteria) permite usar aliases. Mas nem todos os bancos de dados permitem usar aliases (por exemplo (full_name || ' ' || last_name) as full_name) em uma wherecláusula.

De acordo com os documentos do PostgreSQL :

O nome de uma coluna de saída pode ser usado para se referir ao valor da coluna nas cláusulas ORDER BY e GROUP BY, mas não nas cláusulas WHERE ou HAVING; lá você deve escrever a expressão.

Significa a consulta SQL

select p.id, 
      (p.first_name || ' ' || p.last_name) as full_name 
  from person p
 where p.active = true
   and full_name = 'John Doe'

não funciona no PostgreSQL.

Portanto, usar um alias em uma wherecláusula não é uma opção.

Evgeniy Khyst
fonte
0
public interface PersonVO{
  String getFirstName();
  String getLastName();
}

public interface PersonFullNameView{
  PersonVO getFullName();
}

public interface PersonRepository<Person, Long>{

  @Query("SELECT first_name lastName || ' ' || last_name lastName as fullName" + 
         "FROM Person p" +  
         "WHERE p.active = :active AND p.first_name=:firstName AND" + 
         "p.last_name=:lastname"), nativeQuery = true)
  PersonFullNameView methodName(
                     @Param("active" boolean active, 
                     @Param("firstName") String firstName, 
                     @Param("lastName") String lastNam
                     );

}

Observe que você deve chamar os nomes das colunas como "getters" nas interfaces (getFirstName = firstName)

Ele chama projeção baseada em interface. Então você pode criar uma instância de PersonVO:

PersonFullNameView pfnv = repository.methodName(args...);
PersonVo personVO = pfnv.getFullName();

Era disso que você precisava?

andrew17
fonte
Não inteiramente. Eu quero aplicar a lógica em alguma "API baseada em modelo". Mas obrigado pela resposta.
jvitor83 21/03
0

Usando esta http://www.jinq.org/ library eu poderia fazê-lo e ser aplicado ao hibernate (e consequentemente ao banco de dados).

JinqJPAStreamProvider jinqJPAStreamProvider = new JinqJPAStreamProvider(this.entityManager.getMetamodel());

JPAJinqStream<Person> personStream = jinqJPAStreamProvider.streamAll(this.entityManager, Person.class);
personStream = personStream.where(person -> person.getFirstName().equals("Joao"));

// The only trouble is that we have to register the Model we want to project to (i believe it could be solved with reflection)
jinqJPAStreamProvider.registerCustomTupleConstructor(PersonVO.class.getConstructor(Long.class, String.class), PersonVO.class.getMethod("getId"), PersonVO.class.getMethod("getFullName"));

JPAJinqStream<PersonVO> personVOStream = personStream.select(person -> new PersonVO(person.getId(), person.getFirstName() + person.getLastName()));
personVOStream = personVOStream.where(person -> person.getFullName().equals("JoaoCarmo"));

List<PersonVO> resultList = personVOStream.toList();

Obrigado a todos pela ajuda!

jvitor83
fonte