Java在投影前后在不同模型上使用过滤

jvi*_*r83 9 java hibernate projection querydsl java-stream

考虑以下用于hibernate 的JAVA 模型:

@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;
}
Run Code Online (Sandbox Code Playgroud)

以及以下 API 序列化模型(使用spring boot rest 控制器):

public class PersonVO {
    public Long id;
    public String fullName;
}
Run Code Online (Sandbox Code Playgroud)

我想要的是:

  • 对 Person 应用一些过滤(静态定义)
  • 在 PersonVO 应用一些过滤(从@RequestParam 获取)

C# .NET 中,我可以像这样:

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();
Run Code Online (Sandbox Code Playgroud)

我在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?
}
Run Code Online (Sandbox Code Playgroud)

我想将“投影到 VO 模型”与应用于它的“where 表达式”分开,但如果使用投影列(如 fullName),则间接应用它。

这在Java中可能吗?用什么?标准?查询dsl?溪流?(不一定要坚持Java示例)

Evg*_*yst 5

JPA Criteria API 没有这样的功能。而且读起来也不容易

JPA 标准 API

在 Criteria API 中,您需要重用Expression.

工作代码如下所示:

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();
}
Run Code Online (Sandbox Code Playgroud)

生成的 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
    )=?
Run Code Online (Sandbox Code Playgroud)

JPA 标准 API 和 @org.hibernate.annotations.Formula

Hibernate 有一个注解org.hibernate.annotations.Formula,可以稍微简化代码。

向实体添加一个用 注释的计算字段@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
}
Run Code Online (Sandbox Code Playgroud)

在 JPA Criteria API 查询中引用字段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();
}
Run Code Online (Sandbox Code Playgroud)

以及生成的 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=?
Run Code Online (Sandbox Code Playgroud)

休眠条件 API

Hibernate Criteria API(自 Hibernate 5.2 起弃用,支持 JPA Criteria API)允许使用别名。但并非所有数据库都允许(full_name || ' ' || last_name) as full_namewhere子句中使用别名(例如)。

根据PostgreSQL 文档

输出列的名称可用于在 ORDER BY 和 GROUP BY 子句中引用列的值,但不能在 WHERE 或 HAVING 子句中引用;在那里你必须写出表达式。

这意味着 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'
Run Code Online (Sandbox Code Playgroud)

在 PostgreSQL 中不起作用。

因此,在where子句中使用别名不是一种选择。


jvi*_*r83 0

使用这个http://www.jinq.org/库,我可以做到这一点并应用于休眠(以及数据库)。

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();
Run Code Online (Sandbox Code Playgroud)

感谢大家的帮助!