Java mit Filterung bei verschiedenen Modellen vor und nach der Projektion

8

Betrachten Sie das folgende JAVA-Modell für den Ruhezustand :

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

und das folgende Modell für die API-Serialisierung (unter Verwendung des Spring Boot Rest Controllers):

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

Was ich will ist:

  • Lassen Sie die Person filtern (statisch definiert)
  • Lassen Sie einige Filter bei der PersonVO anwenden (erhalten Sie von @RequestParam)

In C # .NET könnte ich wie folgt machen:

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

Was ich in Java bekommen habe ist:

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

Ich möchte die "Projektion auf das VO-Modell" von dem darauf angewendeten "where-Ausdruck" getrennt haben, habe sie aber indirekt angewendet, wenn eine projizierte Spalte (wie fullName) verwendet wird.

Ist das in Java möglich? Mit was? Kriterien? Querydsl? Strom? (Halten Sie sich nicht unbedingt an die Java-Probe)

jvitor83
quelle
1
Mit Streams hätten Sie so etwas wie - 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());wo der Predicateim filterAfter- mapPing verwendete basiertPersonV0
Naman
Aber für Streams wird die gesamte "Abfrage" in der Datenbank gelöst, die die SQL generiert (im Ruhezustand), oder funktioniert sie nur mit In-Memory-Objekten?
jvitor83
Das obige würde nur in Speicherobjekten funktionieren. Es ist nur ein Hinweis darauf, wie Sie mit dem Code in Java umgehen können und nicht, wie Sie ihn im Ruhezustand im Bild implementieren sollten (deshalb ein Kommentar und keine Antwort)
Naman
1
Ich habs! Danke für den Kommentar @Naman! Ich sehe, dass dieses ORM speedment.com/stream das stream()Abfragen der Datenbank ermöglichen kann. Ich denke, das kann meine Frage teilweise beantworten. Aber ich werde es offen halten, um zu sehen, ob jemand dies mit einem konkreten Beispiel beantworten kann (vorzugsweise mit Ruhezustand als Orm).
jvitor83
Sind Sie sicher, dass Entity Framework den Filter für FullName über SQL (und nicht im Speicher) ausführt?
Olivier

Antworten:

5

Die JPA Criteria API verfügt nicht über solche Funktionen. Es ist auch nicht leicht zu lesen 😊

JPA Criteria API

In der Kriterien-API müssen Sie die wiederverwenden Expression.

Der Arbeitscode sieht folgendermaßen aus:

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

Der generierte SQL-Code:

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
    )=?

JPA Criteria API und @org.hibernate.annotations.Formula

Der Ruhezustand hat eine Anmerkung org.hibernate.annotations.Formula , die den Code ein wenig vereinfachen kann.

Fügen Sie der Entität ein berechnetes Feld hinzu, das mit Anmerkungen versehen ist @Formula("first_name || ' ' || last_name") folgenden :

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

Verweisen Sie in der JPA Criteria API-Abfrage auf das Feld 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();
}

Und das generierte SQL:

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

Hibernate Criteria API

Die Hibernate Criteria API (seit Hibernate 5.2 zugunsten der JPA Criteria API veraltet) ermöglicht die Verwendung von Aliasen. Aber nicht alle Datenbanken erlauben die Verwendung von Aliasen (z(full_name || ' ' || last_name) as full_name ) in awhere Klausel zu verwenden.

Laut den PostgreSQL-Dokumenten :

Der Name einer Ausgabespalte kann verwendet werden, um auf den Wert der Spalte in den Klauseln ORDER BY und GROUP BY zu verweisen, nicht jedoch in den Klauseln WHERE oder HAVING. dort müssen Sie stattdessen den Ausdruck ausschreiben.

Es bedeutet die SQL-Abfrage

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

funktioniert nicht in PostgreSQL.

Die Verwendung eines Alias ​​in einer whereKlausel ist daher keine Option.

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

}

Beachten Sie, dass Sie Ihre Spaltennamen in Schnittstellen gleich "getters" nennen müssen (getFirstName = firstName)

Es ruft eine schnittstellenbasierte Projektion auf. Dann können Sie eine Instanz erstellen von PersonVO:

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

Benötigen Sie das?

andrew17
quelle
Nicht komplett. Ich möchte die Logik auf eine "modellbasierte API" anwenden. Aber danke für die Antwort.
jvitor83
0

Mit dieser http://www.jinq.org/ Bibliothek könnte ich es tun und auf den Ruhezustand (und folglich die Datenbank) angewendet werden.

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

Vielen Dank für die Hilfe!

jvitor83
quelle