Категории

Решение проблемы с тегами в блоге: отладка и исправление

30.01.2026 | Статья из категории: php

Решение проблемы с тегами в блоге: отладка и исправление

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

Симптомы проблемы

При редактировании статьи и добавлении тегов происходило следующее:

Вводил: diy raspberry_pi raspberry_pi_5 питание_raspberry_pi usb_type_c usb_pd power_delivery резистор блок_питания_5в
Сохранялось: DIY резистор блок_питания_5в

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

Этап 1: Проверка базы данных

Первым делом проверил, что реально хранится в базе данных:

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в     |
+---------------------+

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

Этап 2: Анализ структуры базы

Проверил структуру таблицы 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). Это означает, что не может быть двух тегов с одинаковым именем или слагом.

Этап 3: Поиск конфликтующих тегов

Проверил, какие теги уже существуют в базе:

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                          |
+------+---------------------------------+---------------------------------+

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

Этап 4: Анализ кода обработки тегов

Нашёл функцию 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:

  1. Создавался слаг raspberry-pi
  2. INSERT IGNORE игнорировал вставку (такой слаг уже есть у raspberry pi)
  3. lastInsertId() возвращал 0
  4. Запрос SELECT WHERE name = 'raspberry_pi' не находил тег (в базе raspberry pi с пробелом)
  5. $tagId становился false
  6. Тег не привязывался к статье ❌

Этап 5: Сравнение тегов

Составил таблицу для наглядности:

В базе (с пробелами) Ввод (с подчёркиваниями) Слаг (одинаковый)
raspberry piraspberry_piraspberry-pi
raspberry pi 5raspberry_pi_5raspberry-pi-5
питание raspberry piпитание_raspberry_piпитание-raspberry-pi
usb type-cusb_type_cusb-type-c

Решение проблемы

Шаг 1: Унификация формата тегов

Решил привести все теги к единому формату — с подчёркиваниями вместо пробелов и дефисов:

-- Заменяем пробелы и дефисы на подчёркивания в именах
UPDATE tags 
SET name = REPLACE(REPLACE(name, ' ', '_'), '-', '_');

-- Обновляем слаги (подчёркивания → дефисы)
UPDATE tags 
SET slug = LOWER(REPLACE(name, '_', '-'));

Результат:

Шаг 2: Проверка дубликатов

Проверил, не появились ли дубликаты после замены:

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;

Результат: дубликатов нет!

Шаг 3: Приведение к нижнему регистру

Привёл все теги к нижнему регистру для единообразия:

-- Приводим все имена тегов к нижнему регистру
UPDATE tags 
SET name = LOWER(name);

-- Слаги уже в нижнем регистре, но на всякий случай:
UPDATE tags 
SET slug = LOWER(slug);

Шаг 4: Исправление кода обработки тегов

Обновил функцию processArticleTags(), чтобы она:

  1. Приводила теги к нижнему регистру
  2. Заменяла пробелы и дефисы на подчёркивания
  3. Искала теги по slug, а не по name
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) {
        // Приводим к нижнему регистру
        $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]);
    }
}

Шаг 5: Тестирование

После всех изменений:

Вводил: diy raspberry_pi raspberry_pi_5 питание_raspberry_pi usb_type_c usb_pd power_delivery резистор блок_питания_5в
Сохранилось: diy raspberry_pi raspberry_pi_5 питание_raspberry_pi usb_type_c usb_pd power_delivery резистор блок_питания_5в

Всё работает!

Полезные запросы для отладки

Просмотр всех тегов статьи

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, ' ', '_'), '-', '_');

Выводы

Ключевые моменты:
  1. Уникальные индексы в базе могут вызывать неожиданное поведение при вставке данных
  2. Поиск по slug надёжнее, чем по name, особенно если форматы могут отличаться
  3. Всегда приводите данные к единому формату (регистр, разделители)
  4. Проверяйте наличие дубликатов после массовых изменений
  5. Используйте INSERT IGNORE для безопасной вставки, но не забывайте проверять результат
Совет: Регулярно проверяйте логи сервера и используйте консоль браузера (F12) для отладки JavaScript-кода. Это поможет быстро находить и исправлять проблемы.

Комментарии

Пока нет комментариев. Будьте первым!

Оставить комментарий

← Назад к списку статей

Посетителей сегодня: 0
о блоге

© Digital Specialist | Не являемся сотрудниками Google, Яндекса и NASA