Группа в MySQL concat может вырваться

(Примечание: этот вопрос не об экранировании запросов, речь идет об экранировании результатов)

Я использую GROUP_CONCAT для объединения нескольких строк в список с разделителями-запятыми. Например, предположим, что у меня есть две (пример) таблицы:

CREATE TABLE IF NOT EXISTS `Comment` (
`id` int(11) unsigned NOT NULL auto_increment,
`post_id` int(11) unsigned NOT NULL,
`name` varchar(255) collate utf8_unicode_ci NOT NULL,
`comment` varchar(255) collate utf8_unicode_ci NOT NULL,
PRIMARY KEY  (`id`),
KEY `post_id` (`post_id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=6 ;

INSERT INTO `Comment` (`id`, `post_id`, `name`, `comment`) VALUES
(1, 1, 'bill', 'some comment'),
(2, 1, 'john', 'another comment'),
(3, 2, 'bill', 'blah'),
(4, 3, 'john', 'asdf'),
(5, 4, 'x', 'asdf');


CREATE TABLE IF NOT EXISTS `Post` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(255) collate utf8_unicode_ci NOT NULL,
PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=7 ;

INSERT INTO `Post` (`id`, `title`) VALUES
(1, 'first post'),
(2, 'second post'),
(3, 'third post'),
(4, 'fourth post'),
(5, 'fifth post'),
(6, 'sixth post');

и я хочу перечислить все сообщения вместе со списком каждого имени пользователя, который прокомментировал сообщение:

SELECT
Post.id as post_id, Post.title as title, GROUP_CONCAT(name) 
FROM Post 
LEFT JOIN Comment on Comment.post_id = Post.id
GROUP BY Post.id

дает мне:

id  title   GROUP_CONCAT( name )
1   first post  bill,john
2   second post     bill
3   third post  john
4   fourth post     x
5   fifth post  NULL
6   sixth post  NULL

это работает отлично, за исключением того, что если имя пользователя содержит запятую это испортит список пользователей. Есть ли у MySQL функция, которая позволит мне избежать этих символов? (Предположим, что имена пользователей могут содержать любые символы, так как это только пример схемы)

10 ответов


Если есть какой-то другой символ, который является незаконным в именах пользователей, вы можете указать другой символ разделителя, используя малоизвестный синтаксис:

...GROUP_CONCAT(name SEPARATOR '|')...

... Вы хотите разрешить трубы? или какой-нибудь персонаж?

Escape символ разделителя, возможно, с обратной косой чертой, но прежде чем сделать это, escape обратные косые черты сами:

group_concat(replace(replace(name, '\', '\\'), '|', '\|') SEPARATOR '|')

Это:

  1. экранировать все обратные слеши с другой слеш
  2. побег сепаратор персонаж слеш
  3. объедините результаты с символом разделителя

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

  1. разделите результаты символом разделителя, где не предшествует обратная косая черта. На самом деле, это немного сложно, вы хотите разделить его, где ему не предшествует нечетное число из blackslashes. Это регулярное выражение будет соответствовать что:
    (?<!\)(?:\\)*\|
  2. замените все экранированные символы разделителя литералами, т. е. замените \ |на/
  3. замените все двойные косые черты обратными косыми чертами, например, замените \\ на \

на самом деле, есть ascii control characters специально разработан для разделения полей базы данных и записей:

0x1F (31): unit (fields) separator

0x1E (30): record separator

0x1D (29): group separator

подробнее: о символах ascii

вы никогда не будете иметь их в именах пользователей и, скорее всего, никогда в любом другом non-binary data в вашей базе данных, чтобы их можно было безопасно использовать:

GROUP_CONCAT(foo SEPARATOR 0x1D)

затем разделить на CHAR(0x1D) на любом языке клиента, который вы хотите.


Я бы предложил GROUP_CONCAT (разделитель имен '\n'), так как \n обычно не происходит. Это может быть немного проще, так как вам не нужно ничего избегать, но может привести к неожиданным проблемам. Кодирование / регулярное выражение декодирования, как предложил ник, конечно, тоже приятно.


REPLACE()

пример:

... GROUP_CONCAT(REPLACE(name, ',', '\,')) 

Примечание. Вы должны использовать двойную обратную косую черту (если вы избегаете запятой с обратной косой чертой), потому что сама обратная косая черта-это магия, и \, становится просто ,.


Если вы собираетесь делать декодирование в своем приложении, возможно, просто используйте hex:

SELECT GROUP_CONCAT(HEX(foo)) ...

или вы также можете поместить длину в них:

SELECT GROUP_CONCAT(CONCAT(LENGTH(foo), ':', foo)) ...

не то, что я тестировал: - D


то, что Ник сказал на самом деле, с улучшением-разделитель может быть более одного символа.

Я часто использовал

GROUP_CONCAT(name SEPARATOR '"|"')

шансы на имя пользователя, содержащее "|", довольно низки, я бы сказал.


вы попадаете в эту серую область, где может быть лучше обработать это за пределами мира SQL.

по крайней мере, это то, что я бы сделал: я бы просто заказал, а не GROUP BY, и цикл через результаты для обработки группировки в качестве фильтра на клиентском языке:

  1. начать с инициализации last_id до NULL
  2. Fetch следующая строка resultset (если нет больше строк, перейдите к шагу 6)
  3. если id строка отличается от last_id начать новую выходную строку:

    а. если last_id не равно NULL, а затем выведите сгруппированную строку

    b. установите новую сгруппированную строку = входную строку, но сохраните имя как один массив элементов

    Си. набор last_id к значению текущего ID

  4. в противном случае (id совпадает с last_id) добавьте имя строки в существующую сгруппированную строку.

  5. вернитесь к Шагу 2
  6. в противном случае вы закончили; если last_id не равно NULL, а затем выведите существующую строку группы.

затем ваш вывод заканчивается, включая имена, организованные как массив, и может решить, как вы хотите обрабатывать/escape/форматировать их.

какой язык / система вы используете? РНР? На Perl? Ява?


Jason S: это именно тот вопрос, с которым я имею дело. Я использую фреймворк PHP MVC и обрабатываю результаты, как вы описываете (несколько строк на результат и код для группировки результатов вместе). Тем не менее, я работаю над двумя функциями для реализации моих моделей. Один возвращает список всех необходимых полей, необходимых для воссоздания объекта, а другой-функцию, которая, задав строку с полями из первой функции, создает экземпляр нового объекта. Это позволяет мне запросить строку из базы данных и легко превратить его обратно в объект, не зная внутренних данных, необходимых модели. Это не работает так хорошо, когда несколько строк представляют один объект, поэтому я пытался использовать GROUP_CONCAT, чтобы обойти эту проблему.


прямо сейчас я разрешаю любой символ. Я понимаю, что трубка вряд ли появится, но я хотел бы ее разрешить.

Как насчет управляющего символа, который вы должны удалить из ввода приложения в любом случае? Я сомневаюсь, что вам нужен eg. вкладка или новая строка в поле Имя.


чтобы расширить некоторые ответы, я реализовал @derobert ' s второе предложение в PHP и он работает хорошо. Учитывая MySQL, например:

GROUP_CONCAT(CONCAT(LENGTH(field), ':', field) SEPARATOR '') AS fields

я использовал следующую функцию, чтобы разделить его:

function concat_split( $str ) {
    // Need to guard against PHP's stupid multibyte string function overloading.
    static $mb_overload_string = null;
    if ( null === $mb_overload_string ) {
        $mb_overload_string = defined( 'MB_OVERLOAD_STRING' )
                && ( ini_get( 'mbstring.func_overload' ) & MB_OVERLOAD_STRING );
    }
    if ( $mb_overload_string ) {
        $mb_internal_encoding = mb_internal_encoding();
        mb_internal_encoding( '8bit' );
    }

    $ret = array();
    for ( $offset = 0; $colon = strpos( $str, ':', $offset ); $offset = $colon + 1 + $len ) {
        $len = intval( substr( $str, $offset, $colon ) );
        $ret[] = substr( $str, $colon + 1, $len );
    }

    if ( $mb_overload_string ) {
        mb_internal_encoding( $mb_internal_encoding );
    }

    return $ret;
}

Я также первоначально реализовал предложение @ııu, используя один из сепараторов @ lemon juice. Он работал нормально, но помимо его усложнения он был медленнее, основная проблема заключалась в том, что PCRE позволяет только фиксированную длину lookbehind, поэтому с помощью предложенного регулярное выражение для разделения требует захвата разделителей, иначе двойные косые черты в конце строк будут потеряны. Поэтому, учитывая MySQL, такой как (примечание 4 PHP backslashes => 2 MySQL backslashes => 1 real backslash):

GROUP_CONCAT(REPLACE(REPLACE(field, '\\', '\\\\'),
    CHAR(31), CONCAT('\\', CHAR(31))) SEPARATOR 0x1f) AS fields

функция разделения была:

function concat_split( $str ) {
    $ret = array();
    // 4 PHP backslashes => 2 PCRE backslashes => 1 real backslash.
    $strs = preg_split( '/(?<!\\)((?:\\\\)*+\x1f)/', $str, -1, PREG_SPLIT_DELIM_CAPTURE );
    // Need to add back any captured double backslashes.
    for ( $i = 0, $cnt = count( $strs ); $i < $cnt; $i += 2 ) {
        $ret[] = isset( $strs[ $i + 1 ] ) ? ( $strs[ $i ] . substr( $strs[ $i + 1 ], 0, -1 ) ) : $strs[ $i ];
    }
    return str_replace( array( "\\x1f", "\\" ), array( "\x1f", "\" ), $ret );
}