Apache Derby Bài 38: Xử lý dữ liệu lớn: Lưu trữ hình ảnh và tệp tin với kiểu BLOB và CLOB
1. Giới hạn của kiểu dữ liệu thông thường và Sự ra đời của LOB
Trong các thiết kế cơ sở dữ liệu trước đây, chúng ta thường sử dụng VARCHAR để lưu trữ chuỗi văn bản. Tuy nhiên, trong Apache Derby, kiểu VARCHAR có kích thước tối đa chỉ là 32,672 byte (khoảng 32KB).
Nếu ứng dụng của bạn cần lưu trữ một bài báo dài 100 trang, một tài liệu XML/JSON khổng lồ, hoặc lưu trữ dữ liệu nhị phân như hình ảnh (JPG, PNG), tệp PDF, hay âm thanh, các kiểu dữ liệu truyền thống sẽ lập tức báo lỗi tràn bộ nhớ.
Để giải quyết bài toán này, SQL chuẩn giới thiệu khái niệm LOB (Large Object), và Apache Derby hỗ trợ hai biến thể chính:
-
CLOB (Character Large Object): Dùng để lưu trữ văn bản siêu dài (chỉ chứa các ký tự chữ).
-
BLOB (Binary Large Object): Dùng để lưu trữ dữ liệu nhị phân thô (ảnh, video, file nén, tệp PDF).
Trong Derby, một cột LOB có thể lưu trữ tối đa lên tới 2 Gigabyte (2GB) dữ liệu, mở ra khả năng biến cơ sở dữ liệu thành một kho lưu trữ tệp tin thực thụ.
2. Khai báo cột BLOB và CLOB trong SQL
Khi khởi tạo bảng, bạn nên chỉ định rõ kích thước giới hạn tối đa mà cột LOB đó được phép lưu trữ (để tối ưu hóa không gian cấp phát đĩa của Engine). Bạn có thể dùng hậu tố K (Kilobytes), M (Megabytes), hoặc G (Gigabytes).
Cú pháp:
SQL
CREATE TABLE ho_so_nhan_vien (
ma_nv VARCHAR(20) PRIMARY KEY,
ho_ten VARCHAR(100),
tieu_su CLOB(100K), -- Lưu tối đa 100 Kilobytes văn bản chữ
anh_dai_dien BLOB(5M), -- Lưu tối đa 5 Megabytes dữ liệu nhị phân
tai_lieu_pdf BLOB(50M) -- Lưu file tài liệu đính kèm lên tới 50MB
);
(Nếu bạn không ghi rõ kích thước, ví dụ chỉ ghi BLOB, Derby sẽ tự động gán kích thước tối đa là 2GB - 2147483647 bytes).
3. Ghi dữ liệu lớn bằng Java JDBC (Thao tác WRITE)
Lỗi phổ biến nhất của các lập trình viên khi làm việc với ảnh là đọc toàn bộ file ảnh thành mảng byte[] rồi đẩy vào lệnh PreparedStatement.setBytes(). Nếu file ảnh nặng 1GB, hệ thống của bạn sẽ lập tức bị OutOfMemoryError do hết RAM.
Cách tiếp cận chuẩn mực và chuyên nghiệp là sử dụng Cơ chế Luồng (Streaming). JDBC sẽ đọc từng đoạn nhỏ của file trên đĩa cứng và đẩy trực tiếp vào database mà không cần nạp toàn bộ vào RAM.
Mã nguồn minh họa chèn file ảnh (BLOB) và văn bản dài (CLOB):
Java
package com.hieu.derby.lob;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class DerbyLOBInsertDemo {
public static void main(String[] args) {
String url = "jdbc:derby://localhost:1527/my_company_db";
String sql = "INSERT INTO ho_so_nhan_vien (ma_nv, ho_ten, tieu_su, anh_dai_dien) VALUES (?, ?, ?, ?)";
// Giả sử có 2 tệp trên ổ cứng
File txtFile = new File("C:/data/tieu_su_dai.txt");
File imgFile = new File("C:/data/avatar.jpg");
try (Connection conn = DriverManager.getConnection(url);
PreparedStatement pstmt = conn.prepareStatement(sql);
// Mở luồng đọc văn bản (Cho CLOB)
FileReader clobReader = new FileReader(txtFile);
// Mở luồng đọc nhị phân (Cho BLOB)
FileInputStream blobStream = new FileInputStream(imgFile)) {
pstmt.setString(1, "NV01");
pstmt.setString(2, "Nguyễn Minh Hiếu");
// 1. Chèn CLOB bằng setCharacterStream
pstmt.setCharacterStream(3, clobReader, (int) txtFile.length());
// 2. Chèn BLOB bằng setBinaryStream
pstmt.setBinaryStream(4, blobStream, (int) imgFile.length());
int rows = pstmt.executeUpdate();
System.out.println("=> Chèn dữ liệu LOB thành công! Số dòng: " + rows);
} catch (Exception e) {
e.printStackTrace();
}
}
}
(Mẹo: Phương thức setBinaryStream kết nối trực tiếp FileInputStream tới CSDL, cho phép chèn file 1GB mà RAM chỉ tốn vài MB).
4. Đọc dữ liệu lớn bằng Java JDBC (Thao tác READ)
Tương tự như khi chèn, khi truy xuất dữ liệu từ CSDL ra ngoài, bạn cũng nên dùng luồng (Stream) để ghi ngược dữ liệu tạo thành các tệp tin lưu xuống ổ cứng.
Mã nguồn minh họa xuất BLOB ra file gốc:
Java
package com.hieu.derby.lob;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class DerbyLOBReadDemo {
public static void main(String[] args) {
String url = "jdbc:derby://localhost:1527/my_company_db";
String sql = "SELECT anh_dai_dien FROM ho_so_nhan_vien WHERE ma_nv = ?";
try (Connection conn = DriverManager.getConnection(url);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "NV01");
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
// 1. Mở luồng đọc nhị phân từ database
InputStream blobStream = rs.getBinaryStream("anh_dai_dien");
if (blobStream != null) {
// 2. Mở luồng ghi ra tệp tin trên ổ cứng
FileOutputStream fos = new FileOutputStream("C:/data/avatar_xuat_ra.jpg");
byte[] buffer = new byte[4096]; // Đọc từng khối 4KB
int bytesRead = -1;
// 3. Vòng lặp chuyển dữ liệu (từ Stream CSDL sang Stream Ổ cứng)
while ((bytesRead = blobStream.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fos.close();
blobStream.close();
System.out.println("=> Xuất file ảnh từ BLOB thành công!");
} else {
System.out.println("Cột anh_dai_dien bị NULL.");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
5. Những lưu ý kiến trúc khi sử dụng LOB trong Derby
Việc lưu trữ trực tiếp file (ảnh, PDF) vào CSDL mang lại lợi thế là bạn có thể Backup/Restore toàn bộ hệ thống (cả dữ liệu chữ và file) trong một thao tác duy nhất (như Bài 35), đồng thời đảm bảo được tính toàn vẹn Transaction (Tính ACID).
Tuy nhiên, bạn cần nắm rõ cơ chế lưu trữ của Derby:
-
Lưu trữ ngoài luồng (Out-of-line storage): Mặc định, Derby không lưu trực tiếp dữ liệu BLOB/CLOB khổng lồ vào cùng một trang nhớ (page) với các cột dữ liệu thường (như
INT,VARCHAR). Nó chỉ lưu một con trỏ (pointer) trên dòng đó, và phần dữ liệu lớn được đẩy ra một tệp cấu trúc ẩn riêng biệt. Nhờ đó, việc Table Scan quét qua các dòng dữ liệu thông thường không bị chậm đi do sự có mặt của các cột LOB khổng lồ. -
Không thể đánh Index: Bạn không thể và không được phép áp dụng ràng buộc
PRIMARY KEY,UNIQUEhay tạo câyB-tree Indextrên cột BLOB/CLOB. -
Bài toán kích thước: Khi CSDL phình to tới hàng trăm Gigabytes do chứa nhiều file, thời gian khởi động, nạp cache và sao lưu (Backup) của Derby sẽ kéo dài đáng kể. Đối với các hệ thống phân tán siêu lớn, người ta thường khuyên lưu file nhị phân trên Hệ thống tệp (File System) hoặc Cloud Storage (như Amazon S3) và chỉ lưu chuỗi đường dẫn (URL) vào cơ sở dữ liệu.
Kết thúc bài học này, kho vũ khí cơ sở dữ liệu của chúng ta đã gần như hoàn thiện: từ chuỗi văn bản, con số, đến cả những file ảnh lớn nhất. Ở bài tiếp theo, chúng ta sẽ làm việc với một kiểu dữ liệu thường xuyên gây rắc rối nhất về mặt múi giờ cho các lập trình viên: Apache Derby Bài 39: Xử lý dữ liệu thời gian (DATE, TIME, TIMESTAMP) và bài toán Timezone.
All rights reserved