Мой сайт мигрировал множество раз:
Поскольку почти всегда это было полуавтоматически, то записей в БД лишних столько, что постоянно натыкаюсь на строки давно былого. Ясен пень, что это производительности не добавляет, как и устойчивости работы.
Поэтому решено не обновляться до Joomla 4, а перенести данные на Joomla 5. Рано или поздно это все равно это надо будет делать, поэтому лучше подготовиться на локальном сайте, а потом в любой момент перенести работающий проект Joomla 5 на хостинг.
Основное на сайте - контент, категории, теги, баннеры и меню. У расширений свои таблицы, так что к миграции они отношения не имеют.
Некоторые расширения больше не поддерживаются и нет смысла их данные тащить далее по жизни.
Все переносится, но тащит за собой кучу старых записей таблицы #__assets, если ее не переносить - материаты не видны в админке и на сайте. Просто так записать в #__assets данные не получится, пришлось разбираться.
Поля lft и rgt в таблице #__assets используются для реализации иерархической структуры данных по методу Nested Set (вложенные множества). Это оптимизированный способ хранения деревьев в реляционных базах данных.
Таблица #__assets в Joomla хранит права доступа (ACL) для всех объектов (статей, категорий, модулей и т.д.). Поля lft и rgt позволяют быстро выбирать все дочерние элементы (например, все статьи в категории) одним запросом.
Значения lft и rgt начинаются с 1 для корня (root.1). Интервалы дочерних элементов вложены в родительские:
sql -- Корректная структура: -- root.1 (lft=1, rgt=8) -- ├── category.1 (lft=2, rgt=5) -- │ ├── article.1 (lft=3, rgt=4) -- └── category.2 (lft=6, rgt=7)
lft/rgt вручную нельзя редактировать без перестроения дерева — это нарушит ACL
1. Родитель (lft=1, rgt=6) ├── 2. Дочерний 1 (lft=2, rgt=3) └── 3. Дочерний 2 (lft=4, rgt=5)
Как оно там рассчитывается я так и не разобрался, хотя и пытался. Но об этом ниже.
Для понимания даю схему связей таблиц
План переноса контента
Устанавливаю Joomla 5 с префиксом таблиц как в Joomla 3. После установки нужна замена id admin (назначенный по умолчанию) на id исходной Joomla 3 в таблицах:
#__user_usergroup_map #__user_profiles #__users
Всегда TRUNCATE + INSERT INTO - в импортируемой таблице записи будут удалены, а структура останется без изменений.

SQL запрос получим такого типа, где XXX расширение таблиц:
TRUNCATE `XXX_content_frontpage`; INSERT INTO `XXX_content_frontpage` (`content_id`, `ordering`) VALUES (75, 6), (96, 7), (277, 2), (287, 5), (288, 4), (289, 3), (290, 1);
Далее аналогично импортирую данные:
- XXX_banner* - все таблицы banner
- XXX_content_frontpage избранное
- XXX_content_rating – рейтинг контента
- XXX_categories
Замена id admin 142, назначенного Joomla 5 при установке на id админа из Joomla 3 в таблицах:
XXX_user_usergroup_map #__user_profiles #__users
После импорта категории в админке открываем и сохраняем заново согласно вложенности, т.е. сначала верхний уровень, а затем дочерние категории. Дочерние категории первыми сохранить не получится:
Категории отныне имеют правильный id, lft и rgt:

Создаем первый материал с минимумом данных. Сохраняем, переходим в таблицу XXX_assets:
- 116 – порядковый номер материала ЗАПОМИНАЕМ!
- 113 – id категории в этой таблице
- 65 66 lft и rgt нового материала
В Joomla 5 в таблице jos_content отсутствует поле xreference - добавляем его перед импортом контента (потом удалим) запросом:
ALTER TABLE `jos_content` ADD COLUMN `xreference` VARCHAR(50) NULL AFTER `language`;
Экспортируем таблицу с материалами jos_content в Joomla 3 и импортируем в БД Joomla 5
Материал с id=1, созданный ранее перезапишется, и перезапишется его asset_id который мы запомнили ранее = 116.
Теперь необходимо пересчитать поле asset_id таблицы jos_content начиная с номера 116 запросом:
UPDATE `jos_content` c
JOIN (
SELECT `id`,
ROW_NUMBER() OVER (ORDER BY `id`) + 115 AS new_asset_id
FROM `jos_content`
) AS temp ON c.id = temp.id
SET c.`asset_id` = temp.new_asset_id;Поскольку сайт J5 будет в разработке, а жизнь на J3 продолжается - добавляем дополнительные 50 материалов, которые будут без контента и неопубликованы.
+50 материалов SQL запрос INSERT INTO
INSERT INTO `jos_content`
(`id`, `asset_id`, `title`, `alias`, `state`, `introtext`, `fulltext`, `catid`, `created`, `created_by`, `images`, `urls`, `attribs`, `metadesc`, `metadata`, `language`, `modified`)
SELECT
NULL AS `id`,
(SELECT MAX(asset_id) FROM `jos_content`) + n.n AS `asset_id`,
CONCAT('Новая статья ', n.n) AS `title`,
CONCAT('new-article-', n.n) AS `alias`,
0 AS `state`,
'Вступление статьи' AS `introtext`,
'Основной текст статьи' AS `fulltext`,
20 AS `catid`,
'2025-05-01 00:00:00' AS `created`,
69 AS `created_by`,
'{"image_intro":"","image_fulltext":""}' AS `images`, '{"urla":false,"urlatext":"","targeta":"","urlb":false,"urlbtext":"","targetb":"","urlc":false,"urlctext":"","targetc":""}' AS `urls`,
'{}' AS `attribs`,
'' AS `metadesc`,
'{}' AS `metadata`,
'*' AS `language`,
'2025-05-01 00:00:00' AS `modified`
FROM
(SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION
SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION
SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION
SELECT 10 UNION SELECT 11 UNION SELECT 12 UNION
SELECT 13 UNION SELECT 14 UNION SELECT 15 UNION
SELECT 16 UNION SELECT 17 UNION SELECT 18 UNION
SELECT 19 UNION SELECT 20 UNION SELECT 21 UNION
SELECT 22 UNION SELECT 23 UNION SELECT 24 UNION
SELECT 25 UNION SELECT 26 UNION SELECT 27 UNION
SELECT 28 UNION SELECT 29 UNION SELECT 30 UNION
SELECT 31 UNION SELECT 32 UNION SELECT 33 UNION
SELECT 34 UNION SELECT 35 UNION SELECT 36 UNION
SELECT 37 UNION SELECT 38 UNION SELECT 39 UNION
SELECT 40 UNION SELECT 41 UNION SELECT 42 UNION
SELECT 43 UNION SELECT 44 UNION SELECT 45 UNION
SELECT 46 UNION SELECT 47 UNION SELECT 48 UNION
SELECT 49 UNION SELECT 50) AS n;
jos_content в поле metedata:
{"robots":"","author":"","rights":"","xreference":""},"xreference":"" остаток поля от Joomla 3 которого нет в Joomla 5, поэтому удаляю запросом:
UPDATE jos_content
SET metadata =
CASE
WHEN metadata = '{"robots":"","author":"","rights":"","xreference":""}'
THEN '{"robots":"","author":"","rights":""}'
WHEN metadata LIKE '%"xreference":""%'
THEN REGEXP_REPLACE(metadata, ',"xreference":""', '')
ELSE metadata
END
WHERE metadata LIKE '%"xreference":""%';С таблицей jos_content закончено, но в админке будет отображаться только 1й материал – который был создан через админку и у которого есть запись в таблице jos_assets
В таблицу jos_assets необходимо добавить столько строк, сколько материалов в таблице jos_content -1. Т.к. 1 запись уже есть для материала id=1
Добавляем строки в таблицу jos_assets по количеству материалов за вычетом 1 уже имеющегося в таблице:
INSERT INTO `jos_assets` (
`id`, `parent_id`, `lft`, `rgt`, `level`, `name`, `title`, `rules`
)
WITH RECURSIVE seq AS (
SELECT 117 AS id, 66 AS lft, 67 AS rgt, 2 AS article_num
UNION ALL
SELECT id + 1, lft + 2, rgt + 2, article_num + 1
FROM seq
WHERE id < 455 -- 117 + 339 - 1 = 455
)
SELECT
id,
100 AS parent_id,
lft,
rgt,
3 AS level,
CONCAT('com_content.article.', article_num) AS name,
CONCAT(id, ' & Article ', article_num) AS title,
'{}' AS rules
FROM seq;
WHERE id < 455 -- 116 (последнее значение id) + 339 (количество добавляемых строк) = 455
Обновляем поле title таблицы jos_assets – это урезанный title таблицы jos_assets до 100 знаков:
UPDATE `jos_assets` a JOIN ` jos_content` c ON a.`id` = c.`asset_id` SET a.`title` = LEFT(c.`title`, 100) -- Обрезаем до 100 символов WHERE a.`id` > 115;
Далее радостный захожу в админку и вижу, что записей в админке нет. В Joomla 5 появилась новая таблица jos_workflow_associations с 3 полями - ее нужно заполнить данными контента этим SQL запросом:
INSERT INTO jos_workflow_associations (item_id, stage_id, extension)
SELECT
n AS item_id,
1 AS stage_id,
'com_content.article' AS extension
FROM (
SELECT a.N + b.N*10 + c.N*100 + 1 AS n
FROM
(SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a,
(SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b,
(SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) c
WHERE a.N + b.N*10 + c.N*100 + 1 <= 455
) numbers;После выполнения запроса все 340 статей появляются в админке Joomla 5.
rgt и lft считается отдельно для каждой категории, и как его рассчитать я так и не понял, но нашлось решение:
пакетно переместить статьи одной категории в другую категорию, а затем так же обратно, и rgt и lft пересчитается самой Joomla 5.
Вот такой получился краткий мануал по переносу контента с Joomla 3 на Joomla 5 с картинками и SQL запросами. Похоже, что часть можно шагов и выкинуть, но я делал это первый раз и записывал по порядку.
Бонусный SQL запрос
Покажет как записаны в материале image_intro и image_fulltext – полезно проверить при перемещении фотографий материалов на сайте папками. Используя расширение DB Replacer можно впоследствии отредактировать записи.
image_intro и image_fulltext
SELECT `jos_content`.`id`, `jos_content`.`catid`, `jos_content`.`title`, `jos_categories`.`title` AS `category`, `jos_content`.`images`, -- Извлекаем value image_intro и выводим в поле float_intro JSON_UNQUOTE(JSON_EXTRACT(`jos_content`.`images`, '$.image_intro')) AS float_intro, -- Извлекаем value image_fulltext и выводим в поле image_fulltext JSON_UNQUOTE(JSON_EXTRACT(`jos_content`.`images`, '$.image_fulltext')) AS image_fulltext FROM `jos_content` INNER JOIN `jos_categories` ON `jos_categories`.`id` = `jos_content`.`catid` WHERE -- Условие отбора записей: все, что имеют в строке stories и id категории =2 `jos_content`.`images` LIKE "%stories%" AND `jos_content`.`catid` = 2



