我使用Oracle后端维护一个PHP驱动的应用程序(OCI8函数)。该应用程序使用Oracle 10g XE开发,并部署在客户拥有的任何版本上。
该应用程序处理单字节文本(ISO-8859-15),在针对Oracle XE的 Western European 版本进行开发时,我从未遇到任何问题。但是,我最近安装了通用版本,我在插入带有非ASCII字符的大字符串时遇到了问题。此版本设置NLS_CHARACTERSET = AL32UTF8
;因为我的应用程序使用WE8ISO8859P15
Oracle默认将输入数据从ISO-8859-15转换为UTF-8(这很好)。但似乎某些大小检查出错了:一个包含1500个€
个字符的字符串(ISO-8889-15中的1500个字节,UTF-8中的4500个字节)似乎溢出VARCHAR2(4000 CHAR)
列。
我已经创建了这个测试表:
CREATE TABLE FOO (
FOO_ID NUMBER NOT NULL ENABLE,
DATA_BYTE VARCHAR2(4000 BYTE),
DATA_CHAR VARCHAR2(4000 CHAR),
CONSTRAINT FOO_PK PRIMARY KEY (FOO_ID)
);
使用此代码可以重现该问题:
<?php
$connection = oci_connect(DB_USER, DB_PASS, DB_CONN_STRING, 'WE8ISO8859P15');
if( !$connection ){
$e = oci_error();
die(htmlspecialchars($e['message']));
}
$id = 1;
$data = str_repeat('€', 1500);
$sql = 'INSERT INTO FOO (FOO_ID, DATA_CHAR) ' .
'VALUES (:id, :data)';
$res = oci_parse($connection, $sql);
if(!$res){
$e = oci_error();
die(htmlspecialchars($e['message']));
}
if(!oci_bind_by_name($res, ':id', $id)){
$e = oci_error();
die(htmlspecialchars($e['message']));
}
if(!oci_bind_by_name($res, ':data', $data)){
$e = oci_error();
die(htmlspecialchars($e['message']));
}
if(!oci_execute($res, OCI_COMMIT_ON_SUCCESS)){
$e = oci_error();
die(htmlspecialchars($e['message']));
}
...触发:
警告:oci_execute(): ORA-01461:sólopuedeenlazar un valor LONG para insertarlo en una columna LONG
当我尝试插入4001字符串时,我得到的错误相同。如果我插入xxx...
而不是€€€
,则不会发生这种情况,如果我将脚本保存为UTF-8并按原样连接则不会发生:
<?php
$connection = oci_connect(DB_USER, DB_PASS, DB_CONN_STRING, 'AL32UTF8');
[更新:我的测试存在缺陷。使用UTF-8并不能避免使用ORA-01461]
如何覆盖此问题? NLS_CHARACTERSET数据库参数不是我控制的东西,将我的应用程序切换到UTF-8可能会导致其他问题(几乎所有客户都有单字节数据库)。
答案 0 :(得分:10)
除非您想使用CLOB而不是VARCHAR2,否则这可能不是您可以解决的问题。
在Oracle中,当您声明列时,默认使用字节长度语义。因此,VARCHAR2(100)例如分配100个字节的存储空间。如果您使用的是ISO 8859-1之类的单字节字符集,则每个字符都需要1个字节的存储空间,因此这也会为100个字符分配空间。但是,如果您使用的是UFT-8之类的多字节字符集,则每个字符可能需要1到4个字节的存储空间。因此,根据数据,VARCHAR2(100)可能只能存储25个字符的数据(英文字符通常需要1个字节,欧洲字符通常需要2个字节,亚洲字符通常需要3个字节)。
您可以告诉Oracle使用字符长度语义,这通常是我从ISO-8859-1数据库迁移到UTF-8数据库时的建议。如果声明列VARCHAR2(100 CHAR),Oracle将为100个字符分配空间,无论最终是100字节还是400字节。您还可以将NLS_LENGTH_SEMANTICS参数设置为CHAR以更改默认值(对于新DDL),以便VARCHAR2(100)分配100个字符的存储而不是100个字节。
不幸的是,对于您来说,Oracle VARCHAR2的大小限制(在SQL引擎而不是PL / SQL引擎的上下文中)是4000字节。因此,即使您声明了一个列VARCHAR2(4000 CHAR),您仍然会被限制为实际插入4000字节的数据,这些数据可能只有1000个字符。例如,在使用AL32UTF8字符集的数据库中,我可以声明一个列VARCHAR2(4000 CHAR),但是插入一个需要2个字节存储空间的字符表明我无法真正插入4000个字符的数据
SQL> create table foo (
2 col1 varchar2(4000 char)
3 );
Table created.
SQL> insert into foo values( rpad( 'abcde', 4000, unistr('\00f6') ) );
1 row created.
SQL> ed
Wrote file afiedt.buf
1* insert into foo values( rpad( 'abcde', 6000, unistr('\00f6') ) )
SQL> /
1 row created.
SQL> select length(col1), lengthb(col1)
2 from foo;
LENGTH(COL1) LENGTHB(COL1)
------------ -------------
2003 4000
2003 4000
如果需要存储4000个UTF-8数据字符,则需要一个可处理16000字节的数据类型,这样就必须转移到CLOB。