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[] / String | Blob / Clob | |
|---|---|---|
| After em.find() | The whole value is in the heap | Only a locator is in the heap |
| Reading the value | Use the field | getBinaryStream(), getCharacterStream(), getSubString() |
| Creating a value | Assign the array or string | Hibernate.getLobHelper().createBlob(…) |
| After the transaction ends | Always readable | Not portable, depends on the driver |
| Good for | Files up to a few MB | Large 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](https://howtodoinjava.com/wp-content/uploads/2026/10/hibernate-example-insertselect-two-panels-comparing-20-mb.png)
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.

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](https://howtodoinjava.com/wp-content/uploads/2026/10/hibernate-example-insertselect-table-java-fields-column-types.png)
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.
| Database | Column type | Maximum size |
|---|---|---|
| MySQL | blob / text | 65,535 bytes |
| MySQL | mediumblob / mediumtext | 16,777,215 bytes (16 MB) |
| MySQL | longblob / longtext | 4,294,967,295 bytes (4 GB) |
| PostgreSQL | bytea / text | 1 GB |
| PostgreSQL | oid (large object) | 4 TB |
| H2 | varbinary / varchar | 1,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
- Lob JavaDoc (Jakarta Persistence 3.2)
- Basic JavaDoc (Jakarta Persistence 3.2)
- Hibernate ORM 7.4 User Guide: Handling LOB data
- Hibernate ORM 7.4 User Guide: Lazy attribute loading
- Hibernate ORM 7.4 User Guide: Maven enhancement plugin
- LobHelper JavaDoc (Hibernate 7.4)
- java.sql.Blob JavaDoc (Java 25)
- MySQL 8.4 Reference Manual: Data Type Storage Requirements
- PostgreSQL 17: Large Objects Introduction
- H2 Data Types
Happy Learning !!
What if..
The file is bigger than the max Memory
I mean, -Xmx512m and my file is 2Gig?
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.