Нубам от нуба, все как обычно 🙂 Недавно выполнял перенос бд с битрикса на october, конкретнее на плагин offline.mall. Плагин непопулярный, поэтому постараюсь выдать больше общей информации, чем частностей. Размеры бд - около 100к товаров, со всеми вытекающими - порядка 400к свойств, 7к категорий и т.д. Ладно, го.
О консольных командах написано тут - https://octobercms.com/docs/console/development , но все же опишу и тут вкратце:
1) создаем свой плагин
2)в нем создаем папку console, в ней уже сам файл команды, у меня - Com.php.
внутри нам нужен метод handle() - он будет выполняться при вводе команды в консоли.
3) в нашем Plugin.php регистрируем команды. Я сделал две команды - одну на перенос таблицы, вторую на удаление содержимого таблицы. Ошибок будет много, так что и вам советую нечто подобное.
public function register()
{
$this->registerConsoleCommand('sql.go','Bulatov\Sql\Console\Com');
$this->registerConsoleCommand('sql.down','Bulatov\Sql\Console\Delete');
}
Первый аргумент - сама команда, второй - путь до исполняемого файла.
Вот и все. Для теста можно внутри метода handle разместить полезную команду -
$this->output->writeln('Hello world!');
она выводит в консоль надпись hello world при исполнении команды. Далее я покажу как ее можно юзать с большей пользой.
Самое первое с чего я начал - с блокнотиком и ручкой создавал карту переноса бд. На первый взгляд кажется, что все примерно одинаково, но дьявол кроется в деталях, например в битриксе бренд выведен отдельным свойством, а в mall прямо в товаре есть столбец brand_id. Таких моментов будет много, так что лучше заранее понять что откуда брать и куда пихать 🙂
Итак, начнем с простого примера, где нам не нужно посторонних таблиц.
DB::transaction(function () {
$this->output->writeln('GO !');
$query = DB::table('bitrix.b_iblock_section')
->where([
['IBLOCK_ID',2],
['ACTIVE', 'Y'],
['GLOBAL_ACTIVE', 'Y'],
])
->orderBY('ID')
->select('NAME','LEFT_MARGIN','RIGHT_MARGIN', 'DEPTH_LEVEL', 'DESCRIPTION','IBLOCK_SECTION_ID','ID')
->chunk(1000,function($query){
foreach($query as $queryItem){
DB::table('mydb.offline_mall_categories')->insert(
[
'id' => $queryItem->ID,
'name' =>Str::after($queryItem->NAME,'______'),
'slug' =>Str::slug($queryItem->NAME.$queryItem->ID),
'nest_left' => $queryItem->LEFT_MARGIN,
'nest_right' => $queryItem->RIGHT_MARGIN,
'nest_depth' => $queryItem->DEPTH_LEVEL,
'description' => Str::limit($queryItem->DESCRIPTION,1024),
'parent_id' => $queryItem->IBLOCK_SECTION_ID,
'created_at' => '2019-05-29 11:22:45',
]);
}
$this->output->writeln(' 1k ready');
});
});
Выглядит может и не очень просто, но я сейчас все поясню.
1)DB::transaction
- советую оборачивать в транзакцию все более-менее большие запросы. Очень сильно ускоряет процесс выполнения запросов, подробнее - https://laravel.com/docs/5.8/database.
2)
query = DB::table('bitrix.b_iblock_section')
->where([
['IBLOCK_ID',2],
['ACTIVE', 'Y'],
['GLOBAL_ACTIVE', 'Y'],
])
->orderBY('ID')
->select('NAME','LEFT_MARGIN','RIGHT_MARGIN', 'DEPTH_LEVEL', 'DESCRIPTION','IBLOCK_SECTION_ID','ID')
Тут мы выбираем таблицу с категориями, далее фильтруем по одним только битриксоидам известным
столбцам ( на самом деле IBLOCL_ID = 2 это категории именно товаров, вторые два - аналог нашего published, как я понял). Далее ставим orderBy по ID и с помощью select выбираем необходимые нам столбцы для переноса. Можно сделать select *, то есть всю таблицу, но опять же это замедлит процесс.
3)
->chunk(1000,function($query){
foreach($query as $queryItem){
чанк нам нужен, чтобы не отвалилась память при большом объеме данных. Он по сути разделяет на куски наш запрос. Количество строк в "куске" мы передаем в первом аргументе, 5000 работало без проблем. Каждый queryItem - одна строка, в которой мы можем обращаться к данным.
4)
DB::table('mydb.offline_mall_categories')->insert(
[
'id' => $queryItem->ID,
'name' =>Str::after($queryItem->NAME,'______'),
'slug' =>Str::slug($queryItem->NAME.$queryItem->ID),
'nest_left' => $queryItem->LEFT_MARGIN,
'nest_right' => $queryItem->RIGHT_MARGIN,
'nest_depth' => $queryItem->DEPTH_LEVEL,
'description' => Str::limit($queryItem->DESCRIPTION,1024),
'parent_id' => $queryItem->IBLOCK_SECTION_ID,
'created_at' => '2019-05-29 11:22:45',
]);
- я решил использовать фасад DB вместо модели Category по двум причинам, первая - я хочу оставить старые id, т.к. на них завязаны все связи. Сохранить связи можно и с новыми id, но это сильно усложнит процесс. Второе - скорость, и вот тут ответа я не знаю, но на практике через фасад оказалось в несколько раз быстрее, чем через модель. Итак, мы выбираем нужную нам таблицу категорий, и через insert записываем в нее все данные.
5) $this->output->writeln('1k ready');
- уже знакомая фишка - выводит в консоль сообщение о каждой тысяче перенесенных категорий. Мб не слишком полезно, но залипательно 🙂
Что делать, если ошиблись? Если ошибка фатальна - то я запускаю свою вторую команду - sql:down, в методе handle которой находится DB::table('ваша таблица')->truncate();
, это полностью очистит таблицу от данных.
Если же просто забыли добавить какое то обязательное поле, то делаем все тоже самое,что и при первом переносе только вместо insert, ставим update,сверяемся по id, а в массиве передаем нужные нам значения. Ниже приведен пример, где я неправильно выставил слаги.
DB::transaction(function () {
$query = DB::table('offline_mall_products')
->select('name','slug','id')
->orderBy('ID')
->chunk(5000, function ($query){
foreach($query as $queryItem)
{
DB::table('offline_mall_products')
->where('id',$queryItem->id)
->update([
'slug' => Str::slug(Str::limit($queryItem->name, 50)),
]);
}
});
});
Насчет Str::slug - это полезная фишка laravel, советую почитать - https://laravel.com/docs/5.8/helpers.
Теперь возьмем более сложную ситуацию, например, когда нам нужно взять данные из нескольких таблиц, а запихать нужно в одну. Тут мы можем сделать либо несколько транзакций, что проще понять, но медленнее делать, либо использовать join. Допустим у категорий есть бренды, которые хранятся в отдельной таблице, назовем для примера bitrix_category_brand, в которой есть столбцы id, category_id,
brand_id, а нам нужно, чтобы brand_id был непосредственно в нашей таблице с категориями, тогда нам нужно вставить перед селектом, например
...
->join('bitrix_category_brand',''bitrix.b_iblock_section.id','category_id')
->select('NAME','LEFT_MARGIN','RIGHT_MARGIN', 'DEPTH_LEVEL',...
Первым параметром передаем таблицу, которую хотим просоеденить, вторым и третьим - индексы, то есть связывающие их id. Будьте внимательны, если в двух таблицах есть столбцы с одинаковым именем, нужно указывать какую именно таблицу вы имеете ввиду, а чтобы не было путаницы после, в селекте можете прописать ('NAME as category_name').
В итоге можно сделать один большой скрипт, а можно переносить по одной таблице.
Как всегда, замечания, пожелания, исправления - буду только рад.