DBIx::Class - получить все отношения, которые использовались в качестве условия, с помощью предварительной выборки?

Вот три таблицы: product, model и product_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_id, которые поддерживает продукт (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` )
}

Затем я попытался дважды выполнить предварительную выборку одной и той же таблицы:

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

Казалось, мне это удалось. Был выполнен только один запрос, из которого я получил все идентификаторы моделей.

Однако я не был уверен, что это правильный(?) путь. Это правильный подход?

Например, если я использовал join вместо prefetching, Product 2 появится в цикле дважды. Я понимаю это, потому что объединенная таблица выглядит так:

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

Почему Product 2 появляется только один раз, когда я использую prefetch?

Результирующие запросы почти одинаковы, за исключением разницы в 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'

person gypark    schedule 18.12.2017    source источник
comment
Честно говоря, я думаю, что ты слишком усложняешь себе жизнь. DBIx::Class — это ORM, где R означает «реляционный». В вашей схеме ожидаются отношения, такие как has_many или belongs_to. После того, как они у вас есть, вам больше не нужно делать что-либо вне цикла вручную. Просто запрос на product_model, который выбирает те строки, которые принадлежат products, где имя eq «Продукт 2» ... надеюсь, это даст вам достаточно подсказок.   -  person vanHoesel    schedule 18.12.2017
comment
@vanHoesel Спасибо за совет. Однако я ищу ВСЕ продукты, а не конкретный продукт. В любом случае я мог бы начать с product_model и prefetch => { product => 'product_models' }, чтобы получить все поддерживаемые модели. На самом деле я этого не делал, потому что в моей реальной работе есть еще несколько условий. Я подумаю еще и рассмотрю то, что вы сказали. Спасибо!   -  person gypark    schedule 19.12.2017


Ответы (1)


Если у вас есть правильные отношения в вашей схеме, это возможно с помощью одного запроса. Но это сложно. Предположим, что ваша база данных выглядит так:

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, а product_id — это имя отношения к таблице Models. Стрелка => 1 указывает на первое соединение от Product до ProductModel. 2 за ProductModel для каждого продукта с моделью m2. См. рисунок модели ER для иллюстрации.

Модель ER

Теперь мы хотим иметь все ProductModel, которые есть у этого Product. Это стрелка 3.

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

И, наконец, чтобы получить Models для этого отношения N:M, мы должны использовать магазин отношений model_id со стрелкой 4.

{
    '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, нам нужно взять их все и повторить. Затем мы помещаем id каждого Model (4) в наш список моделей для этого продукта. После этого только печать.

Вот еще один способ взглянуть на это:

введите описание изображения здесь

Мы могли бы исключить последний model_id из prefetch, но тогда нам пришлось бы использовать get_column('model_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 действительно помогает нам, потому что все странные объединения и объединения полностью абстрагируются, и нам нужен только один единственный запрос, чтобы получить все данные.

person simbabque    schedule 18.12.2017
comment
Круги свободной рукой с сенсорной панелью действительно сложно сделать круговыми. - person simbabque; 18.12.2017