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 '|')
这将:
-
用另一个反斜杠转义任何反斜杠
用反斜杠转义分隔符
用分隔符连接结果
要获得未转义的结果,请以相反的顺序执行相同的操作:
-
在前面没有反斜杠的地方用分隔符分割结果。实际上,这有点棘手,您想将它拆分到前面没有奇数 个黑斜线的地方。此正则表达式将匹配:
(?<!\\)(?:\\\\)*\|
用文字替换所有转义的分隔符,即替换 \|与 |
用单反斜杠替换所有双反斜杠,例如将 \\ 替换为 \
【讨论】:
我最终做了一些稍微不同的事情,但非常接近这个。谢谢! 我也面临同样的问题。上述解决方案效果很好。但我不能写 (? @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
相同)将行名称附加到现有的分组行上。
last_id
不为NULL,则输出现有组行。
然后您的输出最终会包含以数组形式组织的名称,然后您可以决定如何处理/转义/格式化它们。
您使用什么语言/系统? php?珀尔?爪哇?
【讨论】:
【参考方案9】:nick 说的是真的,经过改进 - 分隔符也可以是多个字符。
我经常用
GROUP_CONCAT(name SEPARATOR '"|"')
用户名包含“|”的可能性我会说相当低。
【讨论】:
【参考方案10】:REPLACE()
例子:
... GROUP_CONCAT(REPLACE(name, ',', '\\,'))
请注意,您必须使用双反斜杠(如果您用反斜杠转义逗号),因为反斜杠本身很神奇,\,
变成了简单的,
。
【讨论】:
以上是关于MySQL GROUP_CONCAT 转义的主要内容,如果未能解决你的问题,请参考以下文章
sql server 实现mysql中group_concat,列转行,列用分隔符拼接字符串