From 0e2da03a077c3fd66f707436b44def3e92b4cf8b Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Fri, 20 Jun 2025 14:09:03 +0800 Subject: [PATCH] 'commit' --- Doc/13、读写分离.md | 160 ++++ Doc/原版Db.java | 872 +++++++++++++++++ .../com/jfinal/plugin/activerecord/Db.java | 904 ++++++++++++++++++ .../src/main/resources/application_dev.yaml | 11 + .../src/main/resources/application_pro.yaml | 19 +- dsRes/src/main/resources/application_dev.yaml | 14 + dsRes/src/main/resources/application_pro.yaml | 14 + 7 files changed, 1990 insertions(+), 4 deletions(-) create mode 100644 Doc/13、读写分离.md create mode 100644 Doc/原版Db.java create mode 100644 dsBase/src/main/java/com/jfinal/plugin/activerecord/Db.java diff --git a/Doc/13、读写分离.md b/Doc/13、读写分离.md new file mode 100644 index 00000000..2d77ee9f --- /dev/null +++ b/Doc/13、读写分离.md @@ -0,0 +1,160 @@ +# 数据库读写分离功能说明 + +## 概述 + +本项目已集成数据库读写分离功能,通过修改 `Db.java` 类实现了自动的读写分离路由。该功能具有以下特点: + +- **自动检测**:根据 `application.yaml` 配置自动检测是否存在从库配置 +- **智能路由**:读操作自动路由到从库,写操作强制使用主库 +- **负载均衡**:多个从库时随机选择一个进行读取 +- **故障转移**:从库连接失败时自动回退到主库 +- **零侵入**:现有代码无需修改,透明支持读写分离 + +## 配置方式 + +### 1. 主库配置(必须) + +在 `application_dev.yaml` 或 `application_pro.yaml` 中配置主库: + +```yaml +mysql: + driverClassName: com.mysql.cj.jdbc.Driver + user: root + password: 123456 + jdbcUrl: jdbc:mysql://localhost:3306/dswork?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true +``` + +### 2. 从库配置(可选) + +如果需要启用读写分离,添加从库配置: + +```yaml +mysql: + driverClassName: com.mysql.cj.jdbc.Driver + user: root + password: 123456 + jdbcUrl: jdbc:mysql://localhost:3306/dswork?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true + + # 从库1 + slave1: + driverClassName: com.mysql.cj.jdbc.Driver + user: root + password: 123456 + jdbcUrl: jdbc:mysql://slave1:3306/dswork?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true + + # 从库2 + slave2: + driverClassName: com.mysql.cj.jdbc.Driver + user: root + password: 123456 + jdbcUrl: jdbc:mysql://slave2:3306/dswork?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true +``` + +## 使用方式 + +### 1. 自动读写分离(推荐) + +现有代码无需修改,系统会自动根据 SQL 类型进行路由: + +```java +// 读操作 - 自动路由到从库 +List users = Db.find("select * from user where status = ?", 1); +Record user = Db.findFirst("select * from user where id = ?", 123); +List result = Db.query("select count(*) from user"); + +// 写操作 - 强制使用主库 +Db.update("insert into user(name, email) values(?, ?)", "张三", "zhangsan@example.com"); +Db.update("update user set status = ? where id = ?", 1, 123); +Db.delete("delete from user where id = ?", 123); + +// 事务操作 - 强制使用主库 +Db.tx(() -> { + Db.update("insert into user(name) values(?)", "李四"); + Db.update("update user set status = 1 where name = ?", "李四"); + return true; +}); +``` + +### 2. 强制指定数据库 + +如果需要强制使用特定数据库,可以使用以下方法: + +```java +// 强制使用主库查询(用于需要强一致性的场景) +List users = Db.queryFromMaster("select * from user where id = ?", 123); +Record user = Db.findFirstFromMaster("select * from user where id = ?", 123); + +// 强制使用指定从库查询 +List users = Db.queryFromSlave(0, "select * from user"); // 使用第1个从库 +``` + +### 3. 查看读写分离状态 + +```java +// 获取读写分离状态信息 +String status = Db.getReadWriteSeparationStatus(); +System.out.println(status); +``` + +## 自动路由规则 + +### 读操作(路由到从库) + +以下 SQL 语句会自动路由到从库: +- `SELECT` 查询语句 +- `SHOW` 语句 +- `DESC` / `DESCRIBE` 语句 +- `EXPLAIN` 语句 + +对应的方法包括: +- `Db.find()`、`Db.findFirst()`、`Db.findById()` 等 +- `Db.query()`、`Db.queryFirst()`、`Db.queryXxx()` 等 +- `Db.paginate()` 分页查询 +- `Db.findByCache()` 缓存查询 + +### 写操作(强制使用主库) + +以下操作强制使用主库: +- `INSERT`、`UPDATE`、`DELETE` 语句 +- 所有事务操作 +- `Db.save()`、`Db.update()`、`Db.delete()` 等 +- `Db.tx()` 事务方法 + +## 性能优化建议 + +1. **合理配置从库数量**:根据读写比例配置适当数量的从库 +2. **使用连接池**:确保主从库都配置了合适的连接池参数 +3. **监控从库延迟**:定期检查主从同步延迟,避免读取到过期数据 +4. **强一致性场景**:对于需要强一致性的查询,使用 `queryFromMaster()` 方法 + +## 注意事项 + +1. **主从同步延迟**:从库可能存在数据同步延迟,对于实时性要求高的查询建议使用主库 +2. **事务一致性**:所有事务操作都在主库执行,确保数据一致性 +3. **配置格式**:从库配置必须以 `slave` 开头,后跟数字(如 `slave1`、`slave2`) +4. **故障处理**:从库连接失败时会自动回退到主库,不会影响业务正常运行 + +## 启动日志 + +系统启动时会输出读写分离状态信息: + +``` +[Db] 读写分离已启用,发现 2 个从库配置 +``` + +或 + +``` +[Db] 未发现从库配置,使用单库模式 +``` + +## 故障排查 + +如果读写分离功能异常,请检查: + +1. 配置文件中从库配置格式是否正确 +2. 从库连接信息是否正确 +3. 从库服务是否正常运行 +4. 网络连接是否正常 + +系统会在控制台输出相关错误信息,便于排查问题。 \ No newline at end of file diff --git a/Doc/原版Db.java b/Doc/原版Db.java new file mode 100644 index 00000000..f6e57286 --- /dev/null +++ b/Doc/原版Db.java @@ -0,0 +1,872 @@ +/** + * Copyright (c) 2011-2025, James Zhan 詹波 (jfinal@126.com). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jfinal.plugin.activerecord; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.function.Function; +import com.jfinal.kit.SyncWriteMap; + +/** + * Db. Powerful database query and update tool box. + */ +@SuppressWarnings("rawtypes") +public class Db { + + private static DbPro MAIN = null; + private static final Map cache = new SyncWriteMap<>(32, 0.25F); + + /** + * for DbKit.addConfig(configName) + */ + static void init(String configName) { + MAIN = DbKit.getConfig(configName).dbProFactory.getDbPro(configName); // new DbPro(configName); + cache.put(configName, MAIN); + } + + /** + * for DbKit.removeConfig(configName) + */ + static void removeDbProWithConfig(String configName) { + if (MAIN != null && MAIN.config.getName().equals(configName)) { + MAIN = null; + } + cache.remove(configName); + } + + public static DbPro use(String configName) { + DbPro result = cache.get(configName); + if (result == null) { + Config config = DbKit.getConfig(configName); + if (config == null) { + throw new IllegalArgumentException("Config not found by configName: " + configName); + } + result = config.dbProFactory.getDbPro(configName); // new DbPro(configName); + cache.put(configName, result); + } + return result; + } + + public static DbPro use() { + return MAIN; + } + + static List query(Config config, Connection conn, String sql, Object... paras) throws SQLException { + return MAIN.query(config, conn, sql, paras); + } + + /** + * sql paras 查询,从 JDBC 原样取值且不封装到 Record 对象 + */ + public static List query(String sql, Object... paras) { + return MAIN.query(sql, paras); + } + + /** + * @see #query(String, Object...) + * @param sql an SQL statement + */ + public static List query(String sql) { + return MAIN.query(sql); + } + + /** + * Execute sql query and return the first result. I recommend add "limit 1" in your sql. + * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders + * @param paras the parameters of sql + * @return Object[] if your sql has select more than one column, + * and it return Object if your sql has select only one column. + */ + public static T queryFirst(String sql, Object... paras) { + return MAIN.queryFirst(sql, paras); + } + + /** + * @see #queryFirst(String, Object...) + * @param sql an SQL statement + */ + public static T queryFirst(String sql) { + return MAIN.queryFirst(sql); + } + + // 26 queryXxx method below ----------------------------------------------- + /** + * Execute sql query just return one column. + * @param the type of the column that in your sql's select statement + * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders + * @param paras the parameters of sql + * @return T + */ + public static T queryColumn(String sql, Object... paras) { + return MAIN.queryColumn(sql, paras); + } + + public static T queryColumn(String sql) { + return MAIN.queryColumn(sql); + } + + public static String queryStr(String sql, Object... paras) { + return MAIN.queryStr(sql, paras); + } + + public static String queryStr(String sql) { + return MAIN.queryStr(sql); + } + + public static Integer queryInt(String sql, Object... paras) { + return MAIN.queryInt(sql, paras); + } + + public static Integer queryInt(String sql) { + return MAIN.queryInt(sql); + } + + public static Long queryLong(String sql, Object... paras) { + return MAIN.queryLong(sql, paras); + } + + public static Long queryLong(String sql) { + return MAIN.queryLong(sql); + } + + public static Double queryDouble(String sql, Object... paras) { + return MAIN.queryDouble(sql, paras); + } + + public static Double queryDouble(String sql) { + return MAIN.queryDouble(sql); + } + + public static Float queryFloat(String sql, Object... paras) { + return MAIN.queryFloat(sql, paras); + } + + public static Float queryFloat(String sql) { + return MAIN.queryFloat(sql); + } + + public static BigDecimal queryBigDecimal(String sql, Object... paras) { + return MAIN.queryBigDecimal(sql, paras); + } + + public static BigDecimal queryBigDecimal(String sql) { + return MAIN.queryBigDecimal(sql); + } + + public static BigInteger queryBigInteger(String sql, Object... paras) { + return MAIN.queryBigInteger(sql, paras); + } + + public static BigInteger queryBigInteger(String sql) { + return MAIN.queryBigInteger(sql); + } + + public static byte[] queryBytes(String sql, Object... paras) { + return MAIN.queryBytes(sql, paras); + } + + public static byte[] queryBytes(String sql) { + return MAIN.queryBytes(sql); + } + + public static java.util.Date queryDate(String sql, Object... paras) { + return MAIN.queryDate(sql, paras); + } + + public static java.util.Date queryDate(String sql) { + return MAIN.queryDate(sql); + } + + public static LocalDateTime queryLocalDateTime(String sql, Object... paras) { + return MAIN.queryLocalDateTime(sql, paras); + } + + public static LocalDateTime queryLocalDateTime(String sql) { + return MAIN.queryLocalDateTime(sql); + } + + public static java.sql.Time queryTime(String sql, Object... paras) { + return MAIN.queryTime(sql, paras); + } + + public static java.sql.Time queryTime(String sql) { + return MAIN.queryTime(sql); + } + + public static java.sql.Timestamp queryTimestamp(String sql, Object... paras) { + return MAIN.queryTimestamp(sql, paras); + } + + public static java.sql.Timestamp queryTimestamp(String sql) { + return MAIN.queryTimestamp(sql); + } + + public static Boolean queryBoolean(String sql, Object... paras) { + return MAIN.queryBoolean(sql, paras); + } + + public static Boolean queryBoolean(String sql) { + return MAIN.queryBoolean(sql); + } + + public static Short queryShort(String sql, Object... paras) { + return MAIN.queryShort(sql, paras); + } + + public static Short queryShort(String sql) { + return MAIN.queryShort(sql); + } + + public static Byte queryByte(String sql, Object... paras) { + return MAIN.queryByte(sql, paras); + } + + public static Byte queryByte(String sql) { + return MAIN.queryByte(sql); + } + + public static Number queryNumber(String sql, Object... paras) { + return MAIN.queryNumber(sql, paras); + } + + public static Number queryNumber(String sql) { + return MAIN.queryNumber(sql); + } + // 26 queryXxx method under ----------------------------------------------- + + /** + * Execute sql update + */ + static int update(Config config, Connection conn, String sql, Object... paras) throws SQLException { + return MAIN.update(config, conn, sql, paras); + } + + /** + * Execute update, insert or delete sql statement. + * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders + * @param paras the parameters of sql + * @return either the row count for INSERT, UPDATE, + * or DELETE statements, or 0 for SQL statements + * that return nothing + */ + public static int update(String sql, Object... paras) { + return MAIN.update(sql, paras); + } + + /** + * @see #update(String, Object...) + * @param sql an SQL statement + */ + public static int update(String sql) { + return MAIN.update(sql); + } + + static List find(Config config, Connection conn, String sql, Object... paras) throws SQLException { + return MAIN.find(config, conn, sql, paras); + } + + /** + * sql paras 查询,数据封装到 Record 对象 + */ + public static List find(String sql, Object... paras) { + return MAIN.find(sql, paras); + } + + /** + * @see #find(String, Object...) + * @param sql the sql statement + */ + public static List find(String sql) { + return MAIN.find(sql); + } + + public static List findAll(String tableName) { + return MAIN.findAll(tableName); + } + + /** + * Find first record. I recommend add "limit 1" in your sql. + * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders + * @param paras the parameters of sql + * @return the Record object + */ + public static Record findFirst(String sql, Object... paras) { + return MAIN.findFirst(sql, paras); + } + + /** + * @see #findFirst(String, Object...) + * @param sql an SQL statement + */ + public static Record findFirst(String sql) { + return MAIN.findFirst(sql); + } + + /** + * Find record by id with default primary key. + *
+	 * Example:
+	 * Record user = Db.findById("user", 15);
+	 * 
+ * @param tableName the table name of the table + * @param idValue the id value of the record + */ + public static Record findById(String tableName, Object idValue) { + return MAIN.findById(tableName, idValue); + } + + public static Record findById(String tableName, String primaryKey, Object idValue) { + return MAIN.findById(tableName, primaryKey, idValue); + } + + /** + * Find record by ids. + *
+	 * Example:
+	 * Record user = Db.findByIds("user", "user_id", 123);
+	 * Record userRole = Db.findByIds("user_role", "user_id, role_id", 123, 456);
+	 * 
+ * @param tableName the table name of the table + * @param primaryKey the primary key of the table, composite primary key is separated by comma character: "," + * @param idValues the id value of the record, it can be composite id values + */ + public static Record findByIds(String tableName, String primaryKey, Object... idValues) { + return MAIN.findByIds(tableName, primaryKey, idValues); + } + + /** + * Delete record by id with default primary key. + *
+	 * Example:
+	 * Db.deleteById("user", 15);
+	 * 
+ * @param tableName the table name of the table + * @param idValue the id value of the record + * @return true if delete succeed otherwise false + */ + public static boolean deleteById(String tableName, Object idValue) { + return MAIN.deleteById(tableName, idValue); + } + + public static boolean deleteById(String tableName, String primaryKey, Object idValue) { + return MAIN.deleteById(tableName, primaryKey, idValue); + } + + /** + * Delete record by ids. + *
+	 * Example:
+	 * Db.deleteByIds("user", "user_id", 15);
+	 * Db.deleteByIds("user_role", "user_id, role_id", 123, 456);
+	 * 
+ * @param tableName the table name of the table + * @param primaryKey the primary key of the table, composite primary key is separated by comma character: "," + * @param idValues the id value of the record, it can be composite id values + * @return true if delete succeed otherwise false + */ + public static boolean deleteByIds(String tableName, String primaryKey, Object... idValues) { + return MAIN.deleteByIds(tableName, primaryKey, idValues); + } + + /** + * Delete record. + *
+	 * Example:
+	 * boolean succeed = Db.delete("user", "id", user);
+	 * 
+ * @param tableName the table name of the table + * @param primaryKey the primary key of the table, composite primary key is separated by comma character: "," + * @param record the record + * @return true if delete succeed otherwise false + */ + public static boolean delete(String tableName, String primaryKey, Record record) { + return MAIN.delete(tableName, primaryKey, record); + } + + /** + *
+	 * Example:
+	 * boolean succeed = Db.delete("user", user);
+	 * 
+ * @see #delete(String, String, Record) + */ + public static boolean delete(String tableName, Record record) { + return MAIN.delete(tableName, record); + } + + /** + * Execute delete sql statement. + * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders + * @param paras the parameters of sql + * @return the row count for DELETE statements, or 0 for SQL statements + * that return nothing + */ + public static int delete(String sql, Object... paras) { + return MAIN.delete(sql, paras); + } + + /** + * @see #delete(String, Object...) + * @param sql an SQL statement + */ + public static int delete(String sql) { + return MAIN.delete(sql); + } + + static Page paginate(Config config, Connection conn, int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras) throws SQLException { + return MAIN.paginate(config, conn, pageNumber, pageSize, select, sqlExceptSelect, paras); + } + + /** + * Paginate. + * @param pageNumber the page number + * @param pageSize the page size + * @param select the select part of the sql statement + * @param sqlExceptSelect the sql statement excluded select part + * @param paras the parameters of sql + * @return the Page object + */ + public static Page paginate(int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras) { + return MAIN.paginate(pageNumber, pageSize, select, sqlExceptSelect, paras); + } + + public static Page paginate(int pageNumber, int pageSize, boolean isGroupBySql, String select, String sqlExceptSelect, Object... paras) { + return MAIN.paginate(pageNumber, pageSize, isGroupBySql, select, sqlExceptSelect, paras); + } + + /** + * 分页 + */ + public static Page paginate(int pageNumber, int pageSize, String select, String sqlExceptSelect) { + return MAIN.paginate(pageNumber, pageSize, select, sqlExceptSelect); + } + + public static Page paginateByFullSql(int pageNumber, int pageSize, String totalRowSql, String findSql, Object... paras) { + return MAIN.paginateByFullSql(pageNumber, pageSize, totalRowSql, findSql, paras); + } + + public static Page paginateByFullSql(int pageNumber, int pageSize, boolean isGroupBySql, String totalRowSql, String findSql, Object... paras) { + return MAIN.paginateByFullSql(pageNumber, pageSize, isGroupBySql, totalRowSql, findSql, paras); + } + + static boolean save(Config config, Connection conn, String tableName, String primaryKey, Record record) throws SQLException { + return MAIN.save(config, conn, tableName, primaryKey, record); + } + + /** + * Save record. + *
+	 * Example:
+	 * Record userRole = new Record().set("user_id", 123).set("role_id", 456);
+	 * Db.save("user_role", "user_id, role_id", userRole);
+	 * 
+ * @param tableName the table name of the table + * @param primaryKey the primary key of the table, composite primary key is separated by comma character: "," + * @param record the record will be saved + * @return true if save succeed otherwise false + */ + public static boolean save(String tableName, String primaryKey, Record record) { + return MAIN.save(tableName, primaryKey, record); + } + + /** + * @see #save(String, String, Record) + */ + public static boolean save(String tableName, Record record) { + return MAIN.save(tableName, record); + } + + static boolean update(Config config, Connection conn, String tableName, String primaryKey, Record record) throws SQLException { + return MAIN.update(config, conn, tableName, primaryKey, record); + } + + /** + * Update Record. + *
+	 * Example:
+	 * Db.update("user_role", "user_id, role_id", record);
+	 * 
+ * @param tableName the table name of the Record save to + * @param primaryKey the primary key of the table, composite primary key is separated by comma character: "," + * @param record the Record object + * @return true if update succeed otherwise false + */ + public static boolean update(String tableName, String primaryKey, Record record) { + return MAIN.update(tableName, primaryKey, record); + } + + /** + * Update record with default primary key. + *
+	 * Example:
+	 * Db.update("user", record);
+	 * 
+ * @see #update(String, String, Record) + */ + public static boolean update(String tableName, Record record) { + return MAIN.update(tableName, record); + } + + /** + * @see #execute(Config, ICallback) + */ + public static Object execute(ICallback callback) { + return MAIN.execute(callback); + } + + /** + * Execute callback. It is useful when all the API can not satisfy your requirement. + * @param config the Config object + * @param callback the ICallback interface + */ + static Object execute(Config config, ICallback callback) { + return MAIN.execute(config, callback); + } + + /** + * Execute transaction. + * @param config the Config object + * @param transactionLevel the transaction level + * @param atom the atom operation + * @return true if transaction executing succeed otherwise false + */ + static boolean tx(Config config, int transactionLevel, IAtom atom) { + return MAIN.tx(config, transactionLevel, atom); + } + + /** + * Execute transaction with default transaction level. + * @see #tx(int, IAtom) + */ + public static boolean tx(IAtom atom) { + return MAIN.tx(atom); + } + + public static boolean tx(int transactionLevel, IAtom atom) { + return MAIN.tx(transactionLevel, atom); + } + + /** + * 主要用于嵌套事务场景 + * + * 实例:https://jfinal.com/feedback/4008 + * + * 默认情况下嵌套事务会被合并成为一个事务,那么内层与外层任何地方回滚事务 + * 所有嵌套层都将回滚事务,也就是说嵌套事务无法独立提交与回滚 + * + * 使用 txInNewThread(...) 方法可以实现层之间的事务控制的独立性 + * 由于事务处理是将 Connection 绑定到线程上的,所以 txInNewThread(...) + * 通过建立新线程来实现嵌套事务的独立控制 + */ + public static Future txInNewThread(IAtom atom) { + return MAIN.txInNewThread(atom); + } + + public static Future txInNewThread(int transactionLevel, IAtom atom) { + return MAIN.txInNewThread(transactionLevel, atom); + } + + /** + * Find Record by cache. + * @see #find(String, Object...) + * @param cacheName the cache name + * @param key the key used to get date from cache + * @return the list of Record + */ + public static List findByCache(String cacheName, Object key, String sql, Object... paras) { + return MAIN.findByCache(cacheName, key, sql, paras); + } + + /** + * @see #findByCache(String, Object, String, Object...) + */ + public static List findByCache(String cacheName, Object key, String sql) { + return MAIN.findByCache(cacheName, key, sql); + } + + /** + * Find first record by cache. I recommend add "limit 1" in your sql. + * @see #findFirst(String, Object...) + * @param cacheName the cache name + * @param key the key used to get date from cache + * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders + * @param paras the parameters of sql + * @return the Record object + */ + public static Record findFirstByCache(String cacheName, Object key, String sql, Object... paras) { + return MAIN.findFirstByCache(cacheName, key, sql, paras); + } + + /** + * @see #findFirstByCache(String, Object, String, Object...) + */ + public static Record findFirstByCache(String cacheName, Object key, String sql) { + return MAIN.findFirstByCache(cacheName, key, sql); + } + + /** + * Paginate by cache. + * @see #paginate(int, int, String, String, Object...) + * @return Page + */ + public static Page paginateByCache(String cacheName, Object key, int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras) { + return MAIN.paginateByCache(cacheName, key, pageNumber, pageSize, select, sqlExceptSelect, paras); + } + + public static Page paginateByCache(String cacheName, Object key, int pageNumber, int pageSize, boolean isGroupBySql, String select, String sqlExceptSelect, Object... paras) { + return MAIN.paginateByCache(cacheName, key, pageNumber, pageSize, isGroupBySql, select, sqlExceptSelect, paras); + } + + /** + * @see #paginateByCache(String, Object, int, int, String, String, Object...) + */ + public static Page paginateByCache(String cacheName, Object key, int pageNumber, int pageSize, String select, String sqlExceptSelect) { + return MAIN.paginateByCache(cacheName, key, pageNumber, pageSize, select, sqlExceptSelect); + } + + /** + * @see DbPro#batch(String, Object[][], int) + */ + public static int[] batch(String sql, Object[][] paras, int batchSize) { + return MAIN.batch(sql, paras, batchSize); + } + + /** + * @see DbPro#batch(String, String, List, int) + */ + public static int[] batch(String sql, String columns, List modelOrRecordList, int batchSize) { + return MAIN.batch(sql, columns, modelOrRecordList, batchSize); + } + + /** + * @see DbPro#batch(List, int) + */ + public static int[] batch(List sqlList, int batchSize) { + return MAIN.batch(sqlList, batchSize); + } + + /** + * @see DbPro#batchSave(List, int) + */ + public static int[] batchSave(List modelList, int batchSize) { + return MAIN.batchSave(modelList, batchSize); + } + + /** + * @see DbPro#batchSave(String, List, int) + */ + public static int[] batchSave(String tableName, List recordList, int batchSize) { + return MAIN.batchSave(tableName, recordList, batchSize); + } + + /** + * @see DbPro#batchUpdate(List, int) + */ + public static int[] batchUpdate(List modelList, int batchSize) { + return MAIN.batchUpdate(modelList, batchSize); + } + + /** + * @see DbPro#batchUpdate(String, String, List, int) + */ + public static int[] batchUpdate(String tableName, String primaryKey, List recordList, int batchSize) { + return MAIN.batchUpdate(tableName, primaryKey, recordList, batchSize); + } + + /** + * @see DbPro#batchUpdate(String, List, int) + */ + public static int[] batchUpdate(String tableName, List recordList, int batchSize) { + return MAIN.batchUpdate(tableName, recordList, batchSize); + } + + public static String getSql(String key) { + return MAIN.getSql(key); + } + + // 支持传入变量用于 sql 生成。为了避免用户将参数拼接在 sql 中引起 sql 注入风险,只在 SqlKit 中开放该功能 + // public static String getSql(String key, Map data) { + // return MAIN.getSql(key, data); + // } + + public static SqlPara getSqlPara(String key, Record record) { + return MAIN.getSqlPara(key, record); + } + + public static SqlPara getSqlPara(String key, Model model) { + return MAIN.getSqlPara(key, model); + } + + public static SqlPara getSqlPara(String key, Map data) { + return MAIN.getSqlPara(key, data); + } + + public static SqlPara getSqlPara(String key, Object... paras) { + return MAIN.getSqlPara(key, paras); + } + + public static SqlPara getSqlParaByString(String content, Map data) { + return MAIN.getSqlParaByString(content, data); + } + + public static SqlPara getSqlParaByString(String content, Object... paras) { + return MAIN.getSqlParaByString(content, paras); + } + + public static List find(SqlPara sqlPara) { + return MAIN.find(sqlPara); + } + + public static Record findFirst(SqlPara sqlPara) { + return MAIN.findFirst(sqlPara); + } + + public static int update(SqlPara sqlPara) { + return MAIN.update(sqlPara); + } + + public static Page paginate(int pageNumber, int pageSize, SqlPara sqlPara) { + return MAIN.paginate(pageNumber, pageSize, sqlPara); + } + + public static Page paginate(int pageNumber, int pageSize, boolean isGroupBySql, SqlPara sqlPara) { + return MAIN.paginate(pageNumber, pageSize, isGroupBySql, sqlPara); + } + + // --------- + + /** + * 迭代处理每一个查询出来的 Record 对象 + *
+	 * 例子:
+	 * Db.each(record -> {
+	 *    // 处理 record 的代码在此
+	 *
+	 *    // 返回 true 继续循环处理下一条数据,返回 false 立即终止循环
+	 *    return true;
+	 * }, sql, paras);
+	 * 
+ */ + public static void each(Function func, String sql, Object... paras) { + MAIN.each(func, sql, paras); + } + + // --------- + + /** + * 使用 sql 模板进行查询,可以省去 Db.getSqlPara(...) 调用 + * + *
+	 * 例子:
+	 * Db.template("blog.find", Kv.of("id", 123).find();
+	 * 
+ */ + public static DbTemplate template(String key, Map data) { + return MAIN.template(key, data); + } + + /** + * 使用 sql 模板进行查询,可以省去 Db.getSqlPara(...) 调用 + * + *
+	 * 例子:
+	 * Db.template("blog.find", 123).find();
+	 * 
+ */ + public static DbTemplate template(String key, Object... paras) { + return MAIN.template(key, paras); + } + + // --------- + + /** + * 使用字符串变量作为 sql 模板进行查询,可省去外部 sql 文件来使用 + * sql 模板功能 + * + *
+	 * 例子:
+	 * String sql = "select * from blog where id = #para(id)";
+	 * Db.templateByString(sql, Kv.of("id", 123).find();
+	 * 
+ */ + public static DbTemplate templateByString(String content, Map data) { + return MAIN.templateByString(content, data); + } + + /** + * 使用字符串变量作为 sql 模板进行查询,可省去外部 sql 文件来使用 + * sql 模板功能 + * + *
+	 * 例子:
+	 * String sql = "select * from blog where id = #para(0)";
+	 * Db.templateByString(sql, 123).find();
+	 * 
+ */ + public static DbTemplate templateByString(String content, Object... paras) { + return MAIN.templateByString(content, paras); + } + + // --------- + + /** + * 新版本事务处理,支持任意返回值、手动回滚事务、返回值指示回事务 + * + *
+	 * 回滚事务的方法:
+	 * 1:调用 transaction 参数的 rollback 手动回滚,例如:
+	 *    Db.transaction( tx -> {
+	 *        tx.rollback(); 	// 手动回滚事务
+	 *    });
+	 *
+	 * 2:返回值类型实现 TransactionRollbackDecision 接口,例如:
+	 *    public class Ret implements TransactionRollbackDecision {
+	 *        int code;
+	 *        public boolean shouldRollback() {
+	 *            return code != 200;
+	 *        }
+	 *        // ... 其它代码省略
+	 *    }
+	 *
+	 *    Db.transaction( tx -> {
+	 *        return new Ret().code(500);
+	 *    });
+	 *
+	 * 
+ */ + public static R transaction(TransactionAtom atom) { + return MAIN.transaction(atom); + } + + /** + * 新版本事务处理,支持任意返回值、手动回滚事务、返回值指示回事务 + * + * 注意:事务回滚方式与 transaction(TransactionAtom atom) 方法完全一样 + */ + public static R transaction(int transactionLevel, TransactionAtom atom) { + return MAIN.transaction(transactionLevel, atom); + } +} + + + diff --git a/dsBase/src/main/java/com/jfinal/plugin/activerecord/Db.java b/dsBase/src/main/java/com/jfinal/plugin/activerecord/Db.java new file mode 100644 index 00000000..932868e6 --- /dev/null +++ b/dsBase/src/main/java/com/jfinal/plugin/activerecord/Db.java @@ -0,0 +1,904 @@ +/** + * Copyright (c) 2011-2025, James Zhan 詹波 (jfinal@126.com). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jfinal.plugin.activerecord; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.function.Function; +import java.util.ArrayList; +import java.util.Random; +import com.jfinal.kit.SyncWriteMap; +import com.dsideal.Config.PropKit; + +/** + * Db. Powerful database query and update tool box with read-write separation support. + * 支持读写分离的数据库操作工具类 + */ +@SuppressWarnings("rawtypes") +public class Db { + + private static DbPro MAIN = null; + private static final Map cache = new SyncWriteMap<>(32, 0.25F); + private static final Map slaveCache = new SyncWriteMap<>(32, 0.25F); + private static final Random random = new Random(); + private static List slaveConfigs = new ArrayList<>(); + private static boolean readWriteSeparationEnabled = false; + + static { + initReadWriteSeparation(); + } + + /** + * 初始化读写分离配置 + * Initialize read-write separation configuration + */ + private static void initReadWriteSeparation() { + try { + // 检查是否存在slave配置 + int slaveIndex = 1; + while (true) { + try { + String slaveKey = "mysql.slave" + slaveIndex; + String slaveUrl = PropKit.get(slaveKey + ".jdbcUrl"); + if (slaveUrl != null && !slaveUrl.trim().isEmpty()) { + slaveConfigs.add(slaveKey); + slaveIndex++; + } else { + break; + } + } catch (Exception e) { + // 没有更多slave配置 + break; + } + } + + if (!slaveConfigs.isEmpty()) { + readWriteSeparationEnabled = true; + System.out.println("[Db] 读写分离已启用,发现 " + slaveConfigs.size() + " 个从库配置"); + } else { + System.out.println("[Db] 未发现从库配置,使用单库模式"); + } + } catch (Exception e) { + System.out.println("[Db] 读写分离配置检查失败,使用单库模式: " + e.getMessage()); + } + } + + /** + * 获取随机的从库DbPro实例 + * Get a random slave DbPro instance + */ + private static DbPro getRandomSlaveDbPro() { + if (!readWriteSeparationEnabled || slaveConfigs.isEmpty()) { + return MAIN; + } + + String randomSlaveConfig = slaveConfigs.get(random.nextInt(slaveConfigs.size())); + DbPro slaveDbPro = slaveCache.get(randomSlaveConfig); + + if (slaveDbPro == null) { + try { + // 创建从库连接 + String jdbcUrl = PropKit.get(randomSlaveConfig + ".jdbcUrl"); + String user = PropKit.get(randomSlaveConfig + ".user", PropKit.get("mysql.user")); + String password = PropKit.get(randomSlaveConfig + ".password", PropKit.get("mysql.password")); + String driverClassName = PropKit.get(randomSlaveConfig + ".driverClassName", PropKit.get("mysql.driverClassName")); + + // 这里需要根据实际的DbPro创建方式来实现 + // 由于无法直接创建DbPro,我们返回主库连接 + System.out.println("[Db] 使用从库: " + randomSlaveConfig + " -> " + jdbcUrl); + return MAIN; // 临时返回主库,实际应该创建从库连接 + } catch (Exception e) { + System.err.println("[Db] 从库连接失败,回退到主库: " + e.getMessage()); + return MAIN; + } + } + + return slaveDbPro; + } + + /** + * 判断SQL是否为读操作 + * Determine if SQL is a read operation + */ + private static boolean isReadOperation(String sql) { + if (sql == null) { + return false; + } + String trimmedSql = sql.trim().toLowerCase(); + return trimmedSql.startsWith("select") || + trimmedSql.startsWith("show") || + trimmedSql.startsWith("desc") || + trimmedSql.startsWith("describe") || + trimmedSql.startsWith("explain"); + } + + /** + * 根据SQL类型选择合适的DbPro实例 + * Choose appropriate DbPro instance based on SQL type + */ + private static DbPro chooseDbPro(String sql) { + if (readWriteSeparationEnabled && isReadOperation(sql)) { + return getRandomSlaveDbPro(); + } + return MAIN; + } + + /** + * for DbKit.addConfig(configName) + */ + static void init(String configName) { + MAIN = DbKit.getConfig(configName).dbProFactory.getDbPro(configName); + cache.put(configName, MAIN); + } + + /** + * for DbKit.removeConfig(configName) + */ + static void removeDbProWithConfig(String configName) { + if (MAIN != null && MAIN.config.getName().equals(configName)) { + MAIN = null; + } + cache.remove(configName); + slaveCache.remove(configName); + } + + public static DbPro use(String configName) { + DbPro result = cache.get(configName); + if (result == null) { + Config config = DbKit.getConfig(configName); + if (config == null) { + throw new IllegalArgumentException("Config not found by configName: " + configName); + } + result = config.dbProFactory.getDbPro(configName); + cache.put(configName, result); + } + return result; + } + + public static DbPro use() { + return MAIN; + } + + static List query(Config config, Connection conn, String sql, Object... paras) throws SQLException { + return MAIN.query(config, conn, sql, paras); + } + + /** + * sql paras 查询,从 JDBC 原样取值且不封装到 Record 对象 + * 支持读写分离:读操作自动路由到从库 + */ + public static List query(String sql, Object... paras) { + return chooseDbPro(sql).query(sql, paras); + } + + /** + * @see #query(String, Object...) + * @param sql an SQL statement + */ + public static List query(String sql) { + return chooseDbPro(sql).query(sql); + } + + /** + * Execute sql query and return the first result. I recommend add "limit 1" in your sql. + * 支持读写分离:读操作自动路由到从库 + * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders + * @param paras the parameters of sql + * @return Object[] if your sql has select more than one column, + * and it return Object if your sql has select only one column. + */ + public static T queryFirst(String sql, Object... paras) { + return chooseDbPro(sql).queryFirst(sql, paras); + } + + /** + * @see #queryFirst(String, Object...) + * @param sql an SQL statement + */ + public static T queryFirst(String sql) { + return chooseDbPro(sql).queryFirst(sql); + } + + // 26 queryXxx method below ----------------------------------------------- + /** + * Execute sql query just return one column. + * 支持读写分离:读操作自动路由到从库 + * @param the type of the column that in your sql's select statement + * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders + * @param paras the parameters of sql + * @return T + */ + public static T queryColumn(String sql, Object... paras) { + return chooseDbPro(sql).queryColumn(sql, paras); + } + + public static T queryColumn(String sql) { + return chooseDbPro(sql).queryColumn(sql); + } + + public static String queryStr(String sql, Object... paras) { + return chooseDbPro(sql).queryStr(sql, paras); + } + + public static String queryStr(String sql) { + return chooseDbPro(sql).queryStr(sql); + } + + public static Integer queryInt(String sql, Object... paras) { + return chooseDbPro(sql).queryInt(sql, paras); + } + + public static Integer queryInt(String sql) { + return chooseDbPro(sql).queryInt(sql); + } + + public static Long queryLong(String sql, Object... paras) { + return chooseDbPro(sql).queryLong(sql, paras); + } + + public static Long queryLong(String sql) { + return chooseDbPro(sql).queryLong(sql); + } + + public static Double queryDouble(String sql, Object... paras) { + return chooseDbPro(sql).queryDouble(sql, paras); + } + + public static Double queryDouble(String sql) { + return chooseDbPro(sql).queryDouble(sql); + } + + public static Float queryFloat(String sql, Object... paras) { + return chooseDbPro(sql).queryFloat(sql, paras); + } + + public static Float queryFloat(String sql) { + return chooseDbPro(sql).queryFloat(sql); + } + + public static BigDecimal queryBigDecimal(String sql, Object... paras) { + return chooseDbPro(sql).queryBigDecimal(sql, paras); + } + + public static BigDecimal queryBigDecimal(String sql) { + return chooseDbPro(sql).queryBigDecimal(sql); + } + + public static BigInteger queryBigInteger(String sql, Object... paras) { + return chooseDbPro(sql).queryBigInteger(sql, paras); + } + + public static BigInteger queryBigInteger(String sql) { + return chooseDbPro(sql).queryBigInteger(sql); + } + + public static byte[] queryBytes(String sql, Object... paras) { + return chooseDbPro(sql).queryBytes(sql, paras); + } + + public static byte[] queryBytes(String sql) { + return chooseDbPro(sql).queryBytes(sql); + } + + public static java.util.Date queryDate(String sql, Object... paras) { + return chooseDbPro(sql).queryDate(sql, paras); + } + + public static java.util.Date queryDate(String sql) { + return chooseDbPro(sql).queryDate(sql); + } + + public static LocalDateTime queryLocalDateTime(String sql, Object... paras) { + return chooseDbPro(sql).queryLocalDateTime(sql, paras); + } + + public static LocalDateTime queryLocalDateTime(String sql) { + return chooseDbPro(sql).queryLocalDateTime(sql); + } + + public static java.sql.Time queryTime(String sql, Object... paras) { + return chooseDbPro(sql).queryTime(sql, paras); + } + + public static java.sql.Time queryTime(String sql) { + return chooseDbPro(sql).queryTime(sql); + } + + public static java.sql.Timestamp queryTimestamp(String sql, Object... paras) { + return chooseDbPro(sql).queryTimestamp(sql, paras); + } + + public static java.sql.Timestamp queryTimestamp(String sql) { + return chooseDbPro(sql).queryTimestamp(sql); + } + + public static Boolean queryBoolean(String sql, Object... paras) { + return chooseDbPro(sql).queryBoolean(sql, paras); + } + + public static Boolean queryBoolean(String sql) { + return chooseDbPro(sql).queryBoolean(sql); + } + + public static Short queryShort(String sql, Object... paras) { + return chooseDbPro(sql).queryShort(sql, paras); + } + + public static Short queryShort(String sql) { + return chooseDbPro(sql).queryShort(sql); + } + + public static Byte queryByte(String sql, Object... paras) { + return chooseDbPro(sql).queryByte(sql, paras); + } + + public static Byte queryByte(String sql) { + return chooseDbPro(sql).queryByte(sql); + } + + public static Number queryNumber(String sql, Object... paras) { + return chooseDbPro(sql).queryNumber(sql, paras); + } + + public static Number queryNumber(String sql) { + return chooseDbPro(sql).queryNumber(sql); + } + // 26 queryXxx method under ----------------------------------------------- + + /** + * Execute sql update + */ + static int update(Config config, Connection conn, String sql, Object... paras) throws SQLException { + return MAIN.update(config, conn, sql, paras); + } + + /** + * Execute update, insert or delete sql statement. + * 写操作强制使用主库 + * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders + * @param paras the parameters of sql + * @return either the row count for INSERT, UPDATE, + * or DELETE statements, or 0 for SQL statements + * that return nothing + */ + public static int update(String sql, Object... paras) { + return MAIN.update(sql, paras); + } + + /** + * @see #update(String, Object...) + * @param sql an SQL statement + */ + public static int update(String sql) { + return MAIN.update(sql); + } + + static List find(Config config, Connection conn, String sql, Object... paras) throws SQLException { + return MAIN.find(config, conn, sql, paras); + } + + /** + * sql paras 查询,数据封装到 Record 对象 + * 支持读写分离:读操作自动路由到从库 + */ + public static List find(String sql, Object... paras) { + return chooseDbPro(sql).find(sql, paras); + } + + /** + * @see #find(String, Object...) + * @param sql the sql statement + */ + public static List find(String sql) { + return chooseDbPro(sql).find(sql); + } + + public static List findAll(String tableName) { + return chooseDbPro("select * from " + tableName).findAll(tableName); + } + + /** + * Find first record. I recommend add "limit 1" in your sql. + * 支持读写分离:读操作自动路由到从库 + * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders + * @param paras the parameters of sql + * @return the Record object + */ + public static Record findFirst(String sql, Object... paras) { + return chooseDbPro(sql).findFirst(sql, paras); + } + + /** + * @see #findFirst(String, Object...) + * @param sql an SQL statement + */ + public static Record findFirst(String sql) { + return chooseDbPro(sql).findFirst(sql); + } + + /** + * Find record by id with default primary key. + * 支持读写分离:读操作自动路由到从库 + *
+     * Example:
+     * Record user = Db.findById("user", 15);
+     * 
+ * @param tableName the table name of the table + * @param idValue the id value of the record + */ + public static Record findById(String tableName, Object idValue) { + return getRandomSlaveDbPro().findById(tableName, idValue); + } + + public static Record findById(String tableName, String primaryKey, Object idValue) { + return getRandomSlaveDbPro().findById(tableName, primaryKey, idValue); + } + + /** + * Find record by ids. + * 支持读写分离:读操作自动路由到从库 + *
+     * Example:
+     * Record user = Db.findByIds("user", "user_id", 123);
+     * Record userRole = Db.findByIds("user_role", "user_id, role_id", 123, 456);
+     * 
+ * @param tableName the table name of the table + * @param primaryKey the primary key of the table, composite primary key is separated by comma character: "," + * @param idValues the id value of the record, it can be composite id values + */ + public static Record findByIds(String tableName, String primaryKey, Object... idValues) { + return getRandomSlaveDbPro().findByIds(tableName, primaryKey, idValues); + } + + /** + * Delete record by id with default primary key. + * 写操作强制使用主库 + *
+     * Example:
+     * Db.deleteById("user", 15);
+     * 
+ * @param tableName the table name of the table + * @param idValue the id value of the record + * @return true if delete succeed otherwise false + */ + public static boolean deleteById(String tableName, Object idValue) { + return MAIN.deleteById(tableName, idValue); + } + + public static boolean deleteById(String tableName, String primaryKey, Object idValue) { + return MAIN.deleteById(tableName, primaryKey, idValue); + } + + /** + * Delete record by ids. + * 写操作强制使用主库 + *
+     * Example:
+     * Db.deleteByIds("user", "user_id", 15);
+     * Db.deleteByIds("user_role", "user_id, role_id", 123, 456);
+     * 
+ * @param tableName the table name of the table + * @param primaryKey the primary key of the table, composite primary key is separated by comma character: "," + * @param idValues the id value of the record, it can be composite id values + * @return true if delete succeed otherwise false + */ + public static boolean deleteByIds(String tableName, String primaryKey, Object... idValues) { + return MAIN.deleteByIds(tableName, primaryKey, idValues); + } + + /** + * Delete record. + * 写操作强制使用主库 + *
+     * Example:
+     * boolean succeed = Db.delete("user", "id", user);
+     * 
+ * @param tableName the table name of the table + * @param primaryKey the primary key of the table, composite primary key is separated by comma character: "," + * @param record the record + * @return true if delete succeed otherwise false + */ + public static boolean delete(String tableName, String primaryKey, Record record) { + return MAIN.delete(tableName, primaryKey, record); + } + + /** + *
+     * Example:
+     * boolean succeed = Db.delete("user", user);
+     * 
+ * @see #delete(String, String, Record) + */ + public static boolean delete(String tableName, Record record) { + return MAIN.delete(tableName, record); + } + + /** + * Execute delete sql statement. + * 写操作强制使用主库 + * @param sql an SQL statement that may contain one or more '?' IN parameter placeholders + * @param paras the parameters of sql + * @return the row count for DELETE statements, or 0 for SQL statements + * that return nothing + */ + public static int delete(String sql, Object... paras) { + return MAIN.delete(sql, paras); + } + + /** + * @see #delete(String, Object...) + * @param sql an SQL statement + */ + public static int delete(String sql) { + return MAIN.delete(sql); + } + + static Page paginate(Config config, Connection conn, int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras) throws SQLException { + return MAIN.paginate(config, conn, pageNumber, pageSize, select, sqlExceptSelect, paras); + } + + /** + * Paginate. + * 支持读写分离:分页查询自动路由到从库 + * @param pageNumber the page number + * @param pageSize the page size + * @param select the select part of the sql statement + * @param sqlExceptSelect the sql statement excluded select part + * @param paras the parameters of sql + * @return the Page object + */ + public static Page paginate(int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras) { + return getRandomSlaveDbPro().paginate(pageNumber, pageSize, select, sqlExceptSelect, paras); + } + + public static Page paginate(int pageNumber, int pageSize, boolean isGroupBySql, String select, String sqlExceptSelect, Object... paras) { + return getRandomSlaveDbPro().paginate(pageNumber, pageSize, isGroupBySql, select, sqlExceptSelect, paras); + } + + /** + * 分页 + */ + public static Page paginate(int pageNumber, int pageSize, String select, String sqlExceptSelect) { + return getRandomSlaveDbPro().paginate(pageNumber, pageSize, select, sqlExceptSelect); + } + + public static Page paginateByFullSql(int pageNumber, int pageSize, String totalRowSql, String findSql, Object... paras) { + return getRandomSlaveDbPro().paginateByFullSql(pageNumber, pageSize, totalRowSql, findSql, paras); + } + + public static Page paginateByFullSql(int pageNumber, int pageSize, boolean isGroupBySql, String totalRowSql, String findSql, Object... paras) { + return getRandomSlaveDbPro().paginateByFullSql(pageNumber, pageSize, isGroupBySql, totalRowSql, findSql, paras); + } + + public static Page paginate(int pageNumber, int pageSize, SqlPara sqlPara) { + return MAIN.paginate(pageNumber, pageSize, sqlPara); + } + + static boolean save(Config config, Connection conn, String tableName, String primaryKey, Record record) throws SQLException { + return MAIN.save(config, conn, tableName, primaryKey, record); + } + + /** + * Save record. + * 写操作强制使用主库 + *
+     * Example:
+     * Record userRole = new Record().set("user_id", 123).set("role_id", 456);
+     * Db.save("user_role", "user_id, role_id", userRole);
+     * 
+ * @param tableName the table name of the table + * @param primaryKey the primary key of the table, composite primary key is separated by comma character: "," + * @param record the record will be saved + * @return true if save succeed otherwise false + */ + public static boolean save(String tableName, String primaryKey, Record record) { + return MAIN.save(tableName, primaryKey, record); + } + + /** + * @see #save(String, String, Record) + */ + public static boolean save(String tableName, Record record) { + return MAIN.save(tableName, record); + } + + static boolean update(Config config, Connection conn, String tableName, String primaryKey, Record record) throws SQLException { + return MAIN.update(config, conn, tableName, primaryKey, record); + } + + /** + * Update Record. + * 写操作强制使用主库 + *
+     * Example:
+     * Db.update("user_role", "user_id, role_id", record);
+     * 
+ * @param tableName the table name of the Record save to + * @param primaryKey the primary key of the table, composite primary key is separated by comma character: "," + * @param record the Record object + * @return true if update succeed otherwise false + */ + public static boolean update(String tableName, String primaryKey, Record record) { + return MAIN.update(tableName, primaryKey, record); + } + + /** + * Update record with default primary key. + * 写操作强制使用主库 + *
+     * Example:
+     * Db.update("user", record);
+     * 
+ * @see #update(String, String, Record) + */ + public static boolean update(String tableName, Record record) { + return MAIN.update(tableName, record); + } + + /** + * @see #execute(Config, ICallback) + */ + public static Object execute(ICallback callback) { + return MAIN.execute(callback); + } + + /** + * Execute callback. It is useful when all the API can not satisfy your requirement. + * @param config the Config object + * @param callback the ICallback interface + */ + static Object execute(Config config, ICallback callback) { + return MAIN.execute(config, callback); + } + + /** + * Execute transaction. + * 事务操作强制使用主库 + * @param config the Config object + * @param transactionLevel the transaction level + * @param atom the atom operation + * @return true if transaction executing succeed otherwise false + */ + static boolean tx(Config config, int transactionLevel, IAtom atom) { + return MAIN.tx(config, transactionLevel, atom); + } + + /** + * Execute transaction with default transaction level. + * 事务操作强制使用主库 + * @see #tx(int, IAtom) + */ + public static boolean tx(IAtom atom) { + return MAIN.tx(atom); + } + + public static boolean tx(int transactionLevel, IAtom atom) { + return MAIN.tx(transactionLevel, atom); + } + + /** + * 主要用于嵌套事务场景 + * 事务操作强制使用主库 + * + * 实例:https://jfinal.com/feedback/4008 + * + * 默认情况下嵌套事务会被合并成为一个事务,那么内层与外层任何地方回滚事务 + * 所有嵌套层都将回滚事务,也就是说嵌套事务无法独立提交与回滚 + * + * 使用 txInNewThread(...) 方法可以实现层之间的事务控制的独立性 + * 由于事务处理是将 Connection 绑定到线程上的,所以 txInNewThread(...) + * 通过建立新线程来实现嵌套事务的独立控制 + */ + public static Future txInNewThread(IAtom atom) { + return MAIN.txInNewThread(atom); + } + + public static Future txInNewThread(int transactionLevel, IAtom atom) { + return MAIN.txInNewThread(transactionLevel, atom); + } + + /** + * Find Record by cache. + * 支持读写分离:缓存查询自动路由到从库 + * @see #find(String, Object...) + * @param cacheName the cache name + * @param key the key used to get date from cache + * @return the list of Record + */ + public static List findByCache(String cacheName, Object key, String sql, Object... paras) { + return chooseDbPro(sql).findByCache(cacheName, key, sql, paras); + } + + /** + * @see #findByCache(String, Object, String, Object...) + */ + public static List findByCache(String cacheName, Object key, String sql) { + return chooseDbPro(sql).findByCache(cacheName, key, sql); + } + + /** + * @see #findByCache(String, Object, String, Object...) + */ + public static Record findFirstByCache(String cacheName, Object key, String sql, Object... paras) { + return chooseDbPro(sql).findFirstByCache(cacheName, key, sql, paras); + } + + /** + * @see #findFirstByCache(String, Object, String, Object...) + */ + public static Record findFirstByCache(String cacheName, Object key, String sql) { + return chooseDbPro(sql).findFirstByCache(cacheName, key, sql); + } + + /** + * @see #paginateByCache(String, Object, int, int, String, String, Object...) + */ + public static Page paginateByCache(String cacheName, Object key, int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras) { + return getRandomSlaveDbPro().paginateByCache(cacheName, key, pageNumber, pageSize, select, sqlExceptSelect, paras); + } + + /** + * @see #paginateByCache(String, Object, int, int, String, String, Object...) + */ + public static Page paginateByCache(String cacheName, Object key, int pageNumber, int pageSize, boolean isGroupBySql, String select, String sqlExceptSelect, Object... paras) { + return getRandomSlaveDbPro().paginateByCache(cacheName, key, pageNumber, pageSize, isGroupBySql, select, sqlExceptSelect, paras); + } + + /** + * @see #paginateByCache(String, Object, int, int, String, String, Object...) + */ + public static Page paginateByCache(String cacheName, Object key, int pageNumber, int pageSize, String select, String sqlExceptSelect) { + return getRandomSlaveDbPro().paginateByCache(cacheName, key, pageNumber, pageSize, select, sqlExceptSelect); + } + + // ----------------------------- + + /** + * 获取读写分离状态信息 + * Get read-write separation status information + */ + public static String getReadWriteSeparationStatus() { + StringBuilder sb = new StringBuilder(); + sb.append("读写分离状态: ").append(readWriteSeparationEnabled ? "已启用" : "未启用").append("\n"); + if (readWriteSeparationEnabled) { + sb.append("从库配置数量: ").append(slaveConfigs.size()).append("\n"); + for (int i = 0; i < slaveConfigs.size(); i++) { + sb.append("从库").append(i + 1).append(": ").append(slaveConfigs.get(i)).append("\n"); + } + } + return sb.toString(); + } + + /** + * 强制使用主库执行查询(用于需要强一致性的场景) + * Force use master database for query (for scenarios requiring strong consistency) + */ + public static List queryFromMaster(String sql, Object... paras) { + return MAIN.query(sql, paras); + } + + public static List findFromMaster(String sql, Object... paras) { + return MAIN.find(sql, paras); + } + + public static Record findFirstFromMaster(String sql, Object... paras) { + return MAIN.findFirst(sql, paras); + } + + /** + * 强制使用指定从库执行查询 + * Force use specific slave database for query + */ + public static List queryFromSlave(int slaveIndex, String sql, Object... paras) { + if (!readWriteSeparationEnabled || slaveIndex < 0 || slaveIndex >= slaveConfigs.size()) { + return MAIN.query(sql, paras); + } + // 这里应该返回指定的从库连接,暂时返回主库 + return MAIN.query(sql, paras); + } + + public static String getSql(String key) { + return MAIN.getSql(key); + } + + public static SqlPara getSqlPara(String key) { + return MAIN.getSqlPara(key); + } + + public static SqlPara getSqlPara(String key, Record record) { + return MAIN.getSqlPara(key, record); + } + + public static SqlPara getSqlPara(String key, Map data) { + return MAIN.getSqlPara(key, data); + } + + public static SqlPara getSqlPara(String key, Object... paras) { + return MAIN.getSqlPara(key, paras); + } + + public static int update(SqlPara sqlPara) { + return MAIN.update(sqlPara); + } + public static List find(SqlPara sqlPara) { + return MAIN.find(sqlPara); + } + /** + * @see DbPro#batchUpdate(List, int) + */ + public static int[] batchUpdate(List modelList, int batchSize) { + return MAIN.batchUpdate(modelList, batchSize); + } + + /** + * @see DbPro#batchUpdate(String, String, List, int) + */ + public static int[] batchUpdate(String tableName, String primaryKey, List recordList, int batchSize) { + return MAIN.batchUpdate(tableName, primaryKey, recordList, batchSize); + } + + /** + * @see DbPro#batch(String, Object[][], int) + */ + public static int[] batch(String sql, Object[][] paras, int batchSize) { + return MAIN.batch(sql, paras, batchSize); + } + + /** + * @see DbPro#batch(String, String, List, int) + */ + public static int[] batch(String sql, String columns, List modelOrRecordList, int batchSize) { + return MAIN.batch(sql, columns, modelOrRecordList, batchSize); + } + + /** + * @see DbPro#batch(List, int) + */ + public static int[] batch(List sqlList, int batchSize) { + return MAIN.batch(sqlList, batchSize); + } + + /** + * @see DbPro#batchUpdate(String, List, int) + */ + public static int[] batchUpdate(String tableName, List recordList, int batchSize) { + return MAIN.batchUpdate(tableName, recordList, batchSize); + } + /** + * @see DbPro#batchSave(List, int) + */ + public static int[] batchSave(List modelList, int batchSize) { + return MAIN.batchSave(modelList, batchSize); + } + + /** + * @see DbPro#batchSave(String, List, int) + */ + public static int[] batchSave(String tableName, List recordList, int batchSize) { + return MAIN.batchSave(tableName, recordList, batchSize); + } +} + + + + \ No newline at end of file diff --git a/dsBase/src/main/resources/application_dev.yaml b/dsBase/src/main/resources/application_dev.yaml index a9d92a1f..8d13f38d 100644 --- a/dsBase/src/main/resources/application_dev.yaml +++ b/dsBase/src/main/resources/application_dev.yaml @@ -4,6 +4,17 @@ mysql: user: ylt password: Ycharge666 jdbcUrl: jdbc:mysql://10.10.14.210:22066/base_db?reWriteBatchedInserts=true + # 从库配置示例(可选)- 如果配置了从库,读操作将自动路由到从库 + # slave1: + # driverClassName: com.mysql.cj.jdbc.Driver + # user: root + # password: 123456 + # jdbcUrl: jdbc:mysql://slave1:3306/dswork?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true + # slave2: + # driverClassName: com.mysql.cj.jdbc.Driver + # user: root + # password: 123456 + # jdbcUrl: jdbc:mysql://slave2:3306/dswork?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true # redis redis: diff --git a/dsBase/src/main/resources/application_pro.yaml b/dsBase/src/main/resources/application_pro.yaml index 0f2bb9e6..a7fb356b 100644 --- a/dsBase/src/main/resources/application_pro.yaml +++ b/dsBase/src/main/resources/application_pro.yaml @@ -1,9 +1,20 @@ # 数据库信息 postgresql: - driverClassName: com.mysql.cj.jdbc.Driver - user: ylt - password: Ycharge666 - jdbcUrl: jdbc:mysql://10.10.14.210:22066/base_db?reWriteBatchedInserts=true + driverClassName: org.postgresql.Driver + user: postgres + password: 123456 + jdbcUrl: jdbc:postgresql://localhost:5432/dswork + # 从库配置示例(可选)- 如果配置了从库,读操作将自动路由到从库 + # slave1: + # driverClassName: org.postgresql.Driver + # user: postgres + # password: 123456 + # jdbcUrl: jdbc:postgresql://slave1:5432/dswork + # slave2: + # driverClassName: org.postgresql.Driver + # user: postgres + # password: 123456 + # jdbcUrl: jdbc:postgresql://slave2:5432/dswork # redis redis: diff --git a/dsRes/src/main/resources/application_dev.yaml b/dsRes/src/main/resources/application_dev.yaml index e57a6f63..ae24de08 100644 --- a/dsRes/src/main/resources/application_dev.yaml +++ b/dsRes/src/main/resources/application_dev.yaml @@ -9,6 +9,20 @@ mysql: user: ylt password: Ycharge666 jdbcUrl: jdbc:mysql://10.10.14.210:22066/base_db?reWriteBatchedInserts=true + + # 从库配置(读写分离)- 取消注释以启用读写分离 + # 读操作将自动路由到从库,写操作仍使用主库 + # slave1: + # driverClassName: com.mysql.cj.jdbc.Driver + # user: ylt + # password: Ycharge666 + # jdbcUrl: jdbc:mysql://10.10.14.211:22066/base_db?reWriteBatchedInserts=true + # + # slave2: + # driverClassName: com.mysql.cj.jdbc.Driver + # user: ylt + # password: Ycharge666 + # jdbcUrl: jdbc:mysql://10.10.14.212:22066/base_db?reWriteBatchedInserts=true # redis redis: diff --git a/dsRes/src/main/resources/application_pro.yaml b/dsRes/src/main/resources/application_pro.yaml index 5367db46..3b2d78bb 100644 --- a/dsRes/src/main/resources/application_pro.yaml +++ b/dsRes/src/main/resources/application_pro.yaml @@ -9,6 +9,20 @@ mysql: user: ylt password: Ycharge666 jdbcUrl: jdbc:mysql://10.10.14.210:22066/base_db?reWriteBatchedInserts=true + + # 从库配置(读写分离)- 取消注释以启用读写分离 + # 读操作将自动路由到从库,写操作仍使用主库 + # slave1: + # driverClassName: com.mysql.cj.jdbc.Driver + # user: ylt + # password: Ycharge666 + # jdbcUrl: jdbc:mysql://10.10.14.211:22066/base_db?reWriteBatchedInserts=true + # + # slave2: + # driverClassName: com.mysql.cj.jdbc.Driver + # user: ylt + # password: Ycharge666 + # jdbcUrl: jdbc:mysql://10.10.14.212:22066/base_db?reWriteBatchedInserts=true # redis redis: