在 JPA 投影查询中使用 Record

Java 16 中引入的 Java Record 允许轻松地定义数据类(Data Class),这非常适合用于 JPA 中的投影查询。

Record 不能作为实体类

Record 只能用于投影查询。像 Hibernate 等流行的 JPA 实现创建代理对象时需要无参构造函数、非 final 字段、setter 方法和非 final 的实体类。而这些特性在 Record 中要么被不鼓励使用,要么被明确禁止使用。

Record 和 JPA

如果你在应用中直接使用 JPA,有几种不同的方法可以将记 Record 整合到 DAO 层中。

CriteriaBuilder

Record 可与 CriteriaBuilder 一起使用,如下:

public List<AdvocateRecord> findAllWithCriteriaBuilder() {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<AdvocateRecord> cq 
        = cb.createQuery(AdvocateRecord.class);

    Root<AdvocateEntity> root = cq.from(AdvocateEntity.class);

    cq.select(cb.construct(
            AdvocateRecord.class, 
            root.get("id"), 
            root.get("fName"), 
            root.get("lName"),
            root.get("region"), 
            root.get("twitterFollowers")));

    TypedQuery<AdvocateRecord> q = em.createQuery(cq);
    return q.getResultList();
}

TypedQuery

Record 也可以与 TypedQuery 一起使用,但需要在 JPQL 查询中提供完整类路径的构造函数。

public List<AdvocateNameRecord> 
    findAdvocateNamesByRegionTypedQuery(String region) {

    TypedQuery<AdvocateNameRecord> query = em.createQuery("""
            SELECT
            new com.bk.records.AdvocateNameRecord(a.fName, a.lName)
            FROM AdvocateEntity a
            WHERE region = :region
            """, AdvocateNameRecord.class);

    query.setParameter("region", region);

    return query.getResultList();
}

NativeQuery

Record 也可与 NativeQuery 一起使用。需要提供一个 Mapping 来处理从查询到 Record 字段的映射,就像下面示例中的 AdvocateNameRecordMapping 一样:

public List<AdvocateNameRecord> 
    findAdvocateNamesByIdNativeQuery(int id) {
    Query query = em.createNativeQuery("""
            SELECT
            f_name, l_name
            FROM advocates
            WHERE id = :id
            """, 
            "AdvocateNameRecordMapping");

    query.setParameter("id", id);
    return query.getResultList();
}

Mapping 定义

AdvocateNameRecordMapping Mapping 定义在 AdvocateEntity 实体类中:

@Entity
@Table(name = "advocates")
@SqlResultSetMapping(
        name = "AdvocateNameRecordMapping",
        classes = @ConstructorResult(
            targetClass = AdvocateNameRecord.class,
            columns = { 
                        @ColumnResult(name = "f_name"), 
                        @ColumnResult(name = "l_name")}))
public class AdvocateEntity {
...

Record 和 Spring Data

与直接使用 JPA 一样,使用 Spring Data 时也有几种使用 Record 的方法。

自动映射

如果 Record 与实体的字段相匹配,Spring Data 可以自动处理查询返回的映射,如下例所示:

public interface AdvocateRepo 
    extends CrudRepository<AdvocateEntity, Integer> {

    Iterable<AdvocateRecord> findByRegion(String region);
}

Record

Record AdvocateRecord@EntityAdvocateEntity 的字段相匹配:

public record AdvocateRecord(
int id, 
String fName, 
String lName, 
String region, 
int twitterFollowers) {}

Entity

public class AdvocateEntity {
    @Id
    private int id;
    private String fName;
    private String lName;
    private String region;
    private int twitterFollowers;
    ...
}

Query

Spring Data 还允许在 @Query 中使用 JPQL 查询:

public interface AdvocateRepo 
    extends CrudRepository<AdvocateEntity, Integer> {
    @Query("""
            SELECT 
            new com.bk.records.AdvocateNameRecord(a.fName, a.lName)
            FROM AdvocateEntity a
            WHERE region = ?1
            """)
    Iterable<AdvocateNameRecord> findNamesByRegion(String region);
}

自定义 Repository 实现

Spring Data 还支持 自定义 Repository 实现,这些实现也可用于处理查询返回到 Record 类的映射。

要使用自定义 Repository 实现,需要定义一个接口:

public interface CustomAdvocateRepo {
    Iterable<AdvocateNameRecord> findAllNameRecords();
}

Spring Data Repository 继承该接口:

public interface AdvocateRepo 
    extends CrudRepository<AdvocateEntity, Integer>,
        CustomAdvocateRepo {
}

并提供 Repository 的实现。本例中使用了 RowMapper 来处理查询结果的映射:

public class CustomAdvocateRepoImpl implements CustomAdvocateRepo {
    private JdbcTemplate jdbcTemplate;

    protected CustomAdvocateRepoImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    class AdvocateRecordDtoRowMapper 
    implements RowMapper<AdvocateNameRecord> {
        @Override
        public AdvocateNameRecord 
        mapRow(ResultSet rs, int rowNum) throws SQLException {
            return new AdvocateNameRecord(
                    rs.getString("f_name"), rs.getString("l_name"));
        }
    }

    @Override
    public Iterable<AdvocateNameRecord> findAllNameRecords() {
        return jdbcTemplate.query(
        "SELECT f_name, l_name FROM advocates", 
            new AdvocateRecordDtoRowMapper());

    }
}

进阶阅读


参考:https://wkorando.github.io/sip-of-java/015.html