0

5 советов по работе с базой данных в Laravel приложениях

Интересуешся ИТ? Подписывайся на авторский канал по разработке ПО!
@backend_director

Правильное использование функциональных возможностей Laravel при работе с базой данных может оказать значительное влияние на скорость разработки и производительность вашего приложения. Наши 5 простых советов помогут вам сократить время разработки проекта на начальных этапах, а также заложить правильные практики для оптимизации его работы.

1. Предпочитайте Eloquent сырым запросам, особенно при разработке MVP

Серьезно! Несмотря на то, что Active Record считается анти-паттерном, работа с Eloquent значительно сокращает время разработки приложения, особенно если оно находится на этапе MVP. Не спешите гнаться за over высокой производительностью. Eloquent прекрасно показывает себя на проектах малой и средней сложности, скрывая почти всю работу со слоем данных за простым интерфейсом. Просто сравните эти 2 кусочка кода, выполняющих одинаковую логику:

// With Query Builder (SQL)
$products = DB::table('products')
    ->where('active', 1)
    ->get();

foreach ($products as $product) {
    $attributes = DB::table('product_attributes')
        ->where('product_id', $product->id)
        ->get();
    
    foreach ($attributes as $attribute) {
        // Some logic with attibute
    }
}
// With Eloquent
$products = Product::with('attributes')->active()->get();

foreach ($products as $product) {
    $product->attributes->each(function($attribute) {
        // Some logic with attibute
    });
}

Конечно, если у вас n-гигабайтная база данных и больше 1000 пользователей в минуту, этот пукнт можно пропустить)

2. Включайте в выборку только нужные поля

Обычно, для получения данных из бд, мы используем следующую простую конструкцию:

// Query Builder
$products = DB::table('products')
    ->where('active', 1)
    ->get();

// Eloquent
$products = Product::active()->get();

Этот код создаст запрос такого вида:

SELECT * FROM products WHERE active = '1'

Данный запрос хорош, если нам действительно нужна вся информация о выбранных товарах. В остальных случаях, этот запрос будет избыточен, поскольку на сбор данных уйдет намного больше времени и объема оперативной памяти, чем нужно нам на самом деле.

Хорошей практикой будет положить в результирующий набор данных только нужные нам поля:

// Query Builder
$products = DB::table('products')
    ->where('active', 1)
    ->get([
        'id',
        'price'
    ]);

// Eloquent
$products = Product::active()->get([
    'id',
    'price'
]);

3.Используйте Chunk для обработки больших наборов данных

Как получить и обработать список активных товаров магазина? Например, можно написать такой код:

DB::table('products')
    ->where('active', 1)
    ->get()
    ->each(function ($product) {
       // Some logic with Product 
    });

Но что, если результирующих набор будет содержать 10к товаров? А если 100к или миллион? В конце концов, логическим результатом будет PHP Fatal errorOut of memory.

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

// Query Builder
DB::table('products')
    ->where('active', 1)
    ->chunk(100, function ($products) {
        foreach ($products as $product) {
            // Some logic with Product
        }
    });

// Eloquent
$products = Product::active()->chunk(100, function ($products) {
    foreach ($products as $product) {
        // Some logic with Product
    }
});

В этом примере из таблицы товаров будет извлекаться и обрабатываться 100 записей. Затем извлекаться и обрабатываться еще 100 записей и так далее. Этот цикл будет продолжаться до тех пор, пока не будут обработаны все товары.

4. Используйте жадную загрузку в Eloquent для избегания проблемы N + 1

Эта проблема хорошо описана во всех руководствах по Laravel. Если кратко, то для данного простого участка кода:

// Controller
$products = Product::active()->get();

// View
@foreach ($products as $product) {
    
{{ $product->name }}
Категория: {{ $product->category->name }}
@endforeach

Мы получим такие запросы к базе данных:

SELECT * FROM products WHERE active = '1';
SELECT * FROM category WHERE id = product1.category_id;
SELECT * FROM category WHERE id = product2.category_id;
SELECT * FROM category WHERE id = product3.category_id;
SELECT * FROM category WHERE id = product4.category_id;
...
SELECT * FROM category WHERE id = productN.category_id;

Таким образом, мы выполняем запрос на получение информации о категории для каждого товара в цикле. Соответственно, для N-го количества товаров нам понадобится N+1 запросов (1 запрос для получения товаров и N запросов для получения категории для каждого товара). Эта проблема называется проблема N+1.

Для её решения нужно привести код к такому виду:

$products = Product::with('category')->active()->get();

Теперь наш код выполнит всего 2 запроса к бд:

SELECT * FROM products WHERE active = '1';
SELECT * FROM category WHERE id IN (product1.category_id, product2.category_id, product3.category_id, ...productN.category_id);

5. Добавьте индексы для полей, с которыми часто работаете

Давайте представим, что у нас есть таблица товаров на миллион строк такой структуры:

В стандартном случае, если нам понадобиться получить список всех товаров из категории 3, движок бд выполнит обход миллиона строк, сравнит каждое значение поля category_id со значением 3 и вернет нам результирующий набор. Такой обход займет длительное время (особенно если данных очень много) и может сыграть с нами злую шутку при повышенной нагрузке. Для ускорения выборки нужно проиндексировать поле category_id при написании миграций:

Schema::create('products', function (Blueprint $table) {
   $table->increments('id');
   $table->integer('category_id')->unsigned();
   $table->string('name', 100);
   
   $table->index('category_id', 'i_category_id');
});

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