DBIx::Class - 使用预取获取用作条件的所有关系?

DBIx::Class - get all relationship that was used as a condition using prefetch?

这里有三个 table:productmodelproduct_model,它们将产品和模型映射成 N:M 关系。

product          product_model            model

id name          product_id model_id      id name
------------     -------------------      ----------
p1 Product 1     p1         m1            m1 Model 1
p2 Product 2     p2         m1            m2 Model 2
...              p2         m2

我想做的事:找到所有支持模型 2 的产品(例如 product 2)。然后,对于每个产品,显示该产品支持的 model_ids 列表(product 2 => [ m1,m2 ])

这是我的第一次尝试。我需要 N 个查询来为每个产品搜索 model_ids。

# 1 query for searching products
my @products = $schema->resultset('Product')->search(
   { 'product_models.model_id' => 'm2' },
   { 'join' => 'product_model' },
)
# N queries for searching product_models for each product
foreach my $product ( @products ) {
   my @model_ids = map { $_->model_id } $product->product_models;
   # @model_ids = ( 'm1', 'm2' ) for p2
}

我正在寻找一种仅使用一次查询即可获得结果的方法。将 join 替换为 prefetch 无效。

my @products = $schema->resultset('Product')->search(
   { 'product_models.model_id' => 'm2' },
   { 'prefetch' => 'product_model' },      # here
)

# no additional queries, but...
foreach my $product ( @products ) {
   my @model_ids = map { $_->model_id } $product->product_models;
   # now, @model_ids contains only ( `m2` )
}

接下来,我尝试了"prefetch same table twice":

my @products = $schema->resultset('Product')->search(
    { 'product_models.model_id' => 'm2' },
    { 'prefetch' => [ 'product_models', 'product_models' ] },
);

foreach my $product ( @products ) {
    my @model_ids = map { $_->model_id } $product->product_models;
}

看来我成功了。只执行了一个查询,我从中获得了所有模型 ID。

但是我不太确定这是正确的(?)方式。这是正确的做法吗?

例如,如果我使用 join 而不是 prefetching,Product 2 会在循环中出现两次。我明白了,因为加入的 table 就像:

id name      p_m.p_id p_m.m_id p_m_2.p_id p_m_2.m_id
p2 Product 2 p2       m2       p2         m1
p2 Product 2 p2       m2       p2         m2   -- Product 2, one more time

为什么我用prefetchProduct 2只出现一次?

除了 SELECT 个字段的不同之外,结果查询几乎相同:

SELECT "me"."id", "me"."name",
 "product_models"."product_id", "product_models"."model_id",    -- only in prefetch
 "product_models_2"."product_id", "product_models_2"."model_id" --
  FROM "product" "me"
  LEFT JOIN "product_model" "product_models"
    ON "product_models"."product_id" = "me"."id"
  LEFT JOIN "product_model" "product_models_2"
    ON "product_models_2"."product_id" = "me"."id"
WHERE "product_models"."model_id" = 'm2'

如果您的架构中有正确的关系,则可以通过单个查询实现。但这很棘手。假设您的数据库如下所示:

CREATE TABLE product
 (`id` VARCHAR(2) PRIMARY KEY, `name` VARCHAR(9))
;
INSERT INTO product
 (`id`, `name`) VALUES
 ('p1', 'Product 1'),
 ('p2', 'Product 2')
;
CREATE TABLE product_model (
  `product_id` VARCHAR(2),
  `model_id` VARCHAR(2),
  PRIMARY KEY (product_id, model_id),
  FOREIGN KEY(product_id) REFERENCES product(id),
  FOREIGN KEY(model_id) REFERENCES model(id)
)
;
INSERT INTO product_model
 (`product_id`, `model_id`) VALUES
 ('p1', 'm1'),
 ('p2', 'm1'),
 ('p2', 'm2')
;
CREATE TABLE model
 (`id` VARCHAR(2) PRIMARY KEY, `name` VARCHAR(7))
;
INSERT INTO model
 (`id`, `name`) VALUES
 ('m1', 'Model 1'),
 ('m2', 'Model 2')
;

这基本上是您从问题中得出的数据库。我添加了主键和外键。反正你可能有那些。

我们现在可以从中创建模式。我写了一个简单的程序,使用 DBIx::Class::Schema::Loader 来做到这一点。它会即时创建一个 SQLite 数据库。 (如果没有人把它放在 CPAN 上,我会)。

上面的 SQL 将放在 __DATA__ 部分。

use strict;
use warnings;
use DBIx::Class::Schema::Loader qw/ make_schema_at /;

# create db
unlink 'foo.db';
open my $fh, '|-', 'sqlite3 foo.db' or die $!;
print $fh do { local $/; <DATA> };
close $fh;

$ENV{SCHEMA_LOADER_BACKCOMPAT} = 1;

# create schema
my $dsn = 'dbi:SQLite:foo.db';
make_schema_at(
    'DB',
    {
        #    debug => 1,
    },
    [ $dsn, 'sqlite', '', ],
);

$ENV{DBIC_TRACE} = 1;

# connect schema
my $schema = DB->connect($dsn);

# query goes here

__DATA__
# SQL from above

现在我们已经有了,我们可以专注于查询。起初这看起来很可怕,但我会尽力解释。

my $rs     = $schema->resultset('Product')->search(
    { 'product_models.model_id' => 'm2' },
    {
        'prefetch' => {
            product_models => {
                product_id => {
                    product_models => 'model_id'
                }
            }
        }
    },
);

while ( my $product = $rs->next ) {
    foreach my $product_model ( $product->product_models->all ) {
        my @models;
        foreach my $supported_model ( $product_model->product_id->product_models->all ) {
            push @models, $supported_model->model_id->id;
        }
        printf "%s: %s\n", $product->id, join ', ', @models;
    }
}

prefetch 表示加入此关系,并保留数据供以后使用。因此,要获得您产品的所有型号,我们必须编写

#          1                   2
{ prefetch => { product_models => 'product_id' } }

其中 product_models 是 N:M table,product_id 是模型 table 的关系名称。箭头 => 1 表示从 ProductProductModel 的第一个连接。 2 用于 ProductModel 返回具有模型 m2 的每个产品。参见 ER 模型的图解。

现在我们想要拥有这个 Product 拥有的所有 ProductModel。那是箭头 3.

#          1                   2               3
{ prefetch => { product_models => { product_id => 'product_models' } } }

最后,要获得 N:M 关系的 Model,我们必须使用带有箭头 4model_id 关系车间.

{
    'prefetch' => {                            # 1
        product_models => {                    # 2
            product_id => {                    # 3
                product_models => 'model_id'   # 4
            }
        }
    }
},

看看ER模型图应该就清楚了。请记住,默认情况下,每个连接都是 LEFT OUTER 连接,因此它将始终获取所有行,而不会丢失任何内容。 DBIC 会为您解决这些问题。

现在要访问所有这些,我们需要迭代。 DBIC 为我们提供了一些工具来做到这一点。

while ( my $product = $rs->next ) {

    #                                   1
    foreach my $product_model ( $product->product_models->all ) {
        my @models;

        #                                           2           3
        foreach my $supported_model ( $product_model->product_id->product_models->all ) {

            #                             4 
            push @models, $supported_model->model_id->id;
        }
        printf "%s: %s\n", $product->id, join ', ', @models;
    }
}

首先,我们抓取所有 ProductModel 个条目 (1)。对于其中的每一个,我们采用 Product (2)。每行总是只有一个Product,因为那样我们就有了一个1:N关系,所以我们可以直接访问它。此 Product 又具有 ProductModel 关系。那是 3。因为这是N面,所以我们需要把它们都拿来迭代。然后,我们将每个 Model (4) 的 id 推入该产品的模型列表。之后就是打印了。

这是另一种看待它的方式:

我们可以删除 prefetch 中的最后一个 model_id,但是我们必须使用 get_column('model_id') 来获取 ID。它将为我们节省一个连接。

现在如果我们打开 DBIC_TRACE=1,我们会得到这个 SQL 语句:

SELECT me.id, me.name, product_models.product_id, product_models.model_id, product_id.id, product_id.name, product_models_2.product_id, product_models_2.model_id, model_id.id, model_id.name
FROM product me
LEFT JOIN product_model product_models ON product_models.product_id = me.id
LEFT JOIN product product_id ON product_id.id = product_models.product_id
LEFT JOIN product_model product_models_2 ON product_models_2.product_id = product_id.id
LEFT JOIN model model_id ON model_id.id = product_models_2.model_id
WHERE (product_models.model_id = 'm2')
ORDER BY me.id

如果我们 运行 针对我们的数据库,我们有这些行:

p2|Product 2|p2|m2|p2|Product 2|p2|m1|m1|Model 1
p2|Product 2|p2|m2|p2|Product 2|p2|m2|m2|Model 2

当然,如果我们手动完成那是非常无用的,但是 DBIC 的魔力确实帮助了我们,因为所有奇怪的连接和组合都被完全抽象掉了,我们只需要一个查询就可以获取所有数据。