CHAR语义和ORA-01461

时间:2011-03-08 09:03:44

标签: php oracle oci8

我使用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可能会导致其他问题(几乎所有客户都有单字节数据库)。

1 个答案:

答案 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。