2025-09-22 17:13:22 +08:00
|
|
|
|
package com.yj.earth.common.util;
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
import org.apache.commons.dbcp2.BasicDataSource;
|
|
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
import java.sql.*;
|
|
|
|
|
|
import java.util.*;
|
|
|
|
|
|
import java.lang.reflect.*;
|
2025-09-29 17:34:21 +08:00
|
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
|
|
import java.time.format.DateTimeParseException;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 优化版SQLite工具类
|
|
|
|
|
|
*/
|
2025-09-22 17:13:22 +08:00
|
|
|
|
public class SQLiteUtil {
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
// 加载SQLite JDBC驱动(静态初始化)
|
2025-09-22 17:13:22 +08:00
|
|
|
|
static {
|
|
|
|
|
|
try {
|
|
|
|
|
|
Class.forName("org.sqlite.JDBC");
|
|
|
|
|
|
} catch (ClassNotFoundException e) {
|
|
|
|
|
|
throw new RuntimeException("无法加载SQLite JDBC驱动", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
// 统一日期格式(LocalDateTime)
|
2025-09-22 17:13:22 +08:00
|
|
|
|
private static final DateTimeFormatter LOCAL_DATE_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
// 连接池缓存:key=数据库文件路径,value=DBCP2数据源(支持多连接复用)
|
|
|
|
|
|
private static final Map<String, BasicDataSource> DATA_SOURCE_POOL = new ConcurrentHashMap<>();
|
|
|
|
|
|
|
|
|
|
|
|
// 字段缓存:缓存类的字段映射(避免反射重复开销)
|
|
|
|
|
|
private static final Map<Class<?>, Map<String, Field>> FIELD_CACHE = new ConcurrentHashMap<>();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ========================== 连接池核心方法 ==========================
|
|
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 从DBCP2连接池获取连接(自动复用/创建连接)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param dbFilePath 数据库文件绝对路径
|
|
|
|
|
|
* @return 线程安全的Connection(使用后需通过try-with-resources自动归还)
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
|
|
|
|
|
public static Connection getConnection(String dbFilePath) throws SQLException {
|
2025-09-29 17:34:21 +08:00
|
|
|
|
if (dbFilePath == null || dbFilePath.trim().isEmpty()) {
|
|
|
|
|
|
throw new IllegalArgumentException("数据库文件路径不能为空");
|
|
|
|
|
|
}
|
|
|
|
|
|
// 不存在则创建数据源,存在则直接从池获取连接
|
|
|
|
|
|
BasicDataSource dataSource = DATA_SOURCE_POOL.computeIfAbsent(dbFilePath, SQLiteUtil::createDataSource);
|
|
|
|
|
|
return dataSource.getConnection();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建DBCP2数据源(配置SQLite性能参数)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param dbFilePath 数据库文件路径
|
|
|
|
|
|
* @return 配置优化后的BasicDataSource
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static BasicDataSource createDataSource(String dbFilePath) {
|
|
|
|
|
|
BasicDataSource dataSource = new BasicDataSource();
|
|
|
|
|
|
// 1. 基础JDBC配置
|
|
|
|
|
|
dataSource.setDriverClassName("org.sqlite.JDBC");
|
|
|
|
|
|
dataSource.setUrl("jdbc:sqlite:" + dbFilePath);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 连接池核心参数(根据并发量调整,SQLite不建议过多连接)
|
|
|
|
|
|
dataSource.setMaxTotal(30); // 最大连接数:20-50(根据服务器CPU/内存调整)
|
|
|
|
|
|
dataSource.setMaxIdle(15); // 最大空闲连接:保留部分连接避免频繁创建
|
|
|
|
|
|
dataSource.setMinIdle(5); // 最小空闲连接:保证基础并发响应速度
|
|
|
|
|
|
dataSource.setTimeBetweenEvictionRunsMillis(60000); // 连接检测间隔:1分钟
|
|
|
|
|
|
dataSource.setMinEvictableIdleTimeMillis(300000); // 连接空闲超时:5分钟(清理长期空闲连接)
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 连接有效性验证(避免使用失效连接)
|
|
|
|
|
|
dataSource.setTestOnBorrow(true); // 借连接时验证
|
|
|
|
|
|
dataSource.setTestOnReturn(false); // 还连接时不验证(减少开销)
|
|
|
|
|
|
dataSource.setValidationQuery("SELECT 1"); // 轻量验证SQL(SQLite支持)
|
|
|
|
|
|
dataSource.setValidationQueryTimeout(2); // 验证超时:2秒
|
|
|
|
|
|
|
|
|
|
|
|
// 4. PreparedStatement缓存(减少SQL解析开销)
|
|
|
|
|
|
dataSource.setPoolPreparedStatements(true);
|
|
|
|
|
|
dataSource.setMaxOpenPreparedStatements(100); // 最大缓存100个PreparedStatement
|
|
|
|
|
|
|
|
|
|
|
|
// 5. SQLite底层性能优化(关键!提升并发能力)
|
|
|
|
|
|
try (Connection conn = dataSource.getConnection();
|
|
|
|
|
|
Statement stmt = conn.createStatement()) {
|
|
|
|
|
|
stmt.execute("PRAGMA journal_mode=WAL;"); // 启用WAL模式:支持多读者+单写者(核心优化)
|
|
|
|
|
|
stmt.execute("PRAGMA cache_size=-20000;"); // 页面缓存20MB(负号表示KB单位,内存足可调大)
|
|
|
|
|
|
stmt.execute("PRAGMA synchronous=NORMAL;"); // 同步级别:平衡性能与安全(崩溃最多丢WAL日志)
|
|
|
|
|
|
stmt.execute("PRAGMA temp_store=MEMORY;"); // 临时表/索引存内存(减少磁盘IO)
|
|
|
|
|
|
stmt.execute("PRAGMA busy_timeout=2000;"); // 忙等待超时:2秒(避免瞬时并发锁等待)
|
|
|
|
|
|
} catch (SQLException e) {
|
|
|
|
|
|
throw new RuntimeException("初始化SQLite数据源失败(路径:" + dbFilePath + ")", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return dataSource;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 关闭指定数据库的数据源(释放所有连接)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param dbFilePath 数据库文件路径
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static void closeDataSource(String dbFilePath) {
|
|
|
|
|
|
if (dbFilePath == null) return;
|
|
|
|
|
|
BasicDataSource dataSource = DATA_SOURCE_POOL.remove(dbFilePath);
|
|
|
|
|
|
if (dataSource != null) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
dataSource.close(); // DBCP2会自动关闭所有活跃/空闲连接
|
|
|
|
|
|
} catch (SQLException e) {
|
|
|
|
|
|
System.err.println("关闭SQLite数据源失败(路径:" + dbFilePath + "):" + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 关闭所有数据源(JVM退出时自动调用)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static void closeAllDataSources() {
|
|
|
|
|
|
for (BasicDataSource dataSource : DATA_SOURCE_POOL.values()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
dataSource.close();
|
|
|
|
|
|
} catch (SQLException e) {
|
|
|
|
|
|
System.err.println("关闭SQLite数据源失败:" + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 清理缓存(避免内存泄漏)
|
|
|
|
|
|
DATA_SOURCE_POOL.clear();
|
|
|
|
|
|
FIELD_CACHE.clear();
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
// JVM关闭钩子:确保程序退出时释放所有数据源资源
|
|
|
|
|
|
static {
|
|
|
|
|
|
Runtime.getRuntime().addShutdownHook(new Thread(SQLiteUtil::closeAllDataSources));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ========================== 数据查询核心方法 ==========================
|
|
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 执行查询并返回单个对象(优化版:连接池+字段缓存)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param dbFilePath 数据库路径
|
|
|
|
|
|
* @param sql 查询SQL
|
|
|
|
|
|
* @param params SQL参数
|
|
|
|
|
|
* @param clazz 返回对象类型
|
|
|
|
|
|
* @return 单个对象(无结果返回null)
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
2025-09-29 17:34:21 +08:00
|
|
|
|
public static <T> T queryForObject(String dbFilePath, String sql, List<Object> params, Class<T> clazz) throws SQLException {
|
2025-09-22 17:13:22 +08:00
|
|
|
|
List<T> results = queryForList(dbFilePath, sql, params, clazz);
|
|
|
|
|
|
return results.isEmpty() ? null : results.get(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 执行查询并返回对象列表(优化版:预构建字段映射+资源自动回收)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param dbFilePath 数据库路径
|
|
|
|
|
|
* @param sql 查询SQL
|
|
|
|
|
|
* @param params SQL参数
|
|
|
|
|
|
* @param clazz 返回对象类型
|
|
|
|
|
|
* @return 结果列表(无结果返回空列表)
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
2025-09-29 17:34:21 +08:00
|
|
|
|
public static <T> List<T> queryForList(String dbFilePath, String sql, List<Object> params, Class<T> clazz) throws SQLException {
|
2025-09-22 17:13:22 +08:00
|
|
|
|
List<T> resultList = new ArrayList<>();
|
2025-09-29 17:34:21 +08:00
|
|
|
|
if (sql == null || sql.trim().isEmpty()) {
|
|
|
|
|
|
throw new IllegalArgumentException("查询SQL不能为空");
|
|
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
// 预加载字段映射(缓存生效,避免重复反射)
|
|
|
|
|
|
Map<String, Field> fieldMap = getFieldMap(clazz);
|
2025-09-22 17:13:22 +08:00
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
// try-with-resources:自动关闭Connection、PreparedStatement、ResultSet
|
|
|
|
|
|
try (Connection conn = getConnection(dbFilePath);
|
|
|
|
|
|
PreparedStatement pstmt = createPreparedStatement(conn, sql, params);
|
|
|
|
|
|
ResultSet rs = pstmt.executeQuery()) {
|
2025-09-22 17:13:22 +08:00
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
ResultSetMetaData metaData = rs.getMetaData();
|
|
|
|
|
|
int columnCount = metaData.getColumnCount();
|
|
|
|
|
|
// 预构建「列名-字段」映射(一次构建,循环复用)
|
|
|
|
|
|
ColumnFieldMapping[] mappings = buildColumnFieldMappings(metaData, columnCount, fieldMap);
|
2025-09-22 17:13:22 +08:00
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
// 处理结果集(反射赋值)
|
|
|
|
|
|
while (rs.next()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
T obj = clazz.getDeclaredConstructor().newInstance(); // 无参构造器(需确保类有)
|
|
|
|
|
|
for (int i = 0; i < columnCount; i++) {
|
|
|
|
|
|
ColumnFieldMapping mapping = mappings[i];
|
|
|
|
|
|
if (mapping.field != null) {
|
|
|
|
|
|
Object value = rs.getObject(i + 1); // ResultSet列索引从1开始
|
|
|
|
|
|
setFieldValueOptimized(obj, mapping.field, value);
|
|
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
resultList.add(obj);
|
2025-09-29 17:34:21 +08:00
|
|
|
|
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException |
|
|
|
|
|
|
InvocationTargetException e) {
|
|
|
|
|
|
throw new SQLException("创建对象实例失败(类型:" + clazz.getName() + ")", e);
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
} catch (SQLException e) {
|
|
|
|
|
|
// 异常时关闭当前数据源(避免后续请求使用异常连接)
|
|
|
|
|
|
closeDataSource(dbFilePath);
|
|
|
|
|
|
throw new SQLException("执行查询失败(SQL:" + sql + ")", e);
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return resultList;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 执行增删改SQL(优化版:连接池+自动提交)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param dbFilePath 数据库路径
|
|
|
|
|
|
* @param sql 增删改SQL
|
|
|
|
|
|
* @param params SQL参数
|
|
|
|
|
|
* @return 影响行数
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
|
|
|
|
|
public static int executeUpdate(String dbFilePath, String sql, List<Object> params) throws SQLException {
|
2025-09-29 17:34:21 +08:00
|
|
|
|
if (sql == null || sql.trim().isEmpty()) {
|
|
|
|
|
|
throw new IllegalArgumentException("执行SQL不能为空");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
try (Connection conn = getConnection(dbFilePath);
|
|
|
|
|
|
PreparedStatement pstmt = createPreparedStatement(conn, sql, params)) {
|
|
|
|
|
|
return pstmt.executeUpdate();
|
2025-09-29 17:34:21 +08:00
|
|
|
|
} catch (SQLException e) {
|
|
|
|
|
|
closeDataSource(dbFilePath);
|
|
|
|
|
|
throw new SQLException("执行增删改失败(SQL:" + sql + ")", e);
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 执行计数查询(优化版:轻量结果处理)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param dbFilePath 数据库路径
|
|
|
|
|
|
* @param sql 计数SQL(如SELECT COUNT(*) ...)
|
|
|
|
|
|
* @param params SQL参数
|
|
|
|
|
|
* @return 计数结果(无结果返回0)
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
2025-09-29 17:34:21 +08:00
|
|
|
|
public static int queryForCount(String dbFilePath, String sql, List<Object> params) throws SQLException {
|
|
|
|
|
|
if (sql == null || sql.trim().isEmpty()) {
|
|
|
|
|
|
throw new IllegalArgumentException("计数SQL不能为空");
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
try (Connection conn = getConnection(dbFilePath);
|
|
|
|
|
|
PreparedStatement pstmt = createPreparedStatement(conn, sql, params);
|
|
|
|
|
|
ResultSet rs = pstmt.executeQuery()) {
|
|
|
|
|
|
return rs.next() ? rs.getInt(1) : 0;
|
|
|
|
|
|
} catch (SQLException e) {
|
|
|
|
|
|
closeDataSource(dbFilePath);
|
|
|
|
|
|
throw new SQLException("执行计数查询失败(SQL:" + sql + ")", e);
|
|
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 执行DDL语句(创建表/索引等)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param dbFilePath 数据库路径
|
|
|
|
|
|
* @param sql DDL语句
|
|
|
|
|
|
* @param params SQL参数(可选,如动态表名)
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
2025-09-29 17:34:21 +08:00
|
|
|
|
public static void executeDDL(String dbFilePath, String sql, List<Object> params) throws SQLException {
|
|
|
|
|
|
if (dbFilePath == null || dbFilePath.trim().isEmpty()) {
|
|
|
|
|
|
throw new IllegalArgumentException("数据库文件路径不能为空");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sql == null || sql.trim().isEmpty()) {
|
|
|
|
|
|
throw new IllegalArgumentException("DDL语句不能为空");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try (Connection conn = getConnection(dbFilePath);
|
|
|
|
|
|
PreparedStatement pstmt = createPreparedStatement(conn, sql, params)) {
|
|
|
|
|
|
pstmt.execute();
|
|
|
|
|
|
} catch (SQLException e) {
|
|
|
|
|
|
closeDataSource(dbFilePath);
|
|
|
|
|
|
throw new SQLException("执行DDL失败(SQL:" + sql + ",路径:" + dbFilePath + ")", e);
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 重载:无参数的DDL执行
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
2025-09-29 17:34:21 +08:00
|
|
|
|
public static void executeDDL(String dbFilePath, String sql) throws SQLException {
|
|
|
|
|
|
executeDDL(dbFilePath, sql, null);
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
|
|
|
|
|
|
// ========================== 工具辅助方法 ==========================
|
|
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 创建PreparedStatement(复用Connection,避免重复获取)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param conn 已获取的Connection(从连接池来)
|
|
|
|
|
|
* @param sql SQL语句
|
|
|
|
|
|
* @param params 参数列表
|
|
|
|
|
|
* @return 配置好的PreparedStatement
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
2025-09-29 17:34:21 +08:00
|
|
|
|
private static PreparedStatement createPreparedStatement(Connection conn, String sql, List<Object> params) throws SQLException {
|
|
|
|
|
|
PreparedStatement pstmt = conn.prepareStatement(sql);
|
|
|
|
|
|
// 设置SQL参数(处理特殊类型如LocalDateTime、byte[])
|
|
|
|
|
|
if (params != null && !params.isEmpty()) {
|
|
|
|
|
|
for (int i = 0; i < params.size(); i++) {
|
|
|
|
|
|
setPreparedStatementValue(pstmt, i + 1, params.get(i));
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
return pstmt;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 设置PreparedStatement参数(处理SQLite特殊类型映射)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param pstmt PreparedStatement对象
|
|
|
|
|
|
* @param index 参数索引(从1开始)
|
|
|
|
|
|
* @param value 参数值
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
2025-09-29 17:34:21 +08:00
|
|
|
|
private static void setPreparedStatementValue(PreparedStatement pstmt, int index, Object value) throws SQLException {
|
2025-09-22 17:13:22 +08:00
|
|
|
|
if (value == null) {
|
2025-09-29 17:34:21 +08:00
|
|
|
|
pstmt.setNull(index, Types.NULL);
|
|
|
|
|
|
return;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
// 特殊类型处理
|
|
|
|
|
|
if (value instanceof LocalDateTime) {
|
|
|
|
|
|
// LocalDateTime转字符串(SQLite无原生DateTime类型)
|
|
|
|
|
|
String dateStr = ((LocalDateTime) value).format(LOCAL_DATE_TIME_FORMATTER);
|
|
|
|
|
|
pstmt.setString(index, dateStr);
|
|
|
|
|
|
} else if (value instanceof byte[]) {
|
|
|
|
|
|
// 二进制数据(如BLOB)
|
|
|
|
|
|
pstmt.setBytes(index, (byte[]) value);
|
|
|
|
|
|
} else if (value instanceof java.util.Date) {
|
|
|
|
|
|
// Date转Timestamp(兼容SQLite时间处理)
|
|
|
|
|
|
pstmt.setTimestamp(index, new Timestamp(((java.util.Date) value).getTime()));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 通用类型(依赖JDBC自动映射)
|
|
|
|
|
|
pstmt.setObject(index, value);
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 预构建「列名-字段」映射(支持下划线转驼峰)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param metaData 结果集元数据
|
|
|
|
|
|
* @param columnCount 列数
|
|
|
|
|
|
* @param fieldMap 类字段缓存
|
|
|
|
|
|
* @return 映射数组(与列顺序对应)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static ColumnFieldMapping[] buildColumnFieldMappings(ResultSetMetaData metaData, int columnCount, Map<String, Field> fieldMap) throws SQLException {
|
|
|
|
|
|
ColumnFieldMapping[] mappings = new ColumnFieldMapping[columnCount];
|
|
|
|
|
|
for (int i = 0; i < columnCount; i++) {
|
|
|
|
|
|
String columnName = metaData.getColumnName(i + 1); // 列名(如military_name)
|
|
|
|
|
|
ColumnFieldMapping mapping = new ColumnFieldMapping();
|
|
|
|
|
|
mapping.columnName = columnName;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 直接匹配字段名(如列名=字段名)
|
|
|
|
|
|
Field field = fieldMap.get(columnName);
|
|
|
|
|
|
// 2. 下划线转驼峰匹配(如military_name → militaryName)
|
|
|
|
|
|
if (field == null) {
|
|
|
|
|
|
String camelCaseName = underscoreToCamelCase(columnName);
|
|
|
|
|
|
field = fieldMap.get(camelCaseName);
|
|
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
mapping.field = field;
|
|
|
|
|
|
mappings[i] = mapping;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
return mappings;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取类的字段缓存(含父类字段,一次反射永久缓存)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param clazz 目标类
|
|
|
|
|
|
* @return 字段名→Field的映射(不可修改)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static Map<String, Field> getFieldMap(Class<?> clazz) {
|
|
|
|
|
|
return FIELD_CACHE.computeIfAbsent(clazz, key -> {
|
|
|
|
|
|
Map<String, Field> fieldMap = new HashMap<>();
|
|
|
|
|
|
// 遍历当前类及所有父类(直到Object)
|
|
|
|
|
|
Class<?> currentClass = clazz;
|
|
|
|
|
|
while (currentClass != null && currentClass != Object.class) {
|
|
|
|
|
|
Field[] fields = currentClass.getDeclaredFields();
|
|
|
|
|
|
for (Field field : fields) {
|
|
|
|
|
|
field.setAccessible(true); // 突破访问权限(private字段可赋值)
|
|
|
|
|
|
fieldMap.put(field.getName(), field);
|
|
|
|
|
|
}
|
|
|
|
|
|
currentClass = currentClass.getSuperclass();
|
|
|
|
|
|
}
|
|
|
|
|
|
return Collections.unmodifiableMap(fieldMap); // 避免外部修改缓存
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 优化的字段赋值(处理类型转换,避免重复异常捕获)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param obj 目标对象
|
|
|
|
|
|
* @param field 字段
|
|
|
|
|
|
* @param value 待赋值的值
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static void setFieldValueOptimized(Object obj, Field field, Object value) {
|
|
|
|
|
|
if (value == null) return;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
try {
|
|
|
|
|
|
Class<?> fieldType = field.getType();
|
|
|
|
|
|
// 类型匹配直接赋值(无转换开销)
|
|
|
|
|
|
if (fieldType.isInstance(value)) {
|
|
|
|
|
|
field.set(obj, value);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 类型不匹配时转换(支持基础类型、时间、二进制等)
|
|
|
|
|
|
Object convertedValue = convertValueOptimized(fieldType, value);
|
|
|
|
|
|
if (convertedValue != null) {
|
|
|
|
|
|
field.set(obj, convertedValue);
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
} catch (IllegalAccessException e) {
|
|
|
|
|
|
System.err.println("警告:字段赋值失败(字段:" + field.getName() + ",值类型:" + value.getClass().getName() + "):" + e.getMessage());
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 优化的类型转换(支持常见SQLite类型→Java类型)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param targetType 目标类型(字段类型)
|
|
|
|
|
|
* @param value 原始值(ResultSet获取的值)
|
|
|
|
|
|
* @return 转换后的值(无法转换返回null)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static Object convertValueOptimized(Class<?> targetType, Object value) {
|
|
|
|
|
|
// 基础数值类型转换
|
|
|
|
|
|
if (targetType == int.class || targetType == Integer.class) {
|
|
|
|
|
|
if (value instanceof Number) return ((Number) value).intValue();
|
|
|
|
|
|
if (value instanceof String) return parseInteger((String) value);
|
|
|
|
|
|
} else if (targetType == long.class || targetType == Long.class) {
|
|
|
|
|
|
if (value instanceof Number) return ((Number) value).longValue();
|
|
|
|
|
|
if (value instanceof String) return parseLong((String) value);
|
|
|
|
|
|
} else if (targetType == double.class || targetType == Double.class) {
|
|
|
|
|
|
if (value instanceof Number) return ((Number) value).doubleValue();
|
|
|
|
|
|
if (value instanceof String) return parseDouble((String) value);
|
|
|
|
|
|
} else if (targetType == boolean.class || targetType == Boolean.class) {
|
|
|
|
|
|
if (value instanceof Number) return ((Number) value).intValue() != 0;
|
|
|
|
|
|
if (value instanceof String) return "true".equalsIgnoreCase((String) value) || "1".equals(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 字符串类型
|
2025-09-22 17:13:22 +08:00
|
|
|
|
else if (targetType == String.class) {
|
|
|
|
|
|
return value.toString();
|
|
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
// 时间类型(LocalDateTime)
|
|
|
|
|
|
else if (targetType == LocalDateTime.class) {
|
|
|
|
|
|
return convertToLocalDateTime(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 时间类型(Date)
|
|
|
|
|
|
else if (targetType == java.util.Date.class) {
|
|
|
|
|
|
return convertToDate(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 二进制类型(byte[])
|
|
|
|
|
|
else if (targetType == byte[].class) {
|
|
|
|
|
|
return convertToByteArray(value);
|
|
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
// 不支持的类型转换(打印警告)
|
|
|
|
|
|
System.err.println("警告:不支持的类型转换(目标类型:" + targetType.getName() + ",原始值类型:" + value.getClass().getName() + ")");
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========================== 类型转换辅助方法 ==========================
|
|
|
|
|
|
private static Integer parseInteger(String value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return Integer.parseInt(value.trim());
|
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
|
System.err.println("警告:字符串转Integer失败(值:" + value + ")");
|
|
|
|
|
|
return null;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
private static Long parseLong(String value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return Long.parseLong(value.trim());
|
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
|
System.err.println("警告:字符串转Long失败(值:" + value + ")");
|
|
|
|
|
|
return null;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
private static Double parseDouble(String value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return Double.parseDouble(value.trim());
|
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
|
System.err.println("警告:字符串转Double失败(值:" + value + ")");
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static LocalDateTime convertToLocalDateTime(Object value) {
|
|
|
|
|
|
if (value instanceof String) {
|
2025-09-22 17:13:22 +08:00
|
|
|
|
try {
|
2025-09-29 17:34:21 +08:00
|
|
|
|
return LocalDateTime.parse((String) value, LOCAL_DATE_TIME_FORMATTER);
|
|
|
|
|
|
} catch (DateTimeParseException e) {
|
|
|
|
|
|
System.err.println("警告:字符串转LocalDateTime失败(值:" + value + ")");
|
2025-09-22 17:13:22 +08:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
} else if (value instanceof Timestamp) {
|
|
|
|
|
|
return ((Timestamp) value).toLocalDateTime();
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
return null;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
private static java.util.Date convertToDate(Object value) {
|
|
|
|
|
|
if (value instanceof Timestamp) {
|
|
|
|
|
|
return new java.util.Date(((Timestamp) value).getTime());
|
|
|
|
|
|
} else if (value instanceof LocalDateTime) {
|
|
|
|
|
|
return java.util.Date.from(((LocalDateTime) value).atZone(java.time.ZoneId.systemDefault()).toInstant());
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
return null;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
private static byte[] convertToByteArray(Object value) {
|
|
|
|
|
|
if (value instanceof Blob) {
|
|
|
|
|
|
Blob blob = (Blob) value;
|
|
|
|
|
|
try {
|
|
|
|
|
|
return blob.getBytes(1, (int) blob.length());
|
|
|
|
|
|
} catch (SQLException e) {
|
|
|
|
|
|
System.err.println("警告:Blob转byte[]失败:" + e.getMessage());
|
|
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
return null;
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 下划线命名转驼峰命名(如military_type → militaryType)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param str 下划线字符串
|
|
|
|
|
|
* @return 驼峰字符串
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
2025-09-29 17:34:21 +08:00
|
|
|
|
private static String underscoreToCamelCase(String str) {
|
|
|
|
|
|
if (str == null || str.isEmpty()) return str;
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
|
boolean nextUpperCase = false;
|
|
|
|
|
|
for (char c : str.toCharArray()) {
|
|
|
|
|
|
if (c == '_') {
|
|
|
|
|
|
nextUpperCase = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (nextUpperCase) {
|
|
|
|
|
|
sb.append(Character.toUpperCase(c));
|
|
|
|
|
|
nextUpperCase = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sb.append(Character.toLowerCase(c));
|
|
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-29 17:34:21 +08:00
|
|
|
|
return sb.toString();
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
|
|
|
|
|
|
// ========================== 内部辅助类 ==========================
|
|
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 列-字段映射模型(一次性构建,减少循环内计算)
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
2025-09-29 17:34:21 +08:00
|
|
|
|
private static class ColumnFieldMapping {
|
|
|
|
|
|
String columnName; // 数据库列名
|
|
|
|
|
|
Field field; // 对应的Java字段
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
|
|
|
|
|
|
// ========================== 初始化表结构方法(保留原逻辑) ==========================
|
|
|
|
|
|
|
2025-09-22 17:13:22 +08:00
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 初始化模型相关表(model_type + model)
|
2025-09-22 17:13:22 +08:00
|
|
|
|
*/
|
2025-09-29 17:34:21 +08:00
|
|
|
|
public static void initializationModel(String modelPath) throws SQLException {
|
2025-09-22 17:13:22 +08:00
|
|
|
|
String sql = """
|
2025-09-29 17:34:21 +08:00
|
|
|
|
CREATE TABLE IF NOT EXISTS "model_type" (
|
2025-09-22 17:13:22 +08:00
|
|
|
|
"id" TEXT,
|
|
|
|
|
|
"name" TEXT,
|
|
|
|
|
|
"parent_id" TEXT,
|
2025-09-26 13:46:24 +08:00
|
|
|
|
"tree_index" INTEGER,
|
2025-09-22 17:13:22 +08:00
|
|
|
|
"created_at" TEXT,
|
|
|
|
|
|
"updated_at" TEXT,
|
|
|
|
|
|
PRIMARY KEY ("id")
|
|
|
|
|
|
);
|
|
|
|
|
|
""";
|
|
|
|
|
|
executeDDL(modelPath, sql);
|
|
|
|
|
|
|
|
|
|
|
|
sql = """
|
2025-09-29 17:34:21 +08:00
|
|
|
|
CREATE TABLE IF NOT EXISTS "model" (
|
2025-09-22 17:13:22 +08:00
|
|
|
|
"id" TEXT,
|
|
|
|
|
|
"model_type_id" TEXT,
|
|
|
|
|
|
"model_name" TEXT,
|
|
|
|
|
|
"model_type" TEXT,
|
2025-09-29 13:56:36 +08:00
|
|
|
|
"model_data" BLOB,
|
2025-09-22 17:13:22 +08:00
|
|
|
|
"poster_type" TEXT,
|
2025-09-29 13:56:36 +08:00
|
|
|
|
"poster_data" BLOB,
|
2025-09-22 17:13:22 +08:00
|
|
|
|
"created_at" TEXT,
|
|
|
|
|
|
"updated_at" TEXT,
|
|
|
|
|
|
PRIMARY KEY ("id")
|
|
|
|
|
|
);
|
|
|
|
|
|
""";
|
|
|
|
|
|
executeDDL(modelPath, sql);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 16:45:42 +08:00
|
|
|
|
/**
|
2025-09-29 17:34:21 +08:00
|
|
|
|
* 初始化军标相关表(military_type + military)
|
2025-09-23 16:45:42 +08:00
|
|
|
|
*/
|
2025-09-29 17:34:21 +08:00
|
|
|
|
public static void initializationMilitary(String militaryPath) throws SQLException {
|
2025-09-23 16:45:42 +08:00
|
|
|
|
String sql = """
|
2025-09-29 17:34:21 +08:00
|
|
|
|
CREATE TABLE IF NOT EXISTS "military_type" (
|
2025-09-23 16:45:42 +08:00
|
|
|
|
"id" TEXT,
|
|
|
|
|
|
"name" TEXT,
|
|
|
|
|
|
"parent_id" TEXT,
|
2025-09-26 13:46:24 +08:00
|
|
|
|
"tree_index" INTEGER,
|
2025-09-23 16:45:42 +08:00
|
|
|
|
"created_at" TEXT,
|
|
|
|
|
|
"updated_at" TEXT,
|
|
|
|
|
|
PRIMARY KEY ("id")
|
|
|
|
|
|
);
|
|
|
|
|
|
""";
|
2025-09-29 17:34:21 +08:00
|
|
|
|
executeDDL(militaryPath, sql);
|
2025-09-23 16:45:42 +08:00
|
|
|
|
|
|
|
|
|
|
sql = """
|
2025-09-29 17:34:21 +08:00
|
|
|
|
CREATE TABLE IF NOT EXISTS "military" (
|
2025-09-23 16:45:42 +08:00
|
|
|
|
"id" TEXT,
|
|
|
|
|
|
"military_type_id" TEXT,
|
|
|
|
|
|
"military_name" TEXT,
|
|
|
|
|
|
"military_type" TEXT,
|
2025-09-29 17:34:21 +08:00
|
|
|
|
"military_data" BLOB,
|
2025-09-23 16:45:42 +08:00
|
|
|
|
"created_at" TEXT,
|
|
|
|
|
|
"updated_at" TEXT,
|
|
|
|
|
|
PRIMARY KEY ("id")
|
|
|
|
|
|
);
|
|
|
|
|
|
""";
|
2025-09-29 17:34:21 +08:00
|
|
|
|
executeDDL(militaryPath, sql);
|
2025-09-23 16:45:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 17:34:21 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 初始化图标相关表(icon_type + icon)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static void initializationIcon(String iconPath) throws SQLException {
|
2025-09-26 13:46:24 +08:00
|
|
|
|
String sql = """
|
2025-09-29 17:34:21 +08:00
|
|
|
|
CREATE TABLE IF NOT EXISTS "icon_type" (
|
2025-09-26 13:46:24 +08:00
|
|
|
|
"id" TEXT,
|
|
|
|
|
|
"name" TEXT,
|
|
|
|
|
|
"parent_id" TEXT,
|
|
|
|
|
|
"tree_index" INTEGER,
|
|
|
|
|
|
"created_at" TEXT,
|
|
|
|
|
|
"updated_at" TEXT,
|
|
|
|
|
|
PRIMARY KEY ("id")
|
|
|
|
|
|
);
|
|
|
|
|
|
""";
|
|
|
|
|
|
executeDDL(iconPath, sql);
|
|
|
|
|
|
|
|
|
|
|
|
sql = """
|
2025-09-29 17:34:21 +08:00
|
|
|
|
CREATE TABLE IF NOT EXISTS "icon" (
|
2025-09-26 13:46:24 +08:00
|
|
|
|
"id" TEXT,
|
|
|
|
|
|
"icon_type_id" TEXT,
|
|
|
|
|
|
"icon_name" TEXT,
|
|
|
|
|
|
"icon_type" TEXT,
|
|
|
|
|
|
"data" TEXT,
|
|
|
|
|
|
"view" TEXT,
|
|
|
|
|
|
"created_at" TEXT,
|
|
|
|
|
|
"updated_at" TEXT,
|
|
|
|
|
|
PRIMARY KEY ("id")
|
|
|
|
|
|
);
|
|
|
|
|
|
""";
|
|
|
|
|
|
executeDDL(iconPath, sql);
|
|
|
|
|
|
}
|
2025-09-22 17:13:22 +08:00
|
|
|
|
}
|