【问题标题】:MySQL GROUP_CONCAT escapingMySQL GROUP_CONCAT 转义
【发布时间】: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 是否有一个函数可以让我转义这些字符? (请假设用户名可以包含任何字符,因为这只是一个示例架构)

【问题讨论】:

    标签: mysql escaping group-concat


    【解决方案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 '|')
        

        这将:

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

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

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

        【讨论】:

        • 我最终做了一些稍微不同的事情,但非常接近这个。谢谢!
        • 我也面临同样的问题。上述解决方案效果很好。但我不能写 (?
        • @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,然后循环遍历结果以将分组处理为使用客户端语言完成的过滤器:

              1. 首先将last_id 初始化为NULL
              2. 获取结果集的下一行(如果没有更多行,请转到第 6 步)
              3. 如果行的id不同于last_id,则开始一个新的输出行:

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

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

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

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

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

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

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

              【讨论】:

                【解决方案9】:

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

                我经常用

                GROUP_CONCAT(name SEPARATOR '"|"')
                

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

                【讨论】:

                  【解决方案10】:

                  REPLACE()

                  例子:

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

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

                  【讨论】:

                    猜你喜欢
                    • 2011-01-31
                    • 2013-03-28
                    • 2019-11-01
                    • 1970-01-01
                    • 2014-01-25
                    • 2013-03-08
                    • 2022-01-14
                    • 2012-05-07
                    • 2011-06-01
                    相关资源
                    最近更新 更多