Hibernate BLOB and CLOB Mapping With @Lob (Hibernate 7)

Map BLOB and CLOB columns in Hibernate 7 with @Lob on byte[], String, Blob and Clob fields. See the generated column types on H2, MySQL and PostgreSQL, how to stream large files with Blob, and why @Basic(fetch = LAZY) needs bytecode enhancement.

A LOB (large object) is a database column type for values too big for a normal text or binary column, where a BLOB holds binary data such as a PDF resume, and a CLOB holds long text such as a cover letter. In Hibernate, we map such a field with the Jakarta Persistence @Lob annotation on a byte[], String, java.sql.Blob or java.sql.Clob field. Hibernate creates a LOB column for it and uses the JDBC LOB methods to write and read the value.

We use BLOB and CLOB columns when an app keeps files or long documents in the database, such as uploaded resumes or the full text of a contract.

The following example maps a resume as a BLOB and a cover letter as a CLOB, and saves and reads both fields.

@Lob
private byte[] resume;          // H2: blob, MySQL: longblob, PostgreSQL: oid

@Lob
private String coverLetter;     // H2: clob, MySQL: longtext, PostgreSQL: oid
byte[] resume = Files.readAllBytes(Path.of("files/lokesh-resume.pdf"));   // resume.length = 778

JobApplication app = new JobApplication("Lokesh");
app.setResume(resume);
app.setCoverLetter("Dear hiring team, I would like to apply for the Java developer role.");
em.persist(app);      // at commit: insert into JobApplication (candidateName,coverLetter,resume,id) values (?,?,?,?)

JobApplication loaded = em.find(JobApplication.class, id);
// select ja1_0.id,ja1_0.candidateName,ja1_0.coverLetter,ja1_0.resume from JobApplication ja1_0 where ja1_0.id=?
byte[] stored = loaded.getResume();   // the same 778 bytes

Notice that the resume and the cover letter need only @Lob, and Hibernate loads both values with the rest of the row.

Next, we compare byte[] and String fields with Blob and Clob locators, and we stream a 20 MB file without loading it into the heap. The FAQs cover column size limits and the oid type on PostgreSQL.

1. What Are BLOB and CLOB Columns?

Most columns hold short values, such as a varchar(255) name or a bigint id, whereas files and long documents need column types that can grow to megabytes or gigabytes. Databases offer three LOB types, and each one stores a different kind of data.

  • The type BLOB (binary large object) stores bytes, such as PDF files, images and ZIP archives.
  • The type CLOB (character large object) stores text in the database character set, such as cover letters, HTML and extracted document text.
  • The type NCLOB stores text in the national (Unicode) character set.

In an entity, Hibernate supports two kinds of Java types for LOB columns. A materialized type (byte[], String, char[]) holds the whole value in memory. A locator type (java.sql.Blob, java.sql.Clob) is a pointer to the value in the database, and we read it as a stream.

byte[] / StringBlob / Clob
After em.find()The whole value is in the heapOnly a locator is in the heap
Reading the valueUse the fieldgetBinaryStream(), getCharacterStream(), getSubString()
Creating a valueAssign the array or stringHibernate.getLobHelper().createBlob(…)
After the transaction endsAlways readableNot portable, depends on the driver
Good forFiles up to a few MBLarge files that we stream

Hibernate reads a byte[] or String LOB completely into memory when it loads the entity. For a managed entity, Hibernate also keeps a second copy of the array in the persistence context to detect changes at flush (dirty checking). For a 20 MB file, the heap held after a garbage collection differs a lot between the two kinds.

Two panels comparing a 20 MB file loaded as @Lob byte[] and as @Lob Blob: em.find() with byte[] reads the whole value into the entity field and a loaded-state copy, holding about 42 MB of heap, or about 21 MB in read-only mode; em.find() with Blob returns a locator, and getBinaryStream() copies the bytes to a file chunk by chunk, holding about 0 MB
A 20 MB byte[] costs about 40 MB of heap in a managed entity. A Blob costs almost nothing until we stream it.

2. BLOB and CLOB Example

The following example is a job portal built with Hibernate 7.4, Java 25 and an in-memory H2 database. Candidates upload a resume and write a cover letter, and some attach a large portfolio file.

2.1. Job Application Model

The portal has four @Entity classes, and each one shows a different way to map large values.

  • The JobApplication entity stores the resume as byte[] and the cover letter as String.
  • The ApplicationFile entity stores a large attachment as Blob and the text extracted from it as Clob.
  • The JobPosting entity stores a long description and a company logo without @Lob (section 2.5).
  • The ApplicationDraft entity has the same fields as JobApplication but no annotations (section 3.1).

The annotation @Lob picks the column from the field type, so String and Clob fields become a CLOB, whereas byte[] and Blob fields become a BLOB.

@Lob
@Basic(fetch = FetchType.LAZY)   // ignored without bytecode enhancement (section 2.4)
private byte[] resume;

@Lob
private String coverLetter;
private String fileName;
private long size;

@Lob
@Basic(fetch = FetchType.LAZY)
private Blob content;

@Lob
@Basic(fetch = FetchType.LAZY)
private Clob extractedText;

On H2, Hibernate 7.4 creates a blob column for each binary field and a clob column for each text field.

create table JobApplication (id bigint not null, candidateName varchar(255), coverLetter clob, resume blob, primary key (id))
create table ApplicationFile (id bigint not null, size bigint not null, fileName varchar(255), content blob, extractedText clob, primary key (id))

2.2. Saving and Reading a byte[] Resume

A byte[] field needs no special API. We read the file into a byte array, set it on the entity and call em.persist().

byte[] resume = Files.readAllBytes(Path.of("files/lokesh-resume.pdf"));   // 778 bytes

Long appId = emf.callInTransaction(em -> {
  JobApplication app = new JobApplication("Lokesh");
  app.setResume(resume);
  app.setCoverLetter("Dear hiring team, I would like to apply for the Java developer role.");
  em.persist(app);
  return app.getId();
});
// select next value for JobApplication_SEQ
// insert into JobApplication (candidateName,coverLetter,resume,id) values (?,?,?,?)

The call em.find() loads both LOB columns with the rest of the row. When a recruiter clicks “Download resume”, we write the array back to a file.

JobApplication app = em.find(JobApplication.class, appId);
// select ja1_0.id,ja1_0.candidateName,ja1_0.coverLetter,ja1_0.resume from JobApplication ja1_0 where ja1_0.id=?

boolean same = Arrays.equals(resume, app.getResume());      // true
String letter = app.getCoverLetter();                       // "Dear hiring team, I would like to apply ..."
Files.write(Path.of("target/downloaded-resume.pdf"), app.getResume());

2.3. Streaming a Large File With Blob

A portfolio of 20 MB (20,971,520 bytes) should not pass through a byte[]. We create the Blob from an InputStream with Hibernate.getLobHelper().createBlob(stream, length), and the JDBC driver reads the stream while it runs the INSERT.

Long fileId = emf.callInTransaction(em -> {
  try (InputStream in = Files.newInputStream(portfolio)) {
    ApplicationFile file = new ApplicationFile("portfolio.zip", size);
    file.setContent(Hibernate.getLobHelper().createBlob(in, size));
    file.setExtractedText(Hibernate.getLobHelper().createClob("Lokesh, Java Developer, Hibernate, Spring Boot"));
    em.persist(file);
    em.flush();                 // insert while the stream is open
    return file.getId();
  }
});
// insert into ApplicationFile (content,extractedText,fileName,size,id) values (?,?,?,?,?)

Keep the stream open until Hibernate runs the INSERT. Hibernate sends the INSERT at flush, which by default happens at commit, so we call em.flush() inside the try block to run it before the stream closes. Without the flush, we get the error from section 3.3.

To read the file back, we copy the Blob stream to disk. The bytes move in small chunks and never sit in the heap as one array.

ApplicationFile file = em.find(ApplicationFile.class, fileId);

try (InputStream in = file.getContent().getBinaryStream()) {
  Files.copy(in, Path.of("target/portfolio-copy.zip"), StandardCopyOption.REPLACE_EXISTING);   // 20971520
}

Clob text = file.getExtractedText();
long length = text.length();               // 46
String name = text.getSubString(1, 6);     // "Lokesh" (positions start at 1)

For long text, the method getCharacterStream() returns a Reader that we can stream the same way.

2.4. Lazy Loading LOB Columns

A job list page shows candidate names, not resumes, so we want lazy loading for the LOB columns. The annotation @Basic(fetch = FetchType.LAZY) asks for it, but the Jakarta Persistence specification treats LAZY only as a hint. Without bytecode enhancement, Hibernate ignores LAZY on a basic field and loads the LOB with the entity.

JobApplication app = em.find(JobApplication.class, appId);
// select ja1_0.id,ja1_0.candidateName,ja1_0.coverLetter,ja1_0.resume from JobApplication ja1_0 where ja1_0.id=?

boolean loaded = Persistence.getPersistenceUtil().isLoaded(app, "resume");   // true

Bytecode enhancement means Hibernate changes the compiled entity class so that each field read can trigger a load. The hibernate-maven-plugin enhances the classes after compilation.

<plugin>
  <groupId>org.hibernate.orm</groupId>
  <artifactId>hibernate-maven-plugin</artifactId>
  <version>7.4.11.Final</version>
  <executions>
    <execution>
      <goals>
        <goal>enhance</goal>
      </goals>
      <configuration>
        <enableLazyInitialization>true</enableLazyInitialization>
      </configuration>
    </execution>
  </executions>
</plugin>

The example project adds a fileSet that enhances only ApplicationFile, so JobApplication keeps the default behavior for comparison. With enhancement, em.find() skips the LOB columns, and the first getter call loads them.

ApplicationFile file = em.find(ApplicationFile.class, fileId);
// select af1_0.id,af1_0.fileName,af1_0.size from ApplicationFile af1_0 where af1_0.id=?

boolean contentLoaded = Persistence.getPersistenceUtil().isLoaded(file, "content");      // false
file.getContent();
// select af1_0.content,af1_0.extractedText from ApplicationFile af1_0 where af1_0.id=?
boolean textLoaded = Persistence.getPersistenceUtil().isLoaded(file, "extractedText");   // true

Both lazy fields belong to the same default lazy group, so one getter loads both columns.

Two panels: without bytecode enhancement, em.find(JobApplication.class, id) selects id, candidateName, coverLetter and resume, and app.getResume() runs no SQL; with enhancement, em.find(ApplicationFile.class, id) selects only id, fileName and size, and file.getContent() runs a second select for content and extractedText
LAZY on a LOB works only on an enhanced entity. There, the LOBs load on first access.

2.5. Large Columns Without @Lob

The annotation @Lob is not the only way to get a large column. Hibernate picks the column type from the JDBC type and the length of the field, so we can also choose a large type with Hibernate’s @JdbcTypeCode or with a large @Column(length = …).

@JdbcTypeCode(SqlTypes.LONG32VARCHAR)
private String description;        // 2,300 characters saved and read back

@Column(length = 5_000_000)
private byte[] companyLogo;
private byte[] resume;             // varbinary(255)
private String coverLetter;        // varchar(255)

The same mappings produce different columns on each database. The Hibernate dialects for MySQL and PostgreSQL generate the DDL without a database connection, so we can compare the column types side by side.

Table of Java fields and the column types Hibernate 7.4 generates on H2 2.5, MySQL 8 and PostgreSQL 17: @Lob byte[] and @Lob Blob become blob, longblob and oid; @Lob String and @Lob Clob become clob, longtext and oid; byte[] without @Lob becomes varbinary(255), varbinary(255) and bytea; String without @Lob becomes varchar(255) on all three; @JdbcTypeCode(LONG32VARCHAR) String becomes clob, longtext and text; @Column(length = 5_000_000) byte[] becomes blob, mediumblob and bytea
On MySQL, the length decides between mediumblob and longblob. On PostgreSQL, @Lob means oid, while the other mappings give bytea and text.

The complete project on GitHub bootstraps Hibernate with HibernatePersistenceConfiguration and prints the DDL and SQL for every step, with 20 JUnit tests that check the results (mvn -q process-classes exec:java, mvn test).

3. Hibernate BLOB and CLOB FAQs

3.1. Why Is My byte[] Column Limited to 255 Bytes?

A byte[] without @Lob or a length becomes varbinary(255), and a String becomes varchar(255). Saving the 778-byte resume into ApplicationDraft fails at commit.

jakarta.persistence.RollbackException: Error while committing the transaction [could not execute statement [Value too long for column "RESUME BINARY VARYING(255)": "X'255044462d312e370a312030206f626a0a3c3c202f54797065202f436174616c6f67202f506167... (778)"; SQL statement:
insert into ApplicationDraft (coverLetter,resume,id) values (?,?,?) [22001-252]] ...

A 260-character cover letter fails the same way on “COVERLETTER CHARACTER VARYING(255)”. The fix is @Lob, a large @Column(length = …) or @JdbcTypeCode on the field. On an existing database, we also have to change the column type with a migration script.

3.2. How Large Can a BLOB or CLOB Be?

The limit comes from the database, not from Hibernate. For example, a MySQL blob column holds a 50 KB scanned ID card, but a 2 MB resume needs mediumblob or longblob.

DatabaseColumn typeMaximum size
MySQLblob / text65,535 bytes
MySQLmediumblob / mediumtext16,777,215 bytes (16 MB)
MySQLlongblob / longtext4,294,967,295 bytes (4 GB)
PostgreSQLbytea / text1 GB
PostgreSQLoid (large object)4 TB
H2varbinary / varchar1,000,000,000 bytes or characters

MySQL also rejects any statement larger than its max_allowed_packet setting, so a big file can fail below the column limit.

3.3. Why Does Saving a Blob From an InputStream Fail at Commit?

Because the stream is already closed when Hibernate sends the INSERT. Without em.flush(), the try block closes the file when persist() returns, but the driver reads the stream later, at commit.

jakarta.persistence.RollbackException: Error while committing the transaction [Unable to bind parameter #1 - org.hibernate.engine.jdbc.proxy.BlobProxy@7e021b12 [IO Exception: "java.nio.channels.ClosedChannelException" [90028-252]] [n/a]]

We either call em.flush() inside the try block, as in section 2.3, or keep the stream open until the transaction commits.

3.4. Can I Read a Blob After the Transaction Ends?

Only on some drivers, so we should not rely on it. On H2, a Blob returned from callInTransaction() still gives all 20,971,520 bytes, but using a LOB locator after its transaction ends is not portable, and other drivers can free the locator at commit. So we read or copy the stream inside the transaction. On an enhanced entity, we must also load the lazy content field before the transaction ends, otherwise its getter throws a LazyInitializationException.

3.5. Why Does @Lob Map to oid on PostgreSQL?

Hibernate’s PostgreSQL dialect maps @Lob to oid, a reference to a PostgreSQL large object that is stored outside the table. Many teams prefer bytea and text, which PostgreSQL stores with the row. To get them, we leave out @Lob.

@Column(length = 5_000_000)
private byte[] companyLogo;        // bytea

@JdbcTypeCode(SqlTypes.LONG32VARCHAR)
private String description;        // text

3.6. How Do I Reduce the Memory of a byte[] LOB?

When we only read the file, we load the entity in read-only mode. Hibernate then keeps no loaded-state copy, and the heap held by the 20 MB resume drops from about 42 MB to about 21 MB.

JobApplication app = em.find(JobApplication.class, appId, ReadOnlyMode.READ_ONLY);

For files that are often large, a Blob field with streaming (section 2.3) keeps the heap close to zero.

3.7. What Replaced Session.getLobHelper() in Hibernate 7?

The method Session.getLobHelper() is deprecated in Hibernate 7. The static Hibernate.getLobHelper() returns the same LobHelper without a session.

Blob blob = Hibernate.getLobHelper().createBlob(in, size);   // from a stream
Clob clob = Hibernate.getLobHelper().createClob("Lokesh, Java Developer, Hibernate, Spring Boot");

Hibernate 5 code also saved the entity with session.save(), which is removed in Hibernate 7, and em.persist() replaces it.

4. Conclusion

The annotation @Lob maps a byte[] or String field to a BLOB or CLOB column, and that is enough for resumes and cover letters of a few megabytes. For large files, a java.sql.Blob field created from a stream keeps the bytes out of the heap, as long as we read and write the stream inside the transaction. The annotation @Basic(fetch = LAZY) on a LOB needs bytecode enhancement, and on PostgreSQL we decide between oid with @Lob and bytea/text without it.

5. References

Happy Learning !!

Source Code on Github

Leave a Comment

    • BLOB fields should be used for storing small files e.g. resumes (not necessarily). Storing 2 GB data in one row is not a good idea to me. Use file system instead and store file name and path in database.
      Even after this somebody needs to store 2 GB in database, then file must be spilted into multiple parts and store into multiple rows. On retrieval retrieve them all, and join them back.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.