新聞中心
?譯者 | 趙青窕

創(chuàng)新互聯(lián)建站是一家集網(wǎng)站建設,賓川企業(yè)網(wǎng)站建設,賓川品牌網(wǎng)站建設,網(wǎng)站定制,賓川網(wǎng)站建設報價,網(wǎng)絡營銷,網(wǎng)絡優(yōu)化,賓川網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強企業(yè)競爭力??沙浞譂M足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時我們時刻保持專業(yè)、時尚、前沿,時刻以成就客戶成長自我,堅持不斷學習、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實用型網(wǎng)站。
審校 | 孫淑娟
基于JPA實體定義的DDL生成器是許多開發(fā)人員的日常任務。在大多數(shù)情況下,我們使用Hibernate內置生成器或JPA Buddy插件等工具,這些工具會簡化我們的工作,但也有例外,當涉及到在數(shù)據(jù)庫中存儲大量數(shù)據(jù)時,情況會變得有點復雜。
用例:存儲文檔
假設需要在PostgreSQL數(shù)據(jù)庫中存儲一個非空的文檔對象。JPA實體代碼如下所示:
Java:
@Entity
@Table(name = "document")
public class Document {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "date_created", nullable = false)
private LocalDateTime dateCreated;
@Column(name = "doc_txt")
private String docText;
//Getters and setters omitted for brevity
}
問題是:如果我們需要存儲非常長的文檔文本怎么辦?在Java中,字符串數(shù)據(jù)類型可以保存大約2Gb的文本數(shù)據(jù),但是對于上面的模型,表的列(table column)大小將被限制為255個字符。那么,我們應該改變什么呢?
方法1:使用LOB存儲
在關系數(shù)據(jù)庫中,存在一種用于存儲大量數(shù)據(jù)的特定數(shù)據(jù)類型:LOB(Large OBject)。一旦需要在數(shù)據(jù)庫中存儲大型文本,我們就可以開始定義LOB列。我們需要做的就是用@Lob注釋標記docText屬性。
Java:
@Lob
@Column(name = "doc_txt")
private String docText;
讓我們使用Hibernate為表生成DDL,以映射“Document”實體。SQL將是:
SQL:
create table document (
id int8 generated by default as identity,
date_created timestamp not null,
doc_txt oid,
primary key (id)
);
可以看到,doc_text列的數(shù)據(jù)類型是oid。它是什么?在文檔中有如下說明:
PostgreSQL提供了兩種不同的方式來存儲二進制數(shù)據(jù)。二進制數(shù)據(jù)可以使用數(shù)據(jù)類型BYTEA或使用Large Object特性存儲在表中,該特性以特殊格式將二進制數(shù)據(jù)存儲在單獨的表中,并通過在表中存儲OID類型的值來引用該表。
在我們的例子中,第二種方式是有效的。這個單獨的表名為pg_largeobject,它存儲分成“頁”的數(shù)據(jù),通常每個頁2kb,如文檔中所述。
因此,Hibernate將大型文本作為二進制數(shù)據(jù)存儲在單獨的表中。這是否意味著我們應該在選擇數(shù)據(jù)時進行額外的連接或在保存數(shù)據(jù)時進行額外的插入操作?讓我們啟用SQL日志記錄,創(chuàng)建Document實體并使用Spring Data JPA將其保存到數(shù)據(jù)庫中。
Java:
Document doc = new Document();
doc.setDateCreated(LocalDateTime.of(2020, 1, 1, 10, 10));
doc.setDocText("This is the doc text");
Document saved = documentRepository.save(doc);
Hibernate會在控制臺中顯示一個普通的SQL插入:
SQL:
insert
into
document
(date_created, doc_txt)
values
(?, ?)
現(xiàn)在,我們可以通過在控制臺中執(zhí)行以下SQL語句來檢查數(shù)據(jù)是否被正確存儲:
SQL:
select * from document
我們將看到上述命令的結果應該與下表類似:
|
id |
data_created |
doc_txt |
|
1 |
2020-01-01 10:10:00 |
76388 |
我們在這個表中看不到文檔文本,只是對大對象存儲中的對象的一個引用。讓我們檢查pg_largeobject表:
SQL
select * from pg_largeobject where loid=76338
此時,就可以看到文檔文本了。
|
loid |
pageno |
data |
|
76388 |
0 |
This is the doc text |
因此,Hibernate在幕后自動將數(shù)據(jù)保存到兩個表中?,F(xiàn)在,我們可以嘗試使用Spring data JPA獲取文檔數(shù)據(jù):
Java
documentRepository.findById(1L).ifPresent(d -> System.out.println(d.getDocText()));
我們可以在控制臺中看到以下SQL語句:
SQL
select
document0_.id as id1_0_0_,
document0_.date_created as date_cre2_0_0_,
document0_.doc_txt as doc_txt3_0_0_
from
document document0_
where
document0_.id=?
輸出應符合下面的預期:
Plain Text
This is the doc text
Hibernate從pg_largeobject表中選擇數(shù)據(jù)。讓我們嘗試使用JPQL來執(zhí)行相同的查詢。為此,我們創(chuàng)建了一個附加的Spring Data JPA存儲庫方法并調用它:
Java
//repository
@Query("select d from Document d where d.id = ?1")
OptionalfindByIdIs(Long id);
//...
//invocation
documentRepository.findByIdIs(1L).ifPresent(d -> System.out.println(d.getDocText()));
這種方式將無法完成我們的預期工作:
Plain text
org.springframework.orm.jpa.JpaSystemException: Unable to access lob stream
…
Caused by: org.hibernate.HibernateException: Unable to access lob stream
…
Caused by: org.postgresql.util.PSQLException: Large Objects may not be used in auto-commit mode.
Hibernate執(zhí)行額外的數(shù)據(jù)庫讀取來獲取LOB數(shù)據(jù)。在auto-commit模式下,此讀取將在單獨的事務中執(zhí)行。PostgreSQL驅動程序顯式地禁止它,如上面的錯誤消息所示。為了解決這個問題,我們需要在一個事務中執(zhí)行這樣的查詢,或者禁用auto-commit模式。
“CrudRepository”中的Spring Data JPA方法,如findById()和findAll(),默認情況下在一個事務中執(zhí)行。這就是為什么在第一個例子中一切正常的原因。當我們使用Spring Data JPA查詢方法或JPQL查詢時,我們必須像下面的示例那樣顯式地使用@Transactional。
Java
@Transactional
@Query("select d from Document d where d.id = ?1")
OptionalfindByIdIs(Long id);
@Transactional
ListfindByDateCreatedIsBefore(LocalDateTime dateCreated);
但是禁用auto-commit模式似乎比使用@Transactional注釋更可取。例如,為了在Spring Boot中實現(xiàn)默認的連接池(HikariCP),我們需要設置spring.datasource.hikari.auto-commit屬性為false。
將文本存儲在單獨的表中可能會導致其他問題。讓我們添加一個存儲庫方法,使用LIKE子句為docText字段選擇文檔:
Java
@Transactional
ListfindByDocTextLike(String text);
該方法將生成以下查詢:
SQL
select
document0_.id as id1_0_,
document0_.date_created as date_cre2_0_,
document0_.doc_txt as doc_txt3_0_
from
document document0_
where
document0_.doc_txt like ? escape ?
這個查詢將會失敗,錯誤如下:
Plain Text
org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a];
…
Caused by: org.postgresql.util.PSQLException: ERROR: function pg_catalog.like_escape(bigint, character varying) does not exist
Hint: No function matches the given name and argument types. You might need to add explicit type casts.
Hibernate無法生成正確的SQL來處理LOB文本列的LIKE子句。對于這種情況,我們可以使用nativequery。在這個查詢中,我們必須從LOB存儲中獲取文本數(shù)據(jù),并將其轉換為字符串格式。之后,我們可以在LIKE子句中使用它(不要忘記' @Transactional '):
Java
@Query(value = "select * from document d " +
"where convert_from(lo_get(doc_txt), 'UTF-8') like '%'||?1||'%'", nativeQuery = true)
@Transactional
ListfindByDocTextLike(String text);
現(xiàn)在一切都工作正常。請記住,nativequery可能與其他RDBMS不兼容,并且不會在運行時進行驗證。只有在絕對必要的時候才使用。
總結:采用LOB來存儲
在PostgreSQL中將大型文本存儲為LOB對象的優(yōu)點和缺點是什么呢?
優(yōu)點:
- PostgreSQL為LOB對象使用了優(yōu)化的存儲方式;
- 可以存儲多達4Gb的文本。
缺點:
- WHERE子句中的一些函數(shù)(LIKE、SUBSTRING等)在Hibernate中不能用于LOB文本列。對此,我們需要使用nativeQuery;
- 要使用JPQL或Spring Data JPA存儲庫查詢方法獲取文本,我們必須對存儲庫方法使用@Transactional注釋或禁用auto-commit模式。
這里有一個問題:為什么不直接將文本數(shù)據(jù)存儲在表中?我們也來討論一下這種方式。
方式2:Column Re-Definition
PostgreSQL允許我們將長文本數(shù)據(jù)存儲在特定數(shù)據(jù)類型(TEXT)的列中。我們可以在注釋中指定列定義。
Java
@Column(name = "doc_txt", columnDefinition = "text")
private String docText;
這使我們能夠以“通?!钡姆绞教幚黹L文本。沒有事務,native query和JPQL按照預期工作。與LOB類型相比,有一個限制是可存儲的最長字符串約為1GB。
當長度小于4GB時,可以使用LOB,但對于大多數(shù)用例來說已經(jīng)足夠長了。
這里唯一的問題是硬編碼的列定義(hardcoded column definition)。為了克服這個問題,我們可以在Hibernate 5中使用注釋@Type和轉換器org.hibernate.type.TextType。它比前面的列定義有一個優(yōu)點:它不是特定于供應商的(vendor-specific)。
Java
@Type(type = "org.hibernate.type.TextType")
@Column(name = "doc_txt")
private String docText;
在Hibernate 6中,org.hibernate.type.TextType類被刪除了。為了定義存儲長文本的列,我們可以這樣定義屬性:
Java
@Column(name = "doc_txt", length = Length.LOB_DEFAULT)
private String docText;
這將在數(shù)據(jù)庫中給出以下列定義:doc_txt varchar(1048576)。它不是TEXT類型,但仍然可以在表中存儲大約1Gb的文本。它是PostgreSQL中最大的字符串。
在Hibernate 6中,我們可以按照下面的方式定義docText屬性來生成一個包含TEXT數(shù)據(jù)類型的列:
Java
@JdbcTypeCode(SqlTypes.LONG32VARCHAR)
@Column(name = "doc_txt")
private String docText;
不幸的是,自2022年6月開始,Hibernate 6無法從表中獲取數(shù)據(jù)。它生成TEXT類型的正確表和列定義。將數(shù)據(jù)從doc_txt列提取到實體屬性將會失敗。錯誤文本如下所示:
Plain Text
Unknown wrap conversion requested: [B to java.lang.String : `org.hibernate.type.descriptor.java.StringJavaType` (java.lang.String)
因此,在text /VARCHAR列中存儲長文本帶來的問題較少。事務、LIKE條件等沒有問題。唯一的缺點就是存儲大小最多是1Gb。還有其他注意事項嗎?
如果我們在數(shù)據(jù)庫中使用TEXT列類型和@Lob注釋,可能會出現(xiàn)問題。讓我們看看它是如何工作的。首先,讓我們創(chuàng)建一個表文檔,并向其中插入一些數(shù)據(jù):
SQL
create table document (
id int8 generated by default as identity,
date_created timestamp not null,
doc_txt text,
primary key (id)
);
insert into document (id, date_created, doc_txt) values (1, '2021-10-10', 'This is the document text number 1');
我們將使用帶有@Lob列的文檔實體定義:
Java
@Entity
@Table(name = "document")
public class Document {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "date_created", nullable = false)
private LocalDateTime dateCreated;
@Lob
@Column(name = "doc_txt")
private String docText;
//Getters and setters omitted for brevity
}
文檔獲取的代碼將是相同的:
Java
documentRepository.findById(1L).ifPresent(d -> System.out.println(d.getDocText()));
如果我們嘗試執(zhí)行這個repository方法,將會看到下面的結果:
Plain Text
java.lang.IllegalStateException: Failed to execute Application
…
Caused by: org.hibernate.exception.DataException: could not execute query
…
Caused by: org.postgresql.util.PSQLException: Bad value for type long: This is the document text number 1
…
我們可以看到,Hibernate處理@Lob屬性值作為對LOB對象數(shù)據(jù)的引用。數(shù)據(jù)庫表中的TEXT列類型不影響此行為。
那保存數(shù)據(jù)呢?讓我們先清空表,嘗試保存帶有@Lob字段的文檔實體,并使用Spring Data JPA獲取它。下面是對應的代碼:
Java
//Saving
Document doc = new Document();
doc.setDateCreated(LocalDateTime.now());
doc.setDocText("This is another text document");
documentRepository.save(doc);
...
//Fetching
documentRepository.findAll().forEach(d -> System.out.println(d.getDocText));
...
//Result
This is another text document
因此,看起來帶有@Lob屬性的實體似乎可以處理TEXT列。在數(shù)據(jù)庫表中,我們會看到熟悉的畫面:
|
id |
data_created |
doc_txt |
|
1 |
2022-06-16 15:28:26.751041 |
76388 |
|
loid |
pageno |
data |
|
76388 |
0 |
This is another text document |
如果我們使用SQL將文檔數(shù)據(jù)插入到表中,然后選擇數(shù)據(jù),我們將得到以下結果:
SQL
insert into document (date_created, doc_txt) values ('2021-10-10', 'This is the document text');
select * from document;
|
id |
data_created |
doc_txt |
|
1 |
2022-06-16 15:28:26.751041 |
76388 |
|
2 |
2021-10-10 00:00:00 |
This is the document text |
現(xiàn)在我們不能使用Spring data JPA從數(shù)據(jù)庫中選擇數(shù)據(jù)。當選擇第二行時,應用程序將因類型轉換錯誤而崩潰。
讓我們將@Type注釋添加到屬性中…
Java
@Lob
@Type(type = "org.hibernate.type.TextType")
@Column(name = "doc_txt")
private String docText;
并嘗試將文檔的文本數(shù)據(jù)打印到應用程序控制臺。
Java
documentRepository.findAll().forEach(d -> System.out.println(d.getDocText));
我們將會看到下面的輸出信息:
Hibernate: select document0_.id as id1_0_, document0_.date_created as date_cre2_0_, document0_.text as text3_0_ from document document0_
76388
This is the document text
使用@Type注釋,我們可以選擇數(shù)據(jù),但是OID引用被轉換為文本,因此我們“丟失”了存儲在LOB存儲中的文本。
總結:在表中存儲長文本
那么,在數(shù)據(jù)庫中將長文本存儲為文本列的優(yōu)點和缺點是什么呢?
優(yōu)點:
- 查詢將會按照預期工作;不需要單獨的事務或nativequery。
缺點:
- 存儲大小限制為1Gb
- 混合使用@Lob屬性定義和TEXT列數(shù)據(jù)類型可能會導致意外結果。
總結:如何在PostgreSQL中存儲長文本
1.在大多數(shù)情況下,將長文本數(shù)據(jù)與其他實體數(shù)據(jù)一起存儲在同一個表中應該沒問題。它將允許您使用Hibernate和SQL來操作數(shù)據(jù)。
- 在Hibernate 5中,使用@Type(type = "org.hibernate.type.TextType")注釋JPA實體屬性。
- 如果你使用Hibernate 6,使用@Column(name =…, length = length . lob_default)注釋用于列定義。
- 注意,使用這種方法時,存儲的文本不能超過1Gb。
2.如果您計劃存儲大量的字符數(shù)據(jù)(超過1Gb),那么對JPA實體屬性使用@Lob注釋。Hibernate
將使用PostgreSQL來對大數(shù)據(jù)存儲進行優(yōu)化。在使用lob時,有幾件事需要考慮。
- 我們必須在一個事務中執(zhí)行JPQL查詢和Spring Data JPA查詢方法,或者顯式禁用auto-commit模式。
- 要在WHERE條件中使用LOB列,我們可能需要使用nativequery。
3.Hibernate文檔中有一個很好的建議:請不要僅僅因為你想要一個TEXT列就使用JPA的@Lob注釋。@Lob注釋的目的不是控制DDL的生成!因此,不要將@Lob實體屬性定義與TEXT列數(shù)據(jù)類型一起使用。
希望這些簡單的實用方法可以幫助你使用Hibernate在PostgreSQL中存儲文本數(shù)據(jù)時,避免一些不必要的問題。
譯者介紹
趙青窕,社區(qū)編輯,從事驅動開發(fā)工作。
原文標題:??How to Store Text in PostgreSQL: Tips, Tricks, and Traps???,作者:Andrey Belyaev?
網(wǎng)頁標題:如何在PostgreSQL中存儲文本
分享地址:http://m.fisionsoft.com.cn/article/cogdspd.html


咨詢
建站咨詢
