Yii2. Ленивая и жадная загрузка

20.03.2019

Теги: MySQLWeb-разработкаYii2БазаДанныхТеорияФреймворк

Итак, мы создали связи для моделей, и теперь можем легко получать связанные данные для категорий и товаров каталога. Однако, в этом удобстве может скрываться один подводный камень. Посмотрим еще раз на код, который позволяет получать связанные данные. Например, для главной страницы каталога, где выводятся корневые разделы.

-- Таблица `category`

CREATE TABLE `category` (
  `id` int(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Первичный ключ',
  `parent_id` int(10) NOT NULL DEFAULT '0' COMMENT 'Родительская категория',
  `name` varchar(100) NOT NULL COMMENT 'Название категории',
  `sortorder` tinyint(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Порядок сортировки'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `category` (`id`, `parent_id`, `name`, `sortorder`) VALUES
(1, 0, 'Первая категория', 1),
(2, 0, 'Вторая категория', 2),
(3, 0, 'Третья категория', 3),
(4, 0, 'Четвертая категрия', 4),
(5, 1, 'Первая дочерняя', 1),
(6, 1, 'Вторая дочерняя', 1),
(7, 1, 'Третья дочерняя', 1),
(8, 1, 'Четвертая дочерняя', 1),

-- Таблица `product`

CREATE TABLE `product` (
  `id` int(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Первичный ключ',
  `category_id` int(10) NOT NULL DEFAULT '0' COMMENT 'Родительская категория',
  `name` varchar(200) NOT NULL COMMENT 'Название товара',
  `sortorder` tinyint(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Порядок сортировки'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `product` (`id`, `category_id`, `name`, `sortorder`) VALUES
(1, 1, 'Первый товар', 1),
(2, 1, 'Второй товар', 2),
(3, 2, 'Первый товар', 1),
(4, 2, 'Второй товар', 2),
(5, 5, 'Пятый товар', 1),
(6, 5, 'Шестой товар', 2);

Класс модели, где задаются связи:

<?php
namespace app\models;
use yii\db\ActiveRecord;

class Category extends ActiveRecord
{
    // товары категории
    public function getProducts() {
        // связь таблицы БД `category` с таблицей `product`
        return $this->hasMany(Product::className(), ['category_id' => 'id']);
    }
    
    // родительская категория
    public function getParent() {
        // связь таблицы БД `category` с таблицей `category`
        return $this->hasOne(self::className(), ['id' => 'parent_id']);
    }

    // дочерние категории
    public function getChildren() {
        // связь таблицы БД `category` с таблицей `category`
        return $this->hasMany(self::className(), ['parent_id' => 'id']);
    }
}

Класс контроллера, где получаем данные от модели:

<?php
namespace app\controllers;

use yii\web\Controller;
use app\models\Category;
use app\models\Product;

class CatalogController extends Controller {

    public function actionIndex() {
        // получаем информацию о корневых категориях
        $categories = Category::find()->where(['parent_id' => 0])->all();
        return $this->render('index', ['categories' => $categories]);
    }

    /* ... */
}

Представление views/catalog/index.php, где показываем корневые категории:

<?php
/* @var $this yii\web\View */
use yii\helpers\Html;

$this->title = 'Каталог товаров';
?>
<div class="catalog-index">
    <h1><?= Html::encode($this->title) ?></h1>
    <ul>
    <?php foreach ($categories as $category): ?>
        <li><?= $category->name; ?></li>
    <?php endforeach; ?>
    </ul>
</div>
<h1>Каталог товаров</h1>
<ul>
    <li>Первая категория</li>
    <li>Вторая категория</li>
    <li>Третья категория</li>
    <li>Четвертая категория</li>
</ul>

Давайте заглянем в Yii Debugger — инструмент, который расположен в правом нижнем углу страницы. Там только один запрос (не считая служебных):

SELECT * FROM `category` WHERE `parent_id` = 0

Теперь получим в представлении связанные данные о дочерних категориях для каждой корневой категории:

<?php
/* @var $this yii\web\View */
use yii\helpers\Html;

$this->title = 'Каталог товаров';
?>
<div class="catalog-index">
    <h1><?= Html::encode($this->title) ?></h1>
    <ul>
    <?php foreach ($categories as $category): ?>
        <li>
        <?php
            echo $category->name;
            // данные о дочерних категориях
            $children = $category->children;
            if (!empty($children)) {
                echo '<ul>';
                foreach ($children as $child) {
                    echo '<li>', $child->name, '</li>';
                }
                echo '</ul>';
            }
        ?>
        </li>
    <?php endforeach; ?>
    </ul>
</div>
<h1>Каталог товаров</h1>
<ul>
    <li>Первая категория
        <ul>
            <li>Первая дочерняя</li>
        </ul>
    </li>
    <li>Вторая категория
        <ul>
            <li>Вторая дочерняя</li>
        </ul>
    </li>
    <li>Третья категория
        <ul>
            <li>Третья дочерняя</li>
        </ul>
    </li>
    <li>Четвертая категория
        <ul>
            <li>Четвертая дочерняя</li>
        </ul>
    </li>
</ul>

Еще раз заглянем заглянем в Yii Debugger. Там пять запросов (не считая служебных):

SELECT * FROM `category` WHERE `parent_id` = 0
SELECT * FROM `category` WHERE `parent_id` = 1
SELECT * FROM `category` WHERE `parent_id` = 2
SELECT * FROM `category` WHERE `parent_id` = 3
SELECT * FROM `category` WHERE `parent_id` = 4

Происходит так из-за так называемой ленивой загрузки данных (отложенная загрузка). Именно она и использовалась при обращении к виртуальному свойству, доступному после реализации связи моделей. Использовать ленивую загрузку очень удобно, поскольку она работает только тогда, когда мы ее вызываем. В этом легко убедиться, если убрать обращение к виртуальному свойству из представления:

// $children = $category->children;
SELECT * FROM `category` WHERE `parent_id` = 0

Но как быть, если связи моделей все же нужны? В данном случае вместо отложенной загрузки нужно использовать другой вариант загрузки связанных данных — жадную загрузку. Этот вариант выбирает данные всегда, вне зависимости от того, нужны ли они.

Для того, чтобы использовать жадную загрузку данных, нам достаточно добавить метод with(), указав в качестве параметра наименование связи (геттер связи в модели).

<?php
namespace app\controllers;

use yii\web\Controller;
use app\models\Category;
use app\models\Product;

class CatalogController extends Controller {

    public function actionIndex() {
        // получаем информацию о корневых категориях
        $categories = Category::find()->where(['parent_id' => 0])->with(['children'])->all();
        return $this->render('index', ['categories' => $categories]);
    }

    /* ... */
}

Все, больше ничего в коде менять не нужно.

<h1>Каталог товаров</h1>
<ul>
    <li>Первая категория
        <ul>
            <li>Первая дочерняя</li>
        </ul>
    </li>
    <li>Вторая категория
        <ul>
            <li>Вторая дочерняя</li>
        </ul>
    </li>
    <li>Третья категория
        <ul>
            <li>Третья дочерняя</li>
        </ul>
    </li>
    <li>Четвертая категория
        <ul>
            <li>Четвертая дочерняя</li>
        </ul>
    </li>
</ul>

Теперь проверим запросы к БД в Yii Debugger:

SELECT * FROM `category` WHERE `parent_id` = 0
SELECT * FROM `category` WHERE `parent_id` IN (1, 2, 3, 4)

Поиск: MySQL • Web-разработка • Yii2 • База данных • Фреймворк • Ленивая загрузка • Жадная загрузка • Модель • Связь • Связанные данные • hasOne • hasMany • With

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.