Spring Boot Jpa JPQL 选择除特定列之外的列

Posted

技术标签:

【中文标题】Spring Boot Jpa JPQL 选择除特定列之外的列【英文标题】:Spring Boot Jpa JPQL Selecting columns except specific columns 【发布时间】:2020-04-04 07:13:11 【问题描述】:

例如,我有两个具有OneToOne 关联的实体。

@Entity
class Entity1(
    @Column val columnToSelect1: String,
    @Column val columnToSelect2: String,
    @Column val columnToSelect3: String,
    @Column val columnToSelect4: String,
    @Column val columnToSelect5: String,
    @Column val columnToSelect6: String,
    @Column val columnToSelect7: String,
    @Column val columnToSelect8: String,
    @Column val columnToSelect9: String,
    @Column val columnToSelect10: String,


    @OneToOne
    @JoinColumn
    val columnNotToSelect: Entity2
)

而且,除了 val columnNotToSelect: Entity2 这样的特定列之外,我还想选择很多次。 这是因为选择 Entity2 会导致触发另一个并非总是需要的查询。

目前,我正在像这样实现该要求。

interface Entity1Getter 
    fun getColumnToSelect1(): String
    fun getColumnToSelect2(): String
    fun getColumnToSelect3(): String
    ...


interface Entity1CrudRepository : CrudRepository<Entity1, UUID> 
    // select all columns
    fun findAll(): List<Entity1>

    // select all columns except columnNotToSelect
    @Query(
        "SELECT " +
        "e.columnToSelect1 as columnToSelect1" +
        "e.columnToSelect2 as columnToSelect2" +
        "e.columnToSelect3 as columnToSelect3" +
        "e.columnToSelect4 as columnToSelect4" +
        "e.columnToSelect5 as columnToSelect5" +
        "e.columnToSelect6 as columnToSelect6" +
        "e.columnToSelect7 as columnToSelect7" +
        "e.columnToSelect8 as columnToSelect8" +
        "e.columnToSelect9 as columnToSelect9" +
        "e.columnToSelect10 as columnToSelect10" +
        "FROM Entity1 e"
    )
    fun findAllExceptOneColumn(): List<Entity1Getter>

我必须排列所有要查询的列,效率很低。

问题

    除了特定的列之外,还有什么不同的方法可以选择? 或者,有什么不同的方法可以不选择关联的列? (不触发另一个查询)

谢谢:D

【问题讨论】:

使 OneToOne 关联变得懒惰。它会执行一个额外的查询,因为你让它急切地离开了。 【参考方案1】:

JPA 世界中所需的行为称为EntityGraph。您可以创建一个特定的实体图(或一组图,如果需要多个)。然后标记一个特定的查询方法来使用这个实体图。作为返回结果,您将获取原始实体,但不在图中的所有其他属性将处于 LAZY 获取模式。

例如:

@NamedEntityGraph(
    name = "Entity1.exceptColumn", 
    attributeNodes = 
        @NamedAttributeNode("columnToSelect1"), 
        @NamedAttributeNode("columnToSelect2"), 
        @NamedAttributeNode("columnToSelect3"), 
        @NamedAttributeNode("columnToSelect4"), 
        @NamedAttributeNode("columnToSelect5"), 
        @NamedAttributeNode("columnToSelect6"), 
        @NamedAttributeNode("columnToSelect7"), 
        @NamedAttributeNode("columnToSelect8"), 
        @NamedAttributeNode("columnToSelect9"), 
        @NamedAttributeNode("columnToSelect10")
    
)
@Entity
class Entity1(
    @Column val columnToSelect1: String,
    @Column val columnToSelect2: String,
    @Column val columnToSelect3: String,
    @Column val columnToSelect4: String,
    @Column val columnToSelect5: String,
    @Column val columnToSelect6: String,
    @Column val columnToSelect7: String,
    @Column val columnToSelect8: String,
    @Column val columnToSelect9: String,
    @Column val columnToSelect10: String,

    @OneToOne
    @JoinColumn
    val columnNotToSelect: Entity2
)

然后在您的数据存储库中:

interface Entity1CrudRepository : CrudRepository<Entity1, UUID> 
    // select all columns
    fun findAll(): List<Entity1>

    // select all columns except columnNotToSelect
    @EntityGraph(value = "Entity1.exceptColumn")
    @Query("SELECT e FROM Entity1 e ")
    fun findAllExceptOneColumn(): List<Entity1>


注意:您使用的是相同的Entity1 类,但实体图中的属性以 LAZY fetch 模式标记。

实现此目的的其他方法是使用投影查询,在这种情况下,您正在创建 DTO,并为此编写查询:

data class Entity1Projection (
    val columnToSelect1: String,
    val columnToSelect2: String,
    val columnToSelect3: String,
    val columnToSelect4: String,
    val columnToSelect5: String,
    val columnToSelect6: String,
    val columnToSelect7: String,
    val columnToSelect8: String,
    val columnToSelect9: String,
    val columnToSelect10: String
)

然后在您的存储库中,您可以定义以下内容:

interface Entity1CrudRepository : CrudRepository<Entity1, UUID> 
    // select all columns
    fun findAll(): List<Entity1>

    // select all columns except columnNotToSelect
    @Query("""
    SELECT new fully.qualified.package_name.Entity1Projection(
        e.columnToSelect1, 
        e.columnToSelect2, 
        e.columnToSelect3, 
        e.columnToSelect4, 
        e.columnToSelect5, 
        e.columnToSelect6, 
        e.columnToSelect7, 
        e.columnToSelect8, 
        e.columnToSelect9, 
        e.columnToSelect10
    ) 
    FROM Entity1 e """)
    fun findAllExceptOneColumn(): List<Entity1Projection>


每种方法都有其优点和缺点,您决定使用哪种方法。

【讨论】:

【参考方案2】:

@Ilya Dyoshin 在很多方面都是对的,但也有一些陷阱。 首先,@OneToOne 是这样一种关联类型,在双向映射的情况下不能轻易获取惰性。这是因为 Hibernate 没有其他方法可以知道是否将 null 或 Proxy 分配给此变量。 在这种情况下,至少有 3 个选项:

使用Bytecode enhancement 在子端使用@MapsId@oneToOne注解 声明columnNotToSelect 关联不是@OneToOne,而是@OneToMany List&lt;Val&gt;,并始终返回columnNotToSelect.get(0)。 Here is a fine detalization 第一两例。

说到单向关联,它也不是第一眼看上去那么容易。正如@Ilya Dyoshin 之前所写,EntityGraph 是一件非常好的事情,但您应该记住,有两个提示定义了图形选择行为:"javax.persistence.loadgraph" 和 “javax.persistence.fetchgraph”。 In theory 必须按照EntityGraphType 工作:

FETCH: 当使用javax.persistence.fetchgraph属性指定实体图时,实体图的属性节点指定的属性被视为FetchType.EAGER,未指定的属性被视为FetchType.LAZY LOAD: 使用javax.persistence.loadgraph属性指定实体图时,实体图的属性节点指定的属性按FetchType.EAGER处理,未指定的属性按FetchType.EAGER处理到他们指定或默认的 FetchType

但在实践中,它几乎总是依赖于一些微妙之处,包括 JPA 提供程序(Hibernate、EclipseLink 等)及其优化器

让我们看一个小例子


数据库

create table debt(
    id bigint identity(1,1),    
    contract nvarchar(128), 
    constraint PK_DEBT primary key(id)
)

create table debt_detail(   
    r_debt_id int not null unique,
    dict_value_1 int,
    number_value_1 money,
    constraint PK_DEBT_DETAIL primary key(r_debt_id),
    constraint FK_DEBT_DETAIL_DEBT_ID foreign key (r_debt_id)references debt(id)
)

insert into debt(contract)
values(N'aaa',N'bbb',N'ccc')

insert into debt_detail(r_debt_id, dict_value_1, number_value_1)
select id, 1, 2.2
from debt 
where id in (1, 2, 3)

实体

@Table(name = "debt")
@Entity
@NamedEntityGraphs(
    @NamedEntityGraph(name = "testDebt", attributeNodes = 
        @NamedAttributeNode(value = "id"),
        @NamedAttributeNode(value = "contract")
    )
)

public class Debt 

  @Id
  @Basic(optional = false)
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "ID")
  private Long id;
  @Column(name = "contract")
  private String contract;

  @OneToOne()
  @JoinColumn(name = "ID", referencedColumnName = "R_DEBT_ID", insertable = false, updatable = false, nullable = false)
  private DebtDetail debtDetail;

  ************


@Table(name = "debt_detail")
@Entity
public class DebtDetail 

  @Id
  @Column(name = "R_DEBT_ID")
  @Basic(optional = false)
  private Long refDebtId;
  @Column(name = "dict_value_1")
  private String dictValue1;
  @Column(name = "number_value_1")
  private String numberValue1;

  ******************

回购

@Repository
public interface DebtRepository extends CrudRepository<Debt, Long> 

  @Override
  Debt findOne(Long id);

  @EntityGraph(value = "testDebt", type = EntityGraphType.LOAD)
  Debt findById(Long id);

  @EntityGraph(value = "testDebt", type = EntityGraphType.FETCH)
  Debt findDebtById(Long id);

经理

@Component
public class DebtManager 

  private static final Logger logger = LoggerFactory.getLogger(DebtManager.class);

  @Autowired
  private DebtRepository debtRepository;

  public void debt()     
    logger.info("findOne start");
    Debt debt = debtRepository.findOne(1L);
    logger.info("findOne stop. ", debt.getDebtDetail().getClass());
    //logger.info("findOne stop. ", debt.getDebtDetail());

    logger.info("load start");
    Debt debtLoad = debtRepository.findById(2L);
    logger.info("load stop. ", debtLoad.getDebtDetail().getClass());
    //logger.info("load stop. ", debtLoad.getDebtDetail());

    logger.info("fetch start");
    Debt debtFetch = debtRepository.findDebtById(3L);
    logger.info("fetch stop. ", debtFetch.getDebtDetail().getClass());
    //logger.info("fetch stop. ", debtFetch.getDebtDetail());
  

现在让我们调用debtManager.debt() 并查看日志: debtRepository.findOne(1L) 会产生这样的结果:

select
        debt0_.id as id1_0_0_,
        debt0_.contract as contract2_0_0_,
        debtdetail1_.r_debt_id as r_debt_i1_4_1_,
        debtdetail1_.dict_value_1 as dict_val2_4_1_,
        debtdetail1_.number_value_1 as number_v3_4_1_ 
    from
        debt debt0_ 
    left outer join
        debt_detail debtdetail1_ 
            on debt0_.id=debtdetail1_.r_debt_id 
    where
        debt0_.id=?

...INFO ... - findOne 停止。类 ....DebtDetail

和其他查询:

    select
        debt0_.id as id1_0_,
        debt0_.contract as contract2_0_ 
    from
        debt debt0_ 
    where
        debt0_.id=?

    select
        debtdetail0_.r_debt_id as r_debt_i1_4_0_,
        debtdetail0_.dict_value_1 as dict_val2_4_0_,
        debtdetail0_.number_value_1 as number_v3_4_0_ 
    from
        debt_detail debtdetail0_ 
    where
        debtdetail0_.r_debt_id=?

...INFO ... - 加载停止。类 ....DebtDetail

...INFO ... - 获取停止。类 ....DebtDetail

所有请求都包含对debt_detail 的查询。它发生的原因是 JPA 中 @OneToOne 的默认行为是 FetchType.EAGER 并且映射没有强制标志(可选 = false)。 “图形化”和“非图形化”查询之间的差异是由于最后一个没有强定义,Hibernate 获取所有连接到Debt 实体,而“图形化”查询总是获取 certan 图中的所有实体,但在我们的例子中@NamedEntityGraph 注释不包含任何实体。 更重要的一件事 - 在所有情况下,获取的类型都是 DebtDetail 即实体类

现在让我们在Debt 类的@OneToOne 定义中添加一些内容: @OneToOne(fetch = FetchType.LAZY, optional = false)

日志已更改 - 对于所有情况,结果相同:

select
    debt0_.id as id1_0_,
    debt0_.contract as contract2_0_ 
from
    debt debt0_ 
where
    debt0_.id=?

这是因为我们直接定义了LAZY 策略。但这不是唯一的变化。 DebtDetail 类日志记录必须如下所示:

...INFO ... - 加载停止。类 .... DebtDetail_$$_jvst2bb_0

这意味着连接的实体已被代理,并且父实体(债务)具有对代理对象的引用,该代理对象不包含除主键之外的具体实体的任何数据。这就是为什么对getDebtDetail().getClass() 的处理不会导致DebtDetail 获取 - 我们读取的是代理类属性,而不是具体的实体类。

现在让我们添加 DebtDetail 直读:使用 debt.getDebtDetail() 取消注释代码日志就足够了。 日志已再次更改 - 对于所有情况,结果相同:

select
        debt0_.id as id1_0_,
        debt0_.contract as contract2_0_ 
    from
        debt debt0_ 
    where
        debt0_.id=?

...INFO  ... - load stop. class ....DebtDetail_$$_jvstadd_0

    select
        debtdetail0_.r_debt_id as r_debt_i1_4_0_,
        debtdetail0_.dict_value_1 as dict_val2_4_0_,
        debtdetail0_.number_value_1 as number_v3_4_0_ 
    from
        debt_detail debtdetail0_ 
    where
        debtdetail0_.r_debt_id=?

正如您现在所看到的,链条是:读取父级 -> 记录子级的类类型(代理)-> 延迟读取子级,其中代理对象将“未代理”

如果我们将传播策略更改为EAGER,那么这两个实体都将在父实体读取期间被读取。

总结 EntityGraphes 的使用情况,花时间调查您的 JPA 提供程序行为及其根据注释定义的变化。

Here is 又一篇关于实体图的好文章

说到Projections

没有必要创建一个新类——一个接口就足够了:

public interface DebtProjection 
  Long getId();

  String getContract();


@Repository
public interface DebtRepository extends CrudRepository<Debt, Long> 

  DebtProjection getById(Long id);

它的明显优势是独立于任何关系 - 它只会返回已定义的属性

【讨论】:

【参考方案3】:

我将在其他答案中添加延迟初始化在这里不起作用的原因是因为您引用的实体包含 JoinColumn。如果 joincolumn 在引用 Entity2 的实体内,则惰性关联将起作用。另一位同事正确地说,在Entity2包含JoinColumn的情况下,使用字节码检测可以解决您的延迟加载问题。他也正确地说hibernate无法理解关联的集合是null还是Proxy,但这只是如果实体引用不包含连接列,则更正。只要实体持有joincolumn就没有问题。

让我们从数据库的角度来看。

通过此设置,延迟加载将从 entity1 工作到 entity2。

table Entity1 (
   id: varchar,
   entity2_id:varchar
)

table Entity2 (
   id: varchar
)

使用此设置,延迟加载将无法从 entity1 到 entity2。

table Entity1 (
       id: varchar,

    )

    table Entity2 (
       id: varchar
       entity1_id:varchar
    )

除非您使用字节码检测或不使用 OneToOne 关系,否则您将使用 OneToMany 双向事件,尽管它是 OneToOne 并公开与 OneToOne 关系相对应的适当接口,而不是公开集合。

【讨论】:

以上是关于Spring Boot Jpa JPQL 选择除特定列之外的列的主要内容,如果未能解决你的问题,请参考以下文章

JPQL 的 Spring Boot JPA“查询验证失败”错误

Spring jpa jpql查询

Spring JPA 和 Hibernate 的 JPQL 查询错误

没有 JPQL 查询的 Spring-data-jpa 中的 CURRENT_DATE

如何在 JPQL(Spring JPA 查询)中使用 JOIN 执行 UPDATE 语句?

无法在 FROM 子句中使用子查询编写 JPQL 查询 - Spring Data Jpa - Hibernate