Fetch

使用 fetch,我们可以指定应在关联 Bean 中获取的属性。通过 OneToOne、OneToMany、ManyToOne 和 ManyToMany 关联的 Bean。

Fetch 表示我们希望查询急切加载该路径,并且我们更希望SQL JOIN 的方式完成此操作。

QCustomer cust = QCustomer.alias();
QContact cont = QContact.alias();

List<Customer> customers =
  new QCustomer()
  .select(cust.name, cust.version, cust.whenCreated)    // root level properties
  .contacts.fetch(cont.email)                           // contacts is a OneToMany path

  .name.istartsWith("Rob")
  .findList();

生成的 sql 为

select t0.id, t0.name, t0.version, t0.when_created,    -- customer columns
       t1.id, t1.email                                 -- contact columns
from customer t0
left join contact t1 on t1.customer_id = t0.id
where lower(t0.name) like ? escape'|' order by t0.id

contacts 路径是 OneToMany,因此我们获得到 contact 表的 left join。我们仅获取 contact 的 id 和 email 属性。

使用查询表达式 API 编写的上述查询为

List<Customer> customers =
  database.find(Customer.class)
    .select("name, version, whenCreated")    // root level properties
    .fetch("contacts", "email")              // contacts is a OneToMany path

    .where()
    .istartsWith("name", "Rob")
    .findList();

Fetch 变体 - fetch、fetchQuery、fetchCache、fetchLazy

fetch 有 4 种变体。它们的摘要如下

  • fetch - 急切获取,优先使用 SQL JOIN
  • fetchQuery - 使用单独的辅助 SQL 查询急切获取
  • fetchCache - 急切获取,命中 L2 缓存,缓存未命中使用单独的辅助 SQL 查询获取
  • fetchLazy - 延迟获取

Ebean 在将 ORM 查询转换为 SQL 查询时应用 规则,以便

  • Ebean 不会生成 SQL 笛卡尔积
  • Ebean 将遵守 SQL 中的 maxRows

Ebean 会自动将 fetch 路径转换为 fetchQuery 路径,以确保遵守这些规则。实际上,可以包含在 SQL 查询中的 OneToMany 和 ManyToMany 路径的数量有限(否则我们会得到一个 SQL 笛卡尔积,这是不可取的)。

FetchQuery

当我们指定 fetchQuery 时,我们告诉 Ebean 我们希望急切获取此路径,但我们希望使用单独的 SQL 查询(称为“辅助查询”或“查询联接”)来完成此操作。

我们选择在特定路径上执行此操作,因为我们知道基于基数和结果的相对宽度等因素,SQL 联接与查询联接的相对成本。

List<Order> orders =
  new QOrder()
    .customer.fetchQuery()                // fetch customer (and children) using a "secondary query"
    .customer.billingAddress.fetch()
    .customer.shippingAddress.fetch()
    .findList();

使用上述查询,我们使用 fetchQuery 显式获取客户及其账单和送货地址,并使用第二个单独的 SQL 查询。

主查询
-- "primary" query fetches the orders only
select t0.id, ...
from orders t0
辅助查询(又称“查询联接”)
-- "secondary" query fetches customers with
-- their billing and shipping addresses

select t0.id, t0.inactive, t0.name, ...,                         -- customer
       t1.id, t1.line1, t1.line2, t1.city, ...,                  -- customers billing address
       t2.id, t2.line1, t2.line2, t2.city, ...                   -- customers shipping address
from customer t0
join address t1 on t1.id = t0.billing_address_id
join address t2 on t2.id = t0.shipping_address_id
where t0.id in (?, ?, ... )                                      -- customer ids

FetchCache

fetchCache 类似于 fetchQuery,只不过我们首先命中 L2 缓存。

List<Order> orders =
  new QOrder()
    .customer.fetchCache()         // Hit L2 cache for customer, for cache miss load from database
    .findList();

在上述查询中,查询从数据库中获取订单。对于 Customer,它将命中 Customer 的 L2 缓存,对于缓存未命中,它将从数据库加载 Customer。此操作一次以 100 个客户为一批执行。

FetchLazy

使用 fetchLazy,我们不想急切获取此路径,但如果它被延迟加载,我们正在优化延迟加载查询将获取的内容。

例如,延迟加载 Customer 仅获取名称。
List<Order> orders =
  new QOrder()
    .fetchLazy("customer", "name")
    .findList();

...

// invoke lazy loading will only fetch customer name
customer.getName();
例如,延迟加载 Customer 另外获取 billingAddress 和 shippingAddress。
QCustomer cust = QCustomer.alias();

List<Order> orders =
  new QOrder()
    .fetchLazy("customer")
    .fetch("customer.billingAddress")  // fetched with lazy load of customer
    .fetch("customer.shippingAddress") // fetched with lazy load of customer

    .findList();


...

// invoke lazy loading will fetch customer name + billingAddress + shippingAddress
customer.getName();

别名查询 Bean

使用查询 Bean 时,我们使用 alias Bean 为 selectfetch 提供属性。

// "alias" beans for customer and contact which
// we use to specify the properties to select and fetch
QCustomer cust = QCustomer.alias();
QContact con = QContact.alias();

// using query beans
List<Customer> customers =
  new QCustomer()
  .select(cust.name, cust.version, cust.whenCreated)    // customer properties only
  .contacts.fetch(con.email)                            // contact properties only

  .name.istartsWith("Rob")
  .findList();

 

Kotlin 别名查询 Bean

对于 Kotlin,alias Bean 是查询 Bean 的伴生对象,可通过 _alias 访问。否则,查询在 Kotlin 中是相同的。

val cust = QCustomer._alias
val con = QContact._alias

Select + Fetch = 投影查询调优

使用 selectfetch,我们实际上正在调优查询,控制加载对象图的哪一部分。

通常,我们希望查询仅从数据库加载它需要的内容。这使我们能够降低查询成本、减少获取并通过网络拉取的数据量,以及使用覆盖索引等。

select -> “图形根 级属性”

select 定义在 对象图的根级别 获取什么。

fetch -> “图形叶 属性”

fetch 定义为 对象图的叶 获取什么。这转化为 在对象图的 OneToOne、OneToMany、ManyToOne 和 ManyToMany 路径上获取什么

请注意,FetchGroup 提供了 select + fetch 的替代方案。

ORM 查询到 SQL 查询规则

Ebean 在将 ORM 查询转换为 SQL 查询时会强制执行 2 条规则。这些规则会限制在将 ORM 查询转换为 SQL 查询时允许的 SQL 连接数。这两条规则是

  • Ebean 不会生成 SQL 笛卡尔积
  • Ebean 将遵守 SQL 中的 maxRows

规则 1:无 SQL 笛卡尔积

这条规则暗示 Ebean 最多允许 1 个连接到 OneToMany 路径或 ManyToMany 路径。如果 ORM 查询包含多个此类路径,则 ORM 查询将被转换为多个 SQL 查询并执行。

我们避免 SQL 笛卡尔积,因为它极有可能产生非常糟糕的 SQL 查询。

规则 2:始终在 SQL 中遵守 maxRows

这条规则暗示如果存在 maxRows,Ebean 不能包含任何连接到 OneToMany 路径或 ManyToMany 路径。这是因为 SQL max rows 作用于行,所以我们不能包含基数大于 1 的路径的 SQL 连接。

遵守 SQL 中的 maxRows 非常重要,因为数据库通常可以为我们提供更好的查询计划和更高效的 SQL 查询。

Ebean 检测哪些路径是“ToMany”路径,并确定是否需要将其中任何路径从获取路径转换为获取查询路径。Ebean 首先考虑 @FetchPreference 映射,然后考虑在查询中指定路径的顺序。也就是说,通常第一个 ToMany 路径保持为获取,而其他 ToMany 路径转换为获取查询