Perl DBI (MySQL) 在准备语句中放置单引号而不是实际参数
Perl DBI (MySQL) puts single quote instead of actual parameter in prepared statement
我正在尝试将一个简单的查询作为准备好的语句进行,但没有成功。这是代码:
package sqltest;
use DBI;
DBI->trace(2);
my $dbh = DBI->connect('dbi:mysql:database=test;host=***;port=3306','the_username', '****');
my $prep = 'SELECT me.id, me.session_data, me.expires FROM sys_session me WHERE me.id = ?';
$dbh->{RaiseError} = 1;
my $sth = $dbh->prepare($prep);
$sth->bind_param(1, 'session:06b6d2138df949524092eefc066ee5ab3598bf96');
$sth->execute;
DBI::dump_results($sth);
MySQL 服务器响应语法错误 near '''
.
DBI 跟踪的输出显示
-> bind_param for DBD::mysql::st (DBI::st=HASH(0x21e35cc)~0x21e34f4 1 'session:06b6d2138df949524092eefc066ee5ab3598bf96') thr#3ccdb4
Called: dbd_bind_ph
<- bind_param= ( 1 ) [1 items] at perl_test_dbi_params.pl line 10
[...]
>parse_params statement SELECT me.id, me.session_data, me.expires FROM sys_session me WHERE me.id = ?
Binding parameters: SELECT me.id, me.session_data, me.expires FROM sys_session me WHERE me.id = '
[...]
DBD::mysql::st execute failed: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''' at line 1
所以对我来说,声明似乎没有按应有的方式准备。
当我发送不带参数的查询时,它按预期工作。
我在这里想念什么?
DBI 版本为 DBI 1.637-ithread
,MySQL 版本为 5.5.57-0+deb8u1
测试 Windows perl 5, version 26, subversion 1 (v5.26.1) built for MSWin32-x86-multi-thread-64int
和 Ubuntu perl 5, version 22, subversion 1 (v5.22.1) built for x86_64-linux-gnu-thread-multi
编辑1:
对于上下文:我在将 Catalyst 与 Catalyst::Plugin::Session::Store::DBIC 一起使用时注意到了这个问题。在这里,id 列是一个 Varchar(72) 类型,它包含一个 session-id。
编辑2:
- DBD::mysql 版本为
4.043
- 通过
$sth->execute('session:foo');
绑定导致同样的问题
- 通过
$sth->bind_param('session:foo', SQL_VARCHAR);
绑定导致同样的问题
- 绑定数字字段确实有效,但仅适用于显式类型定义
$sth->bind_param(1, 1512407082, SQL_INTEGER);
编辑3:
我抽出时间做了更多测试,但没有得到令人满意的结果:
- 我能够使用较旧的服务器进行测试并且它有效。 DBI 和 DBD::mysql 的版本相同,但我发现服务器使用 MySQL 5.5 客户端,在 DBI-trace 中报告为
MYSQL_VERSION_ID 50557
,而我的原始测试服务器都使用 MySQL 5.7 MYSQL_VERSION_ID 50720
和 MYSQL_VERSION_ID 50716
$dbh->{mysql_server_prepare} = 1;
有效!也许这对找到这个问题的人有帮助,但我现在宁愿找出问题的真正原因
经过一些测试,我得出的结论是,这似乎是 DBD::mysql 和 [=41 之间的兼容性问题=] 客户端 5.7 (and/or MySQL 服务器 5.5).
至少,我找到了 Ubuntu 16 (xenial) 的解决方案,所以对于其他人来说,可能 运行 会遇到同样的问题:
- 如所述 here 降级至 MySQL 5.6。对我来说,安装
libmysqlclient-dev
没有 server/client 就足够了
- 重新安装 DBD::MySQL
sudo cpanm --reinstall DBD::mysql
,以便使用现在安装的 MySQL 5.6 进行构建
我会在 DBD::mysql GitHub 提交一个问题,如果有任何关于这个问题的消息,我会更新这个答案。
另一种解决方案,对我也有效:
让服务器准备您的语句 $dbh->{mysql_server_prepare} = 1;
从您的跟踪日志中可以看出,问号占位符 (?) 已由 DBD::mysql 替换为 one 撇号 (')。所以这是纯粹的 DBD::mysql 错误。乍一看它根本没有意义......因为占位符被放入两个撇号的参数替换。
可以在此处找到执行此占位符替换的相关代码:https://metacpan.org/source/MICHIELB/DBD-mysql-4.043/dbdimp.c#L784-786
*ptr++ = '\'';
ptr += mysql_real_escape_string(sock, ptr, valbuf, vallen);
*ptr++ = '\'';
那么问题来了,上面的 C 代码可以在 *ptr 缓冲区中只生成一个撇号吗?答案是肯定的,when mysql_real_escape_string() returns integer of same value as pointer size minus one -- 模拟减一的数字操作,当两个撇号都写到 *ptr 中的相同位置时缓冲区。
这会发生吗?是的,它可以,因为 Oracle 在 MySQL 5.7.6 客户端库中更改了 API of mysql_real_escape_string() C 函数:
https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-6.html#mysqld-5-7-6-feature
Incompatible Change: A new C API function, mysql_real_escape_string_quote(), has been implemented as a replacement for mysql_real_escape_string() because the latter function can fail to properly encode characters when the NO_BACKSLASH_ESCAPES SQL mode is enabled. In this case, mysql_real_escape_string() cannot escape quote characters except by doubling them, and to do this properly, it must know more information about the quoting context than is available. mysql_real_escape_string_quote() takes an extra argument for specifying the quoting context. For usage details, see mysql_real_escape_string_quote().
来自 MySQL 5.7.6 版本的 mysql_real_escape_string() 的文档说:
https://dev.mysql.com/doc/refman/5.7/en/mysql-real-escape-string.html
Return Values: The length of the encoded string that is placed into the to argument, not including the terminating null byte, or -1 if an error occurs.
因此,如果您在 MySQL 服务器上启用 NO_BACKSLASH_ESCAPES SQL 模式,那么来自 MySQL 5.7.6 客户端的 mysql_real_escape_string() 将无法工作并且return 错误,因此 -1 转换为 unsigned long。 unsigned long 在 32 位和 64 位 x86 平台上都与指针大小相同,因此来自 DBD::mysql 驱动程序的以上 C 代码导致一个撇号字符。
现在我为 DBD::MariaDB driver (fork of DBD::mysql) in following pull request: https://github.com/gooddata/DBD-MariaDB/pull/77
解决了这个问题
因此 DBD::MariaDB 与 MySQL 5.7 客户端库一起编译时也将兼容。
我正在尝试将一个简单的查询作为准备好的语句进行,但没有成功。这是代码:
package sqltest;
use DBI;
DBI->trace(2);
my $dbh = DBI->connect('dbi:mysql:database=test;host=***;port=3306','the_username', '****');
my $prep = 'SELECT me.id, me.session_data, me.expires FROM sys_session me WHERE me.id = ?';
$dbh->{RaiseError} = 1;
my $sth = $dbh->prepare($prep);
$sth->bind_param(1, 'session:06b6d2138df949524092eefc066ee5ab3598bf96');
$sth->execute;
DBI::dump_results($sth);
MySQL 服务器响应语法错误 near '''
.
DBI 跟踪的输出显示
-> bind_param for DBD::mysql::st (DBI::st=HASH(0x21e35cc)~0x21e34f4 1 'session:06b6d2138df949524092eefc066ee5ab3598bf96') thr#3ccdb4
Called: dbd_bind_ph
<- bind_param= ( 1 ) [1 items] at perl_test_dbi_params.pl line 10
[...]
>parse_params statement SELECT me.id, me.session_data, me.expires FROM sys_session me WHERE me.id = ?
Binding parameters: SELECT me.id, me.session_data, me.expires FROM sys_session me WHERE me.id = '
[...]
DBD::mysql::st execute failed: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''' at line 1
所以对我来说,声明似乎没有按应有的方式准备。 当我发送不带参数的查询时,它按预期工作。
我在这里想念什么?
DBI 版本为 DBI 1.637-ithread
,MySQL 版本为 5.5.57-0+deb8u1
测试 Windows perl 5, version 26, subversion 1 (v5.26.1) built for MSWin32-x86-multi-thread-64int
和 Ubuntu perl 5, version 22, subversion 1 (v5.22.1) built for x86_64-linux-gnu-thread-multi
编辑1:
对于上下文:我在将 Catalyst 与 Catalyst::Plugin::Session::Store::DBIC 一起使用时注意到了这个问题。在这里,id 列是一个 Varchar(72) 类型,它包含一个 session-id。
编辑2:
- DBD::mysql 版本为
4.043
- 通过
$sth->execute('session:foo');
绑定导致同样的问题 - 通过
$sth->bind_param('session:foo', SQL_VARCHAR);
绑定导致同样的问题 - 绑定数字字段确实有效,但仅适用于显式类型定义
$sth->bind_param(1, 1512407082, SQL_INTEGER);
编辑3:
我抽出时间做了更多测试,但没有得到令人满意的结果:
- 我能够使用较旧的服务器进行测试并且它有效。 DBI 和 DBD::mysql 的版本相同,但我发现服务器使用 MySQL 5.5 客户端,在 DBI-trace 中报告为
MYSQL_VERSION_ID 50557
,而我的原始测试服务器都使用 MySQL 5.7MYSQL_VERSION_ID 50720
和MYSQL_VERSION_ID 50716
$dbh->{mysql_server_prepare} = 1;
有效!也许这对找到这个问题的人有帮助,但我现在宁愿找出问题的真正原因
经过一些测试,我得出的结论是,这似乎是 DBD::mysql 和 [=41 之间的兼容性问题=] 客户端 5.7 (and/or MySQL 服务器 5.5).
至少,我找到了 Ubuntu 16 (xenial) 的解决方案,所以对于其他人来说,可能 运行 会遇到同样的问题:
- 如所述 here 降级至 MySQL 5.6。对我来说,安装
libmysqlclient-dev
没有 server/client 就足够了 - 重新安装 DBD::MySQL
sudo cpanm --reinstall DBD::mysql
,以便使用现在安装的 MySQL 5.6 进行构建
我会在 DBD::mysql GitHub 提交一个问题,如果有任何关于这个问题的消息,我会更新这个答案。
另一种解决方案,对我也有效:
让服务器准备您的语句 $dbh->{mysql_server_prepare} = 1;
从您的跟踪日志中可以看出,问号占位符 (?) 已由 DBD::mysql 替换为 one 撇号 (')。所以这是纯粹的 DBD::mysql 错误。乍一看它根本没有意义......因为占位符被放入两个撇号的参数替换。
可以在此处找到执行此占位符替换的相关代码:https://metacpan.org/source/MICHIELB/DBD-mysql-4.043/dbdimp.c#L784-786
*ptr++ = '\'';
ptr += mysql_real_escape_string(sock, ptr, valbuf, vallen);
*ptr++ = '\'';
那么问题来了,上面的 C 代码可以在 *ptr 缓冲区中只生成一个撇号吗?答案是肯定的,when mysql_real_escape_string() returns integer of same value as pointer size minus one -- 模拟减一的数字操作,当两个撇号都写到 *ptr 中的相同位置时缓冲区。
这会发生吗?是的,它可以,因为 Oracle 在 MySQL 5.7.6 客户端库中更改了 API of mysql_real_escape_string() C 函数:
https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-6.html#mysqld-5-7-6-feature
Incompatible Change: A new C API function, mysql_real_escape_string_quote(), has been implemented as a replacement for mysql_real_escape_string() because the latter function can fail to properly encode characters when the NO_BACKSLASH_ESCAPES SQL mode is enabled. In this case, mysql_real_escape_string() cannot escape quote characters except by doubling them, and to do this properly, it must know more information about the quoting context than is available. mysql_real_escape_string_quote() takes an extra argument for specifying the quoting context. For usage details, see mysql_real_escape_string_quote().
来自 MySQL 5.7.6 版本的 mysql_real_escape_string() 的文档说:
https://dev.mysql.com/doc/refman/5.7/en/mysql-real-escape-string.html
Return Values: The length of the encoded string that is placed into the to argument, not including the terminating null byte, or -1 if an error occurs.
因此,如果您在 MySQL 服务器上启用 NO_BACKSLASH_ESCAPES SQL 模式,那么来自 MySQL 5.7.6 客户端的 mysql_real_escape_string() 将无法工作并且return 错误,因此 -1 转换为 unsigned long。 unsigned long 在 32 位和 64 位 x86 平台上都与指针大小相同,因此来自 DBD::mysql 驱动程序的以上 C 代码导致一个撇号字符。
现在我为 DBD::MariaDB driver (fork of DBD::mysql) in following pull request: https://github.com/gooddata/DBD-MariaDB/pull/77
解决了这个问题因此 DBD::MariaDB 与 MySQL 5.7 客户端库一起编译时也将兼容。