Java utilizando filtros en diferentes modelos antes y después de la proyección

8

Considere el siguiente modelo JAVA para hibernar :

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

y el siguiente modelo para la serialización de API (utilizando el controlador de Spring Boot Rest):

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

Lo que quiero es:

  • Aplicar un poco de filtrado a la Persona (estáticamente definido)
  • Aplicar un poco de filtrado en PersonVO (obtener de @RequestParam)

En C # .NET podría hacer 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();

Lo que obtuve en Java es:

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?
}

Quiero haber separado la "proyección al modelo VO" de la "expresión de donde" se le aplicó, pero que se aplique indirectamente si se usa una columna proyectada (como fullName).

¿Es esto posible en Java? ¿Usando qué? Criterios? Querydsl? ¿Corriente? (no se quede necesariamente con la muestra de Java)

jvitor83
fuente
1
Usando Streams, podría haber hecho 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());donde se Predicateusa el utilizado en el ping filterposteriormapPersonV0
Naman
Pero para las transmisiones, ¿toda la "consulta" se resolverá en la base de datos que genera el sql (usando hibernate) o solo funciona con objetos en memoria?
jvitor83
Lo anterior funcionaría solo con objetos en memoria. Es solo una pista de cómo puede manejar el código en Java y no cómo debe elegir implementarlo con hibernación en la imagen (por eso es un comentario y no una respuesta)
Naman
1
¡Entendido! Gracias por el comentario @Naman! Veo que este ORM speedment.com/stream puede permitir utilizar stream()para consultar la base de datos. Creo que esto puede responder parcialmente a mi pregunta. Pero lo mantendré abierto para ver si alguien puede responder eso con un ejemplo concreto (preferiblemente usando hibernate como orm).
jvitor83
¿Está seguro de que Entity Framework realiza el filtro en FullName a través de SQL (y no en la memoria)?
Olivier

Respuestas:

5

JPA Criteria API no tiene dicha funcionalidad. Además, no es fácil de leer 😊

API de criterios JPA

En la API de criterios, debe volver a utilizar el Expression.

El código de trabajo se ve así:

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();
}

El código SQL generado:

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 criterios JPA y @org.hibernate.annotations.Formula

Hibernate tiene una anotación org.hibernate.annotations.Formulaque puede simplificar un poco el código.

Agregue a la entidad un campo calculado anotado con @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
}

Y en la consulta API de Criterios JPA, haga referencia al 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();
}

Y el SQL generado:

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 criterios de hibernación

Hibernate Criteria API (en desuso desde Hibernate 5.2 a favor de JPA Criteria API) permite usar alias. Pero no todas las bases de datos permiten usar alias (por ejemplo (full_name || ' ' || last_name) as full_name) en una wherecláusula.

De acuerdo con los documentos de PostgreSQL :

El nombre de una columna de salida se puede usar para referirse al valor de la columna en las cláusulas ORDER BY y GROUP BY, pero no en las cláusulas WHERE o HAVING; allí debes escribir la expresión en su lugar.

Significa la 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'

no funciona en PostgreSQL.

Entonces, usar un alias en una wherecláusula no es una opción.

Evgeniy Khyst
fuente
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
                     );

}

Tenga en cuenta que debe llamar a los nombres de sus columnas iguales a "getters" en las interfaces (getFirstName = firstName)

Se llama proyección basada en interfaz. Luego puede crear una instancia de PersonVO:

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

¿Es eso lo que necesitabas?

andrew17
fuente
No completamente. Quiero aplicar la lógica en alguna "API basada en modelo". Pero gracias por la respuesta.
jvitor83
0

Usando esta biblioteca http://www.jinq.org/ podría hacerlo y aplicarme a la hibernación (y en consecuencia a la base de datos).

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

¡Gracias por toda la ayuda!

jvitor83
fuente