MySQL GROUP_CONCAT экранирование - PullRequest
13 голосов
/ 17 января 2009

(ПРИМЕЧАНИЕ. Этот вопрос не об экранировании запросов, а об экранировании результатов)

Я использую 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 ]

38 голосов
/ 07 марта 2012

На самом деле существуют 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) на любом языке клиента, который вы хотите.

13 голосов
/ 17 января 2009

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

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

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

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

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

Это будет:

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

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

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

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

3 голосов
/ 17 января 2009

REPLACE()

Пример:

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

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

1 голос
/ 17 января 2009

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

SELECT GROUP_CONCAT(HEX(foo)) ...

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

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

Не то чтобы я тоже тестировал: -D

0 голосов
/ 31 мая 2016

Просто, чтобы расширить некоторые ответы, я реализовал второе предложение @derobert в 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 допускает только просмотр задним числом фиксированной длины, поэтому использование предложенного регулярного выражения для разделения требует захвата разделителей, в противном случае двойные обратные слеши в конце строк будут потеряны. Таким образом, учитывая MySQL, такой как (примечание 4 обратная косая черта PHP => 2 обратная косая черта MySQL => 1 реальная обратная косая черта):

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 );
}
0 голосов
/ 18 января 2009

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

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

0 голосов
/ 17 января 2009

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

0 голосов
/ 17 января 2009

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

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

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

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

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

    с. установите last_id на значение текущего идентификатора

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

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

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

Какой язык / систему вы используете? PHP? Perl? Джава?

0 голосов
/ 17 января 2009

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

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

GROUP_CONCAT(name SEPARATOR '"|"')

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

...