浅析JDBC与数据库如何进行数据交互及类型转换
JDBC 作为 Java 应用程序和数据库之间的桥梁,需要解决两个核心问题:
- 网络通信协议:如何在网络中传输 SQL 命令和结果数据
- 数据类型映射:如何转换 Java 类型和数据库类型
一、数据交互流程
1. 整体交互架构
Java 应用程序
↓ [JDBC API 调用]
JDBC Driver 实现
↓ [协议转换]
网络协议包 (TCP/IP)
↓ [网络传输]
数据库服务器
↓ [协议解析]
数据库引擎 (解析、优化、执行)
↓ [结果集序列化]
网络协议包
↓ [网络传输]
JDBC Driver (反序列化)
↓ [ResultSet 封装]
Java 应用程序
2. 底层通信协议示例
以 MySQL 为例,通信基于 MySQL 自有协议:
简化的协议交互过程
1. 握手包 (服务端 → 客户端):协议版本、线程ID、挑战随机数、服务器能力标志
2. 认证包 (客户端 → 服务端):用户名、密码(加密后的挑战响应)、数据库名
3. 命令包 (客户端 → 服务端):COM_QUERY (0x03) + "SELECT * FROM user"
4. 结果集包 (服务端 → 客户端)
- 列数量包
- 列定义包 (每个列的名称、类型)
- 行数据包 (每行数据的二进制/文本格式)
- EOF 包 (结束标志)
3. 请求-响应序列
// 实际网络抓包的数据示例 (简化) // 客户端发送: [0x03] [S E L E C T * F R O M u s e r] // 服务端响应: // 第一个包: 列数 [0x1c 0x00 0x00 0x00] // 28字节长度的包 [0x03] // 结果集标志 [0x02] // 2列 // 第二个包: 列定义 "id" [0x18 0x00 0x00 0x00] // 包长度 [0x04] // 列定义标志 [0x03] [i d] // 列名 // 第三个包: 列定义 "name" [0x1a 0x00 0x00 0x00] [0x04] [0x05] [n a m e] // 第四包: 行数据 [0x10 0x00 0x00 0x00] [0x00] // 行标志 [0x01] [0x31] // id=1 [0x05] [T o m] // name='Tom' // 第五包: EOF [0x05 0x00 0x00 0x00] [0xfe] // EOF 标志
二、数据类型转换机制
1. 转换核心:JDBC 类型系统
JDBC 定义了中间类型 java.sql.Types,作为 Java 和数据库的桥梁:
Java 类型 ←→ JDBC Types ←→ 数据库类型
2. 常见类型映射表
| JDBC Types | Java 类型 | MySQL | Oracle | PostgreSQL |
|---|---|---|---|---|
BIT | boolean, Boolean | BIT, BOOLEAN | - | BOOL |
TINYINT | byte, Byte | TINYINT | - | - |
SMALLINT | short, Short | SMALLINT | - | SMALLINT |
INTEGER | int, Integer | INT, INTEGER | NUMBER(10) | INTEGER |
BIGINT | long, Long | BIGINT | NUMBER(19) | BIGINT |
FLOAT | double, Double | FLOAT | FLOAT(16) | FLOAT8 |
DOUBLE | double, Double | DOUBLE | FLOAT(32) | FLOAT8 |
DECIMAL | java.math.BigDecimal | DECIMAL, NUMERIC | NUMBER | NUMERIC |
CHAR | String | CHAR | CHAR | CHAR |
VARCHAR | String | VARCHAR | VARCHAR2 | VARCHAR |
LONGVARCHAR | String | TEXT, MEDIUMTEXT | LONG | TEXT |
DATE | java.sql.Date | DATE | DATE | DATE |
TIME | java.sql.Time | TIME | - | TIME |
TIMESTAMP | java.sql.Timestamp | DATETIME, TIMESTAMP | TIMESTAMP | TIMESTAMP |
BINARY | byte[] | BINARY | RAW | BYTEA |
VARBINARY | byte[] | VARBINARY | RAW | BYTEA |
BLOB | java.sql.Blob | BLOB | BLOB | BYTEA |
CLOB | java.sql.Clob | TEXT, LONGTEXT | CLOB | TEXT |
ARRAY | java.sql.Array | - | VARRAY | ARRAY |
3. 转换实现原理
写入数据库方向(Java → 数据库)
// PreparedStatement 内部转换流程
pstmt.setInt(1, 123);
// 实际转换步骤:
// 1. Java int → JDBC Types.INTEGER
// 2. 根据驱动实现转换为数据库特定格式
// 3. 按照协议编码为网络字节流
// MySQL 驱动内部简化实现 (Connector/J)
public void setInt(int parameterIndex, int x) throws SQLException {
// 检查参数位置有效性
checkBounds(parameterIndex);
// 存储为参数对象,包含值和类型
Parameter p = getParameter(parameterIndex);
p.value = x;
p.type = Types.INTEGER;
p.isNull = false;
// 实际写入时调用编码方法
// byte[] encoded = encodeInteger(x, 4); // 4字节小端序
}从数据库读取方向(数据库 → Java)
int id = rs.getInt("id");
// MySQL 驱动内部简化实现
public int getInt(String columnLabel) throws SQLException {
// 1. 找到列索引
int columnIndex = findColumn(columnLabel);
// 2. 获取原始字节数据
byte[] rawData = rowData.getColumnData(columnIndex);
// 3. 根据协议格式解码
// MySQL 整数格式: 小端序,固定长度
// 4. 类型转换
if (rawData == null) return 0; // SQL NULL 返回 0
// 解码为 int
int value = (rawData[0] & 0xff) |
((rawData[1] & 0xff) << 8) |
((rawData[2] & 0xff) << 16) |
((rawData[3] & 0xff) << 24);
return value;
}4. 类型转换的灵活性
JDBC 允许一定的类型兼容转换:
// 即使数据库是 VARCHAR,也可以读取为 int
ResultSet rs = stmt.executeQuery("SELECT '123' AS num");
int num = rs.getInt("num"); // 自动转换 123
// 内部实现: String → Integer.parseInt()
// 支持的类型转换矩阵 (部分)
// getInt(): VARCHAR, CHAR, DECIMAL, BIGINT, SMALLINT, TINYINT
// getString(): 几乎所有类型(会调用 toString())
// getTimestamp(): DATE, TIME, VARCHAR(符合日期格式)三、高级数据交互机制
1. 预编译与参数绑定
PreparedStatement pstmt = conn.prepareStatement(
"INSERT INTO user(id, name, salary) VALUES(?, ?, ?)"
);
pstmt.setInt(1, 100); // int → Types.INTEGER
pstmt.setString(2, "张三"); // String → Types.VARCHAR
pstmt.setBigDecimal(3, new BigDecimal("9999.99"));
pstmt.executeUpdate();
// MySQL 协议层面的参数替换:
// 服务端收到的是带占位符的预编译语句
// 客户端发送 COM_STMT_PREPARE
// 服务端返回 statement_id
// 客户端发送 COM_STMT_EXECUTE(statement_id, params...)2. 大对象处理(BLOB/CLOB)
// 流式传输,避免内存溢出
try (PreparedStatement pstmt = conn.prepareStatement(
"INSERT INTO files(id, content) VALUES(?, ?)")) {
pstmt.setInt(1, 1);
// CLOB: 字符大对象
Reader reader = new FileReader("large.txt");
pstmt.setCharacterStream(2, reader);
// BLOB: 二进制大对象
InputStream is = new FileInputStream("large.pdf");
pstmt.setBinaryStream(2, is);
pstmt.executeUpdate();
}
// 读取时也使用流式
ResultSet rs = stmt.executeQuery("SELECT content FROM files WHERE id=1");
if (rs.next()) {
InputStream is = rs.getBinaryStream("content");
// 边读边写,不占用大量内存
byte[] buffer = new byte[8192];
int len;
while ((len = is.read(buffer)) != -1) {
// 处理数据
}
}3. 批量操作优化
// 减少网络往返次数
PreparedStatement pstmt = conn.prepareStatement(
"INSERT INTO user(name) VALUES(?)"
);
conn.setAutoCommit(false);
for (int i = 0; i < 1000; i++) {
pstmt.setString(1, "user" + i);
pstmt.addBatch(); // 累积到批次中
if (i % 100 == 0) {
int[] results = pstmt.executeBatch(); // 一次发送100条
conn.commit();
}
}
// 协议层面: 批量发送多个命令包
// 服务端批量执行,减少往返延迟四、性能优化要点
1. 网络交互优化
// 1. 使用连接池减少连接建立开销
// 2. 批量操作减少网络往返
// 3. 只查询需要的列
String sql = "SELECT id, name FROM user"; // 好
String sql = "SELECT * FROM user"; // 差,传输冗余数据
// 4. 使用 fetchSize 控制内存
Statement stmt = conn.createStatement();
stmt.setFetchSize(100); // 每次从数据库取100行
ResultSet rs = stmt.executeQuery("SELECT * FROM huge_table");2. 避免类型转换开销
// 直接使用匹配的类型
int id = rs.getInt("id"); // 高效:直接解码
String idStr = rs.getString("id"); // 低效:int→String 转换
// 使用具体类型而非 Object
Object obj = rs.getObject("amount"); // 需要类型判断和转换
BigDecimal amount = rs.getBigDecimal("amount"); // 直接获取3. 预编译的缓存利用
// 同一个 Connection 中的 PreparedStatement 可以被复用 // 某些驱动支持服务端预编译缓存 // MySQL 需要配置: cachePrepStmts=true String url = "jdbc:mysql://localhost:3306/db?cachePrepStmts=true&useServerPrepStmts=true";
五、实际驱动示例分析
以 MySQL Connector/J 8.0 为例的关键实现:
// 核心类层次
com.mysql.cj.jdbc.ConnectionImpl
└── com.mysql.cj.NativeSession // 管理网络会话
└── com.mysql.cj.protocol.a.NativeProtocol // 协议处理
└── com.mysql.cj.protocol.a.SimplePacketReader // 读取包
└── com.mysql.cj.protocol.a.TimeTrackingPacketWriter // 写入包
// 发送 SQL 的关键方法
protected ResultSetInternalMethods executeInternal(
int maxRowsToRetrieve,
Buffer queryPacket,
boolean sendPacket,
int maxRows,
Catalog nor...
) throws SQLException {
// 1. 发送命令包
this.session.sendCommand(queryPacket, sendPacket);
// 2. 读取响应
this.protocol.readQueryResult(this.columnDefinitions, false, false,
this.session.getServerSession().getCapabilities());
// 3. 解析结果集并转换为 Java 对象
return this.protocol.readAllResults(maxRowsToRetrieve,
true,
this.resultSetFactory,
this.columnDefinitions);
}JDBC 通过精心设计的协议和数据转换机制,在保证类型安全的前提下,实现了 Java 与数据库的高效通信,这套机制经过多年演进已经非常成熟稳定。
到此这篇关于浅析JDBC与数据库如何进行数据交互及类型转换的文章就介绍到这了,更多相关JDBC与数据库数据交互内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
基于@RequestParam注解之Spring MVC参数绑定的利器
这篇文章主要介绍了基于@RequestParam注解之Spring MVC参数绑定的利器,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2025-03-03


最新评论