Невозможно сохранить знак Euro в свойство LOB String с помощью Hibernate/PostgreSQL

У меня возникли проблемы с написанием и чтением специальных символов, таких как знак Евро (€), в свойства LOB String в PostgreSQL 8.4 с Hibernate 3.6.10.

Я знаю, что PostgreSQL предоставляет два разных способа хранения больших объектов символа в столбце таблицы. Они могут быть сохранены либо непосредственно в столбец таблицы, либо косвенно в отдельной таблице (на самом деле это называется pg_largeobject). В последнем случае столбец содержит ссылку (OID) в строке в pg_largeobject.

Поведение по умолчанию в Hibernate 3.6.10 - это косвенный подход OID. Тем не менее, можно добавить дополнительную аннотацию @org.hibernate.annotations.Type(type = "org.hibernate.type.TextType" ) к свойству Lob, чтобы получить прямое поведение хранилища.

Оба подхода работают отлично, за исключением того, что я хочу работать со специальными символами, такими как знак Euro (€). В этом случае механизм прямого хранения продолжает работать, но механизм косвенного хранения прерывается.

Я хотел бы продемонстрировать это на примере. Я создал тестовый объект с 2 свойствами @Lob. Один следует принципу прямого хранения, а другой - косвенному хранению:

@Basic
@Lob
@Column(name = "CLOB_VALUE_INDIRECT_STORAGE", length = 2147483647)
public String getClobValueIndirectStorage()

и

@Basic
@Lob
@org.hibernate.annotations.Type(type="org.hibernate.type.TextType")
@Column(name = "CLOB_VALUE_DIRECT_STORAGE", length = 2147483647)
public String getClobValueDirectStorage()

Если я создаю объект, заполните оба свойства знаком Euro, а затем сохраняйте его в базе данных, я вижу следующее, когда я делаю SELECT, я вижу

 id | clob_value_direct_storage | clob_value_indirect_storage
----+---------------------------+----------------------------
  6 | €                         | 910579                     

Если я затем запрошу таблицу pg_largeobject, я вижу:

  loid  | pageno | data
--------+--------+------
 910579 |      0 | \254

Столбец "data" pg_largeobject имеет тип bytea, что означает, что информация хранится как необработанные байты. Выражение "\ 254" представляет собой один байт, а в UTF-8 - символ "¬". Это именно то значение, которое я возвращаю, когда я загружаю объект обратно из базы данных.

Знак Euro в UTF-8 состоит из 3 байтов, поэтому я ожидал, что столбец "data" будет иметь 3 байта, а не 1.

Это происходит не только для знака Euro, но и для многих специальных символов. Это проблема в Hibernate? Или драйвер JDBC? Есть ли способ, которым я могу настроить это поведение?

Спасибо заранее,
С уважением,
Franck de Bruijn

Ответ 1

После большого поиска в исходном коде Hibernate и драйвера PostgreSQL JDBC мне удалось найти основную причину проблемы. В итоге метод write() BlobOutputStream (предоставляемый драйвером JDBC) вызывается для записи содержимого Clob в базу данных. Этот метод выглядит следующим образом:

public void write(int b) throws java.io.IOException
{
    checkClosed();
    try
    {
        if (bpos >= bsize)
        {
            lo.write(buf);
            bpos = 0;
        }
        buf[bpos++] = (byte)b;
    }
    catch (SQLException se)
    {
        throw new IOException(se.toString());
    }
}

Этот метод принимает "int" (32 бит /4 байта) в качестве аргумента и преобразует его в "байты" (8 бит /1 байт), эффективно теряя 3 байта информации. Строковые представления внутри Java кодируются в кодировке UTF-16, что означает, что каждый символ представлен 16 бит /2 байта. Знак Euro имеет значение int 8364. После преобразования в байт значение 172 остается (в октетном представлении 254).

Я не уверен, что сейчас самое лучшее решение этой проблемы. IMHO драйвер JDBC должен нести ответственность за кодирование/декодирование символов Java UTF-16 любой кодировке, необходимой для базы данных. Тем не менее, я не вижу возможности настройки в коде драйвера JDBC, чтобы изменить его поведение (и я не хочу писать и поддерживать свой собственный код драйвера JDBC).

Поэтому я расширил Hibernate с помощью пользовательского ClobType и смог преобразовать символы UTF-16 в UTF-8 перед записью в базу данных и наоборот при извлечении Clob.

Решения слишком велики, чтобы просто вставить этот ответ. Если вам интересно, напишите мне письмо, и я отправлю его вам.

Cheers, Franck