MySQL GROUP_CONCAT 转义

Posted

技术标签:

【中文标题】MySQL GROUP_CONCAT 转义【英文标题】:MySQL GROUP_CONCAT escaping 【发布时间】:2010-10-01 22:17:09 【问题描述】:

(注意:这个问题不是关于转义查询,而是关于转义结果)

我正在使用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 是否有一个函数可以让我转义这些字符? (请假设用户名可以包含任何字符,因为这只是一个示例架构)

【问题讨论】:

【参考方案1】:

只是为了扩展一些答案,我在 php 中实现了 @derobert 的 second suggestion 并且效果很好。给定 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;

我最初还使用@Lemon Juice 的分隔符之一实现了@ʞɔıu 的建议。它工作得很好,但除了它的复杂性之外它更慢,主要问题是 PCRE 只允许固定长度的lookbehind,因此使用建议的正则表达式进行拆分需要捕获分隔符,否则字符串末尾的双反斜杠将丢失。所以给定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 );

【讨论】:

【参考方案2】:

其实还有ascii control characters专门用来分离数据库字段和记录的:

0x1F (31): unit (fields) separator

0x1E (30): record separator

0x1D (29): group separator

阅读更多:about ascii characters

您永远不会在用户名中使用它们,而且很可能永远不会在数据库中的任何其他non-binary data 中使用它们,因此可以安全地使用它们:

GROUP_CONCAT(foo SEPARATOR 0x1D)

然后以您想要的任何客户端语言按CHAR(0x1D) 分割。

【讨论】:

这应该是公认的答案。对于使用 SQLite 的任何人,其 SELECT "foo_0" || CHAR(0x1F) || "foo_1" AS "foo_concat"...【参考方案3】:

我建议使用 GROUP_CONCAT(name SEPARATOR '\n'),因为 \n 通常不会出现。这可能会更简单一些,因为您不需要逃避任何事情,但可能会导致意想不到的问题。 nick 提出的编码/正则表达式解码当然也不错。

【讨论】:

【参考方案4】:

如果用户名中存在其他非法字符,您可以使用鲜为人知的语法指定不同的分隔符:

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

... 你想允许管道吗?还是什么角色?

转义分隔符,可能使用反斜杠,但在此之前转义反斜杠本身:

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

这将:

    用另一个反斜杠转义任何反斜杠 用反斜杠转义分隔符 用分隔符连接结果

要获得未转义的结果,请以相反的顺序执行相同的操作:

    在前面没有反斜杠的地方用分隔符分割结果。实际上,这有点棘手,您想将它拆分到前面没有奇数 个黑斜线的地方。此正则表达式将匹配:(?&lt;!\\)(?:\\\\)*\| 用文字替换所有转义的分隔符,即替换 \|与 | 用单反斜杠替换所有双反斜杠,例如将 \\ 替换为 \

【讨论】:

我最终做了一些稍微不同的事情,但非常接近这个。谢谢! 我也面临同样的问题。上述解决方案效果很好。但我不能写 (? @Sangam254 这应该是一个单独的问题/帖子。【参考方案5】:

现在我允许任何字符。我知道管道不太可能出现,但我想允许它。

控制字符怎么样,无论如何你都应该从应用程序输入中去掉它?我怀疑你需要例如。名称字段中的制表符或换行符。

【讨论】:

【参考方案6】:

Jason S:这正是我正在处理的问题。我正在使用 PHP MVC 框架,并且正在像您描述的那样处理结果(每个结果多行和将结果组合在一起的代码)。但是,我一直在为我的模型实现两个功能。一个返回重新创建对象所需的所有必要字段的列表,另一个是一个函数,它给定一行包含第一个函数的字段,实例化一个新对象。这让我可以从数据库中请求一行并轻松地将其转回对象,而无需了解模型所需数据的内部结构。当多行代表一个对象时,这不会很好,所以我试图使用 GROUP_CONCAT 来解决这个问题。

【讨论】:

【参考方案7】:

如果您要在应用程序中进行解码,也许只需使用hex

SELECT GROUP_CONCAT(HEX(foo)) ...

或者你也可以在其中输入长度:

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

我也没有测试过:-D

【讨论】:

【参考方案8】:

您正在进入灰色地带,最好在 SQL 世界之外进行后处理。

至少我会这样做:我只需要 ORDER BY 而不是 GROUP BY,然后循环遍历结果以将分组处理为使用客户端语言完成的过滤器:

    首先将last_id 初始化为NULL 获取结果集的下一行(如果没有更多行,请转到第 6 步)

    如果行的id不同于last_id,则开始一个新的输出行:

    一个。如果last_id 不为NULL,则输出分组行

    b.将新分组行设置为输入行,但将名称存储为单个元素数组

    c。将last_id设置为当前ID的值

    否则(id 与last_id 相同)将行名称附加到现有的分组行上。

    返回步骤 2 否则你已经完成了;如果last_id 不为NULL,则输出现有组行。

然后您的输出最终会包含以数组形式组织的名称,然后您可以决定如何处理/转义/格式化它们。

您使用什么语言/系统? php?珀尔?爪哇?

【讨论】:

【参考方案9】:

nick 说的是真的,经过改进 - 分隔符也可以是多个字符。

我经常用

GROUP_CONCAT(name SEPARATOR '"|"')

用户名包含“|”的可能性我会说相当低。

【讨论】:

【参考方案10】:

REPLACE()

例子:

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

请注意,您必须使用双反斜杠(如果您用反斜杠转义逗号),因为反斜杠本身很神奇,\, 变成了简单的,

【讨论】:

以上是关于MySQL GROUP_CONCAT 转义的主要内容,如果未能解决你的问题,请参考以下文章

sql server 实现mysql中group_concat,列转行,列用分隔符拼接字符串

连接和转义字符后输出不完整[重复]

解决mysql中group_concat长度限制的方案

mysql 的group_concat方法

mysql用GROUP_CONCAT合并查询出现乱码?求大神!!!

mysql—group_concat函数