Недавно столкнулся с интересной проблемой в своём блоге — теги отказывались сохраняться корректно. В этой статье подробно опишу процесс отладки и решения, чтобы помочь тем, кто может столкнуться с похожей ситуацией.
При редактировании статьи и добавлении тегов происходило следующее:
Только часть тегов сохранялась, причём с разным регистром букв. Новые теги просто игнорировались.
Первым делом проверил, что реально хранится в базе данных:
MariaDB [blog]> SELECT t.name
FROM tags t
JOIN article_tags at ON t.id = at.tag_id
WHERE at.article_id = 61;
+---------------------+
| name |
+---------------------+
| DIY |
| резистор |
| блок_питания_5в |
+---------------------+
В базе действительно были только эти три тега. Значит, проблема была в процессе сохранения.
Проверил структуру таблицы tags:
MariaDB [blog]> DESCRIBE tags;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(50) | NO | UNI | NULL | |
| slug | varchar(50) | NO | UNI | NULL | |
+-------+-------------+------+-----+---------+----------------+
Обнаружил, что поля name и slug имеют уникальные индексы (UNI). Это означает, что не может быть двух тегов с одинаковым именем или слагом.
Проверил, какие теги уже существуют в базе:
MariaDB [blog]> SELECT * FROM tags
WHERE name LIKE '%raspberry%' OR name LIKE '%usb%' OR name LIKE '%питание%';
+------+---------------------------------+---------------------------------+
| id | name | slug |
+------+---------------------------------+---------------------------------+
| 247 | raspberry pi | raspberry-pi |
| 248 | raspberry pi 5 | raspberry-pi-5 |
| 249 | питание raspberry pi | питание-raspberry-pi |
| 250 | usb type-c | usb-type-c |
| 251 | usb pd | usb-pd |
+------+---------------------------------+---------------------------------+
Вот оно! В базе уже есть теги с пробелами, а я вводил теги с подчёркиваниями.
Нашёл функцию processArticleTags() в файле tags_functions.php:
function processArticleTags($pdo, $articleId, $tagsString) {
if (empty($tagsString)) return;
// Удаляем старые связи
$pdo->prepare("DELETE FROM article_tags WHERE article_id = ?")
->execute([$articleId]);
$tags = array_filter(array_map('trim', explode(',', $tagsString)));
foreach ($tags as $tagName) {
$slug = strtolower(preg_replace('/[^a-zа-я0-9]+/ui', '-', $tagName));
$pdo->prepare("INSERT IGNORE INTO tags (name, slug) VALUES (?, ?)")
->execute([$tagName, $slug]);
$tagId = $pdo->lastInsertId() ?:
$pdo->query("SELECT id FROM tags WHERE name = " . $pdo->quote($tagName))
->fetchColumn();
$pdo->prepare("INSERT IGNORE INTO article_tags (article_id, tag_id) VALUES (?, ?)")
->execute([$articleId, $tagId]);
}
}
Проблема была в этом месте:
$tagId = $pdo->lastInsertId() ?:
$pdo->query("SELECT id FROM tags WHERE name = " . $pdo->quote($tagName))
->fetchColumn();
Когда код пытался вставить тег raspberry_pi:
raspberry-piINSERT IGNORE игнорировал вставку (такой слаг уже есть у raspberry pi)lastInsertId() возвращал 0SELECT WHERE name = 'raspberry_pi' не находил тег (в базе raspberry pi с пробелом)$tagId становился falseСоставил таблицу для наглядности:
| В базе (с пробелами) | Ввод (с подчёркиваниями) | Слаг (одинаковый) |
|---|---|---|
| raspberry pi | raspberry_pi | raspberry-pi |
| raspberry pi 5 | raspberry_pi_5 | raspberry-pi-5 |
| питание raspberry pi | питание_raspberry_pi | питание-raspberry-pi |
| usb type-c | usb_type_c | usb-type-c |
Решил привести все теги к единому формату — с подчёркиваниями вместо пробелов и дефисов:
-- Заменяем пробелы и дефисы на подчёркивания в именах
UPDATE tags
SET name = REPLACE(REPLACE(name, ' ', '_'), '-', '_');
-- Обновляем слаги (подчёркивания → дефисы)
UPDATE tags
SET slug = LOWER(REPLACE(name, '_', '-'));
Результат:
Проверил, не появились ли дубликаты после замены:
SELECT
REPLACE(REPLACE(name, ' ', '_'), '-', '_') AS new_name,
COUNT(*) as count,
GROUP_CONCAT(id ORDER BY id) as ids,
GROUP_CONCAT(name ORDER BY id SEPARATOR ' | ') as original_names
FROM tags
GROUP BY new_name
HAVING count > 1
ORDER BY count DESC;
✅ Результат: дубликатов нет!
Привёл все теги к нижнему регистру для единообразия:
-- Приводим все имена тегов к нижнему регистру
UPDATE tags
SET name = LOWER(name);
-- Слаги уже в нижнем регистре, но на всякий случай:
UPDATE tags
SET slug = LOWER(slug);
Обновил функцию processArticleTags(), чтобы она:
slug, а не по namefunction processArticleTags($pdo, $articleId, $tagsString) {
if (empty($tagsString)) return;
// Удаляем старые связи
$pdo->prepare("DELETE FROM article_tags WHERE article_id = ?")
->execute([$articleId]);
$tags = array_filter(array_map('trim', explode(',', $tagsString)));
foreach ($tags as $tagName) {
// Приводим к нижнему регистру
$tagName = strtolower($tagName);
// Заменяем пробелы и дефисы на подчёркивания
$tagName = preg_replace('/[ -]+/', '_', $tagName);
// Создаём слаг (подчёркивания → дефисы)
$slug = strtolower(preg_replace('/[^a-zа-я0-9]+/ui', '-', $tagName));
// Вставляем или игнорируем если существует
$pdo->prepare("INSERT IGNORE INTO tags (name, slug) VALUES (?, ?)")
->execute([$tagName, $slug]);
// Получаем ID по слагу (надёжнее!)
$stmt = $pdo->prepare("SELECT id FROM tags WHERE slug = ?");
$stmt->execute([$slug]);
$tagId = $stmt->fetchColumn();
// Привязываем к статье
$pdo->prepare("INSERT IGNORE INTO article_tags (article_id, tag_id) VALUES (?, ?)")
->execute([$articleId, $tagId]);
}
}
После всех изменений:
✅ Всё работает!
SELECT t.name
FROM tags t
JOIN article_tags at ON t.id = at.tag_id
WHERE at.article_id = [ID_статьи];
DESCRIBE tags;
SHOW INDEX FROM tags;
SELECT * FROM tags WHERE name LIKE '%raspberry%';
SELECT
REPLACE(REPLACE(name, ' ', '_'), '-', '_') AS new_name,
COUNT(*) as count,
GROUP_CONCAT(id ORDER BY id) as ids
FROM tags
GROUP BY new_name
HAVING count > 1;
-- Создайте резервную копию сначала!
DELETE t1 FROM tags t1
INNER JOIN tags t2
WHERE
t1.id > t2.id
AND REPLACE(REPLACE(t1.name, ' ', '_'), '-', '_') = REPLACE(REPLACE(t2.name, ' ', '_'), '-', '_');
slug надёжнее, чем по name, особенно если форматы могут отличатьсяINSERT IGNORE для безопасной вставки, но не забывайте проверять результат
Комментарии
Пока нет комментариев. Будьте первым!