5 советов по работе с базой данных в Laravel приложениях
Правильное использование функциональных возможностей 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 error: Out 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');
});
Теперь, для получения нужных нам данных, движок бд вместо полного обхода выполнит поиск данных по индексу, что значительно ускорит время выполнения запроса.