Java在投影之前和之后的不同模型中使用过滤。

9

考虑以下JAVA模型,用于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;
}

而下面是API序列化的模型(使用 Spring Boot Rest Controller):

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

我希望实现以下功能:

  • 对于 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();

我掌握的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?
}

我希望将“投影到VO模型”的操作与应用于它的“where表达式”分离,但如果使用了像fullName这样的投影列,则间接应用它。
这在Java中可行吗?使用什么工具?Criteria?Querydsl?Stream?(不一定局限于Java示例)

1
使用 Stream,您可以像这样做 - 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());map 之后使用的 Predicate 基于 PersonV0 - Naman
1
明白了!感谢 @Naman 的评论!我看到这个 ORM https://speedment.com/stream/ 可以允许使用 stream() 来查询数据库。我认为这可以部分回答我的问题。但我会保持开放状态,看看是否有人能用具体的例子来回答(最好使用 hibernate 作为 ORM)。 - jvitor83
你确定 Entity Framework 通过 SQL 对 FullName 进行筛选(而不是在内存中)吗? - Olivier
是的,@Olivier!我确定。只要您不执行IQueryable,它就会继续包括转换/过滤等操作以生成SQL,并在ToList()上执行。 - jvitor83
真的吗?需要分析闭包的代码(可能是任意复杂的)才能推断出正确的SQL语句。这将非常困难... - Olivier
显示剩余2条评论
3个回答

5

JPA Criteria API没有这样的功能。而且,它不容易阅读。

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

生成的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
    )=?

JPA Criteria 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
}

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

生成的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

Hibernate Criteria API已经被弃用(自Hibernate 5.2开始),推荐使用JPA Criteria API。Hibernate Criteria API允许使用别名,但并非所有数据库都允许在where子句中使用别名(例如(full_name || ' ' || last_name) as full_name)。

根据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'

在 PostgreSQL 中无法使用它。

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


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

}

请注意,您必须将列名与接口中的“getter”名称相同(例如getFirstName = firstName)。
这被称为基于接口的投影。然后,您可以创建PersonVO的实例:
PersonFullNameView pfnv = repository.methodName(args...);
PersonVo personVO = pfnv.getFullName();

这是你需要的吗?


不完全是。我想将这个逻辑应用于某个“基于模型的API”。但还是谢谢你的回答。 - jvitor83

0

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

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

感谢大家的帮助!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接