Commit a38811c937b36e2afba97b27087a612a1106108e
1 parent
7f53d60e
fix: 完善多数据库统计查询与限量清理兼容
按执行数据源选择统计日期、分页及分批删除语法。 统一不计数的限量查询入口并迁移 Lambda 查询,补充按类回归测试。
Showing
60 changed files
with
2267 additions
and
261 deletions
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java
| ... | ... | @@ -15,6 +15,11 @@ import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_ |
| 15 | 15 | public class TenantUtils { |
| 16 | 16 | |
| 17 | 17 | /** |
| 18 | + * 平台共享数据的租户编号;不是未设置租户上下文时的 null | |
| 19 | + */ | |
| 20 | + public static final Long GLOBAL_TENANT_ID = 0L; | |
| 21 | + | |
| 22 | + /** | |
| 18 | 23 | * 使用指定租户,执行对应的逻辑 |
| 19 | 24 | * |
| 20 | 25 | * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 | ... | ... |
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java
| ... | ... | @@ -107,11 +107,19 @@ public enum DbTypeEnum { |
| 107 | 107 | */ |
| 108 | 108 | private final String findInSetTemplate; |
| 109 | 109 | |
| 110 | + /** | |
| 111 | + * 根据数据库产品名获得 MyBatis Plus 类型 | |
| 112 | + * | |
| 113 | + * @param databaseProductName 数据库产品名 | |
| 114 | + * @return MyBatis Plus 类型;产品名为空或未登记时返回 null | |
| 115 | + */ | |
| 110 | 116 | public static DbType find(String databaseProductName) { |
| 111 | 117 | if (StrUtil.isBlank(databaseProductName)) { |
| 112 | 118 | return null; |
| 113 | 119 | } |
| 114 | - return MAP_BY_NAME.get(databaseProductName).getMpDbType(); | |
| 120 | + return Optional.ofNullable(MAP_BY_NAME.get(databaseProductName)) | |
| 121 | + .map(DbTypeEnum::getMpDbType) | |
| 122 | + .orElse(null); | |
| 115 | 123 | } |
| 116 | 124 | |
| 117 | 125 | public static String getFindInSetTemplate(DbType dbType) { | ... | ... |
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
| ... | ... | @@ -216,6 +216,39 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> { |
| 216 | 216 | return CollUtil.getLast(selectList(queryWrapper)); |
| 217 | 217 | } |
| 218 | 218 | |
| 219 | + /** | |
| 220 | + * 获取满足条件的第 1 条记录,见 {@link #selectListLimitN(Wrapper, int)} | |
| 221 | + * | |
| 222 | + * @param queryWrapper 查询条件 | |
| 223 | + * @return 第 1 条;不存在返回 null | |
| 224 | + */ | |
| 225 | + default T selectFirstOne(Wrapper<T> queryWrapper) { | |
| 226 | + return CollUtil.getFirst(selectListLimitN(queryWrapper, 1)); | |
| 227 | + } | |
| 228 | + | |
| 229 | + /** | |
| 230 | + * 获取满足条件的前 limit 条记录 | |
| 231 | + * | |
| 232 | + * 使用 MyBatis Plus 分页插件,按执行时连接的数据库类型生成 LIMIT、ROWNUM、OFFSET FETCH 等语句,并且不执行 COUNT 查询; | |
| 233 | + * 保留 queryWrapper 的排序与 select 投影,不能再通过 last 追加行数限制 | |
| 234 | + * | |
| 235 | + * 注意:SQL Server 的分页语句要求有 ORDER BY,queryWrapper 需要设置排序 | |
| 236 | + * | |
| 237 | + * @param queryWrapper 查询条件 | |
| 238 | + * @param limit 条数,不能为负数;为 0 时不查询,直接返回空列表 | |
| 239 | + * @return 记录列表 | |
| 240 | + */ | |
| 241 | + default List<T> selectListLimitN(Wrapper<T> queryWrapper, int limit) { | |
| 242 | + if (limit < 0) { | |
| 243 | + // 分页插件会把负数的 size 视为不分页,从而查询全表 | |
| 244 | + throw new IllegalArgumentException("limit 不能为负数:" + limit); | |
| 245 | + } | |
| 246 | + if (limit == 0) { | |
| 247 | + return CollUtil.newArrayList(); | |
| 248 | + } | |
| 249 | + return selectPage(MyBatisUtils.<T>buildPage(0, limit, false), queryWrapper).getRecords(); | |
| 250 | + } | |
| 251 | + | |
| 219 | 252 | default Long selectCount() { |
| 220 | 253 | return selectCount(new QueryWrapper<>()); |
| 221 | 254 | } | ... | ... |
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java
| 1 | 1 | package cn.iocoder.yudao.framework.mybatis.core.query; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; | |
| 4 | -import com.baomidou.mybatisplus.annotation.DbType; | |
| 5 | 3 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| 6 | 4 | import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; |
| 7 | 5 | import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; |
| ... | ... | @@ -139,28 +137,4 @@ public class QueryWrapperX<T> extends QueryWrapper<T> { |
| 139 | 137 | return this; |
| 140 | 138 | } |
| 141 | 139 | |
| 142 | - /** | |
| 143 | - * 设置只返回最后一条 | |
| 144 | - * | |
| 145 | - * TODO 芋艿:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同 | |
| 146 | - * | |
| 147 | - * @return this | |
| 148 | - */ | |
| 149 | - public QueryWrapperX<T> limitN(int n) { | |
| 150 | - DbType dbType = JdbcUtils.getDbType(); | |
| 151 | - switch (dbType) { | |
| 152 | - case ORACLE: | |
| 153 | - case ORACLE_12C: | |
| 154 | - super.le("ROWNUM", n); | |
| 155 | - break; | |
| 156 | - case SQL_SERVER: | |
| 157 | - case SQL_SERVER2005: | |
| 158 | - super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段 | |
| 159 | - break; | |
| 160 | - default: // MySQL、PostgreSQL、DM 达梦、KingbaseES 大金都是采用 LIMIT 实现 | |
| 161 | - super.last("LIMIT " + n); | |
| 162 | - } | |
| 163 | - return this; | |
| 164 | - } | |
| 165 | - | |
| 166 | 140 | } | ... | ... |
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java
| 1 | 1 | package cn.iocoder.yudao.framework.mybatis.core.util; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 3 | 4 | import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; |
| 4 | 5 | import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; |
| 5 | 6 | import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum; |
| 6 | 7 | import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; |
| 7 | 8 | import com.baomidou.mybatisplus.annotation.DbType; |
| 8 | 9 | import org.springframework.beans.factory.NoSuchBeanDefinitionException; |
| 10 | +import org.springframework.jdbc.datasource.ConnectionHolder; | |
| 11 | +import org.springframework.transaction.support.TransactionSynchronizationManager; | |
| 12 | +import org.springframework.util.ConcurrentReferenceHashMap; | |
| 9 | 13 | |
| 10 | 14 | import javax.sql.DataSource; |
| 11 | 15 | import java.sql.Connection; |
| 16 | +import java.sql.DatabaseMetaData; | |
| 12 | 17 | import java.sql.DriverManager; |
| 13 | 18 | import java.sql.SQLException; |
| 19 | +import java.util.Locale; | |
| 20 | +import java.util.Map; | |
| 14 | 21 | |
| 15 | 22 | /** |
| 16 | 23 | * JDBC 工具类 |
| ... | ... | @@ -20,6 +27,12 @@ import java.sql.SQLException; |
| 20 | 27 | public class JdbcUtils { |
| 21 | 28 | |
| 22 | 29 | /** |
| 30 | + * 数据源与 DB 类型的缓存;弱引用数据源,动态移除的数据源可被回收 | |
| 31 | + */ | |
| 32 | + private static final Map<DataSource, DbType> DB_TYPE_CACHE = | |
| 33 | + new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); | |
| 34 | + | |
| 35 | + /** | |
| 23 | 36 | * 判断连接是否正确 |
| 24 | 37 | * |
| 25 | 38 | * @param url 数据源连接 |
| ... | ... | @@ -55,26 +68,127 @@ public class JdbcUtils { |
| 55 | 68 | } |
| 56 | 69 | |
| 57 | 70 | /** |
| 71 | + * 获得当前执行时路由到的数据源 | |
| 72 | + * | |
| 73 | + * 存在动态数据源时,按 {@link DynamicRoutingDataSource#determineDataSource()} 获取当前线程路由到的数据源;否则使用唯一的数据源 | |
| 74 | + * | |
| 75 | + * @return 数据源 | |
| 76 | + */ | |
| 77 | + public static DataSource getDataSource() { | |
| 78 | + DataSource dataSource = getRoutingDataSource(); | |
| 79 | + return dataSource instanceof DynamicRoutingDataSource | |
| 80 | + ? ((DynamicRoutingDataSource) dataSource).determineDataSource() : dataSource; | |
| 81 | + } | |
| 82 | + | |
| 83 | + /** | |
| 84 | + * 取得注册的路由数据源;没有动态数据源时使用唯一的数据源。 | |
| 85 | + * | |
| 86 | + * @return 路由数据源或唯一数据源 | |
| 87 | + */ | |
| 88 | + private static DataSource getRoutingDataSource() { | |
| 89 | + try { | |
| 90 | + return SpringUtils.getBean(DynamicRoutingDataSource.class); | |
| 91 | + } catch (NoSuchBeanDefinitionException e) { | |
| 92 | + return SpringUtils.getBean(DataSource.class); | |
| 93 | + } | |
| 94 | + } | |
| 95 | + | |
| 96 | + /** | |
| 97 | + * 获得当前数据源上 Spring 事务已绑定的连接,不借出新连接,也不转移连接的关闭责任。 | |
| 98 | + * 动态数据源的事务资源绑定在路由数据源上;事务内再次切换路由键,不会改变已绑定的执行连接。 | |
| 99 | + */ | |
| 100 | + static Connection getTransactionalConnection() { | |
| 101 | + Object resource = TransactionSynchronizationManager.getResource(getRoutingDataSource()); | |
| 102 | + return resource instanceof ConnectionHolder ? ((ConnectionHolder) resource).getConnection() : null; | |
| 103 | + } | |
| 104 | + | |
| 105 | + /** | |
| 58 | 106 | * 通过当前数据库连接获得对应的 DB 类型 |
| 59 | 107 | * |
| 108 | + * 事务内复用已绑定连接;其它情况按当前路由到的数据源实例缓存,命中缓存时不再获取连接 | |
| 109 | + * | |
| 60 | 110 | * @return DB 类型 |
| 61 | 111 | */ |
| 62 | 112 | public static DbType getDbType() { |
| 63 | - DataSource dataSource; | |
| 64 | - try { | |
| 65 | - DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class); | |
| 66 | - dataSource = dynamicRoutingDataSource.determineDataSource(); | |
| 67 | - } catch (NoSuchBeanDefinitionException e) { | |
| 68 | - dataSource = SpringUtils.getBean(DataSource.class); | |
| 113 | + Connection connection = getTransactionalConnection(); | |
| 114 | + if (connection != null) { | |
| 115 | + try { | |
| 116 | + return detectDbType(connection); | |
| 117 | + } catch (SQLException e) { | |
| 118 | + throw new IllegalArgumentException(e.getMessage()); | |
| 119 | + } | |
| 69 | 120 | } |
| 121 | + return getDbType(getDataSource()); | |
| 122 | + } | |
| 123 | + | |
| 124 | + // TODO DONE @AI:方法注释;已说明连接归属、缓存或数据源选择。 | |
| 125 | + /** | |
| 126 | + * 按实际数据源实例缓存数据库类型;检测失败时不写入缓存。 | |
| 127 | + * | |
| 128 | + * @param dataSource 实际路由到的数据源 | |
| 129 | + * @return 数据库类型 | |
| 130 | + */ | |
| 131 | + static DbType getDbType(DataSource dataSource) { | |
| 132 | + return DB_TYPE_CACHE.computeIfAbsent(dataSource, JdbcUtils::detectDbType); | |
| 133 | + } | |
| 134 | + | |
| 135 | + /** | |
| 136 | + * 通过自行借用的连接识别数据库类型,并在结束后关闭该连接。 | |
| 137 | + * | |
| 138 | + * @param dataSource 待识别的数据源 | |
| 139 | + * @return 数据库类型 | |
| 140 | + */ | |
| 141 | + private static DbType detectDbType(DataSource dataSource) { | |
| 70 | 142 | try (Connection conn = dataSource.getConnection()) { |
| 71 | - return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName()); | |
| 143 | + return detectDbType(conn); | |
| 72 | 144 | } catch (SQLException e) { |
| 73 | 145 | throw new IllegalArgumentException(e.getMessage()); |
| 74 | 146 | } |
| 75 | 147 | } |
| 76 | 148 | |
| 77 | 149 | /** |
| 150 | + * 读取执行连接的元数据识别数据库类型,不关闭调用方持有的连接。 | |
| 151 | + * | |
| 152 | + * @param connection 当前执行连接 | |
| 153 | + * @return 数据库类型;产品名为空时保留原有 null 返回 | |
| 154 | + * @throws SQLException 读取数据库元数据失败 | |
| 155 | + */ | |
| 156 | + private static DbType detectDbType(Connection connection) throws SQLException { | |
| 157 | + DatabaseMetaData metaData = connection.getMetaData(); | |
| 158 | + String productName = metaData.getDatabaseProductName(); | |
| 159 | + DbType dbType = DbTypeEnum.find(productName); | |
| 160 | + if (dbType != null || StrUtil.isBlank(productName)) { | |
| 161 | + return dbType; | |
| 162 | + } | |
| 163 | + // 不使用 MyBatis Plus 的 URL 识别,避免其在日志中打印完整 URL | |
| 164 | + dbType = findDbTypeByNativeUrl(metaData.getURL()); | |
| 165 | + if (dbType == null) { | |
| 166 | + throw new IllegalArgumentException("不支持的数据库产品:" + productName); | |
| 167 | + } | |
| 168 | + return dbType; | |
| 169 | + } | |
| 170 | + | |
| 171 | + /** | |
| 172 | + * 按原生驱动 URL 识别产品名未登记的数据库,与 {@link #getDbType(String)} 的归类保持一致 | |
| 173 | + * | |
| 174 | + * @param url JDBC URL | |
| 175 | + * @return DB 类型;无法识别时返回 null | |
| 176 | + */ | |
| 177 | + private static DbType findDbTypeByNativeUrl(String url) { | |
| 178 | + if (StrUtil.isBlank(url)) { | |
| 179 | + return null; | |
| 180 | + } | |
| 181 | + String lowerUrl = url.toLowerCase(Locale.ROOT); | |
| 182 | + if (lowerUrl.contains(":highgo:")) { | |
| 183 | + return DbType.POSTGRE_SQL; | |
| 184 | + } | |
| 185 | + if (lowerUrl.contains(":mariadb:")) { | |
| 186 | + return DbType.MARIADB; | |
| 187 | + } | |
| 188 | + return null; | |
| 189 | + } | |
| 190 | + | |
| 191 | + /** | |
| 78 | 192 | * 判断 JDBC 连接是否为 SQLServer 数据库 |
| 79 | 193 | * |
| 80 | 194 | * @param url JDBC 连接 |
| ... | ... | @@ -95,4 +209,6 @@ public class JdbcUtils { |
| 95 | 209 | return ObjectUtils.equalsAny(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005); |
| 96 | 210 | } |
| 97 | 211 | |
| 212 | + | |
| 213 | + | |
| 98 | 214 | } | ... | ... |
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java
| ... | ... | @@ -61,6 +61,26 @@ public class MyBatisUtils { |
| 61 | 61 | return page; |
| 62 | 62 | } |
| 63 | 63 | |
| 64 | + /** | |
| 65 | + * 构建指定偏移量的分页对象 | |
| 66 | + * | |
| 67 | + * @param offset 偏移量,负数按 0 处理,不要求是每页条数的整数倍 | |
| 68 | + * @param size 每页条数 | |
| 69 | + * @param searchCount 是否查询总数 | |
| 70 | + * @param <T> 记录类型 | |
| 71 | + * @return 分页对象 | |
| 72 | + */ | |
| 73 | + public static <T> Page<T> buildPage(long offset, long size, boolean searchCount) { | |
| 74 | + return new Page<T>(1, size, searchCount) { | |
| 75 | + | |
| 76 | + @Override | |
| 77 | + public long offset() { | |
| 78 | + return Math.max(offset, 0); | |
| 79 | + } | |
| 80 | + | |
| 81 | + }; | |
| 82 | + } | |
| 83 | + | |
| 64 | 84 | @SuppressWarnings("PatternVariableCanBeUsed") |
| 65 | 85 | public static <T> void addOrder(Wrapper<T> wrapper, Collection<SortingField> sortingFields) { |
| 66 | 86 | if (CollUtil.isEmpty(sortingFields)) { |
| ... | ... | @@ -220,7 +240,13 @@ public class MyBatisUtils { |
| 220 | 240 | .replace(FIND_IN_SET_VALUE_PLACEHOLDER, "{" + paramIndex + "}"); |
| 221 | 241 | } |
| 222 | 242 | |
| 223 | - private static boolean isSafeColumnName(String columnName) { | |
| 243 | + /** | |
| 244 | + * 判断表名或列名是否只包含字母、数字、下划线及层级分隔点 | |
| 245 | + * | |
| 246 | + * @param columnName 表名或列名,可带表别名前缀 | |
| 247 | + * @return 是否满足安全标识符格式 | |
| 248 | + */ | |
| 249 | + static boolean isSafeColumnName(String columnName) { | |
| 224 | 250 | return StrUtil.isNotEmpty(columnName) && SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches(); |
| 225 | 251 | } |
| 226 | 252 | ... | ... |
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/SqlDialectUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.mybatis.core.util; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum; | |
| 5 | +import com.baomidou.mybatisplus.annotation.DbType; | |
| 6 | +import org.springframework.util.ConcurrentReferenceHashMap; | |
| 7 | + | |
| 8 | +import javax.sql.DataSource; | |
| 9 | +import java.sql.Connection; | |
| 10 | +import java.sql.DatabaseMetaData; | |
| 11 | +import java.sql.SQLException; | |
| 12 | +import java.util.EnumSet; | |
| 13 | +import java.util.Locale; | |
| 14 | +import java.util.Map; | |
| 15 | +import java.util.Set; | |
| 16 | +import java.util.regex.Pattern; | |
| 17 | + | |
| 18 | +// TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。 | |
| 19 | +// TODO DONE @AI:多余的方法,可以考虑去掉;当然,保留也可以。。。已核对生产 Java/XML 调用,现有方法均有用途,保留。 | |
| 20 | +/** | |
| 21 | + * 日期统计、行数限制、分批删除 SQL 片段的方言工具类 | |
| 22 | + * | |
| 23 | + * 1. 每次生成 SQL 时,按当前执行时路由到的数据源选择方言(按数据源实例缓存),不使用启动时固定的 databaseId 或主库 | |
| 24 | + * 2. 只有明确识别的 PostgreSQL、Oracle、SQL Server 2012+ 输出新片段;其它已识别的数据库(MySQL、MariaDB、OceanBase、H2、 | |
| 25 | + * 达梦、神通、人大金仓、openGauss、GaussDB、瀚高、MogDB 等)保持原有 SQL 片段;完全未识别的数据库抛出异常 | |
| 26 | + * 3. 参数只能是代码中固定的列名,经过白名单校验;格式均为常量,业务值必须继续使用 #{} 绑定 | |
| 27 | + * | |
| 28 | + * 在 XML 或注解中使用:${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} | |
| 29 | + * | |
| 30 | + * @author 芋道源码 | |
| 31 | + */ | |
| 32 | +@SuppressWarnings("EnhancedSwitchMigration") | |
| 33 | +public class SqlDialectUtils { | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * SQL Server 2012 的主版本号 | |
| 37 | + */ | |
| 38 | + private static final int SQL_SERVER_2012_MAJOR_VERSION = 11; | |
| 39 | + | |
| 40 | + /** | |
| 41 | + * 保持原有 SQL 片段的数据库类型 | |
| 42 | + */ | |
| 43 | + private static final Set<DbType> ORIGINAL_DB_TYPES = EnumSet.of(DbType.MYSQL, DbType.OCEAN_BASE, | |
| 44 | + DbType.H2, DbType.DM, DbType.OSCAR, DbType.KINGBASE_ES, DbType.OPENGAUSS, DbType.GAUSS_DB, | |
| 45 | + DbType.SQL_SERVER2005); | |
| 46 | + | |
| 47 | + /** | |
| 48 | + * 兼容 PostgreSQL 的原生驱动 URL:瀚高、openGauss、GaussDB、MogDB、人大金仓 | |
| 49 | + */ | |
| 50 | + private static final Pattern POSTGRESQL_VARIANT_URL_PATTERN = | |
| 51 | + Pattern.compile(":(highgo|opengauss|gaussdb|mogdb|kingbase\\d*):"); | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * MyBatis 参数名:参数或其属性路径,例如 limit、reqVO.limit | |
| 55 | + */ | |
| 56 | + private static final Pattern PARAM_NAME_PATTERN = | |
| 57 | + Pattern.compile("[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*"); | |
| 58 | + | |
| 59 | + /** | |
| 60 | + * 数据源与方言的缓存;弱引用数据源,动态移除的数据源可被回收 | |
| 61 | + */ | |
| 62 | + private static final Map<DataSource, Dialect> DIALECT_CACHE = | |
| 63 | + new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); | |
| 64 | + | |
| 65 | + /** | |
| 66 | + * 方言 | |
| 67 | + */ | |
| 68 | + enum Dialect { | |
| 69 | + | |
| 70 | + /** | |
| 71 | + * 原有 SQL 片段 | |
| 72 | + */ | |
| 73 | + ORIGINAL, | |
| 74 | + POSTGRESQL, | |
| 75 | + ORACLE, | |
| 76 | + SQLSERVER | |
| 77 | + | |
| 78 | + } | |
| 79 | + | |
| 80 | + // ========== 日期字符串 ========== | |
| 81 | + | |
| 82 | + /** | |
| 83 | + * 按天格式化为 yyyy-MM-dd 字符串;原有片段为 DATE_FORMAT(column, '%Y-%m-%d') | |
| 84 | + * | |
| 85 | + * @param column 列名 | |
| 86 | + * @return SQL 片段 | |
| 87 | + */ | |
| 88 | + public static String formatDate(String column) { | |
| 89 | + return formatDate(getDialect(), column); | |
| 90 | + } | |
| 91 | + | |
| 92 | + /** | |
| 93 | + * 按指定方言生成 yyyy-MM-dd 字符串,保留各分支原有格式。 | |
| 94 | + * | |
| 95 | + * @param dialect 已识别的数据库方言 | |
| 96 | + * @param column 代码中固定的安全列名 | |
| 97 | + * @return SQL 片段 | |
| 98 | + */ | |
| 99 | + static String formatDate(Dialect dialect, String column) { | |
| 100 | + checkColumn(column); | |
| 101 | + switch (dialect) { | |
| 102 | + case POSTGRESQL: | |
| 103 | + case ORACLE: | |
| 104 | + return "TO_CHAR(" + column + ", 'YYYY-MM-DD')"; | |
| 105 | + case SQLSERVER: | |
| 106 | + return "CONVERT(varchar(10), " + column + ", 23)"; | |
| 107 | + default: | |
| 108 | + return "DATE_FORMAT(" + column + ", '%Y-%m-%d')"; | |
| 109 | + } | |
| 110 | + } | |
| 111 | + | |
| 112 | + /** | |
| 113 | + * 按月格式化为 yyyyMM 字符串;原有片段为 DATE_FORMAT(column, '%Y%m') | |
| 114 | + * | |
| 115 | + * @param column 列名 | |
| 116 | + * @return SQL 片段 | |
| 117 | + */ | |
| 118 | + // TODO DONE @AI:用不到的,可以暂时先去掉噢; CRM 业绩统计使用 yyyyMM,商城交易统计使用 yyyy-MM,均需保留。 | |
| 119 | + public static String formatYearMonth(String column) { | |
| 120 | + return formatYearMonth(getDialect(), column); | |
| 121 | + } | |
| 122 | + | |
| 123 | + /** | |
| 124 | + * 按指定方言生成 yyyyMM 字符串,供 CRM 按月统计使用。 | |
| 125 | + * | |
| 126 | + * @param dialect 已识别的数据库方言 | |
| 127 | + * @param column 代码中固定的安全列名 | |
| 128 | + * @return SQL 片段 | |
| 129 | + */ | |
| 130 | + static String formatYearMonth(Dialect dialect, String column) { | |
| 131 | + checkColumn(column); | |
| 132 | + switch (dialect) { | |
| 133 | + case POSTGRESQL: | |
| 134 | + case ORACLE: | |
| 135 | + return "TO_CHAR(" + column + ", 'YYYYMM')"; | |
| 136 | + case SQLSERVER: | |
| 137 | + return "CONVERT(varchar(6), " + column + ", 112)"; | |
| 138 | + default: | |
| 139 | + return "DATE_FORMAT(" + column + ", '%Y%m')"; | |
| 140 | + } | |
| 141 | + } | |
| 142 | + | |
| 143 | + /** | |
| 144 | + * 按月格式化为 yyyy-MM 字符串;原有片段为 CONCAT(EXTRACT(YEAR ...), '-', LPAD(...)) | |
| 145 | + * | |
| 146 | + * @param column 列名 | |
| 147 | + * @return SQL 片段 | |
| 148 | + */ | |
| 149 | + // TODO DONE @AI:用不到的,可以暂时先去掉噢; CRM 业绩统计使用 yyyyMM,商城交易统计使用 yyyy-MM,均需保留。 | |
| 150 | + public static String formatYearDashMonth(String column) { | |
| 151 | + return formatYearDashMonth(getDialect(), column); | |
| 152 | + } | |
| 153 | + | |
| 154 | + /** | |
| 155 | + * 按指定方言生成 yyyy-MM 字符串,供交易统计月份标签使用。 | |
| 156 | + * | |
| 157 | + * @param dialect 已识别的数据库方言 | |
| 158 | + * @param column 代码中固定的安全列名 | |
| 159 | + * @return SQL 片段 | |
| 160 | + */ | |
| 161 | + static String formatYearDashMonth(Dialect dialect, String column) { | |
| 162 | + checkColumn(column); | |
| 163 | + switch (dialect) { | |
| 164 | + case POSTGRESQL: | |
| 165 | + case ORACLE: | |
| 166 | + return "TO_CHAR(" + column + ", 'YYYY-MM')"; | |
| 167 | + case SQLSERVER: | |
| 168 | + return "CONVERT(varchar(7), " + column + ", 120)"; | |
| 169 | + default: | |
| 170 | + return "CONCAT(EXTRACT(YEAR FROM " + column + "), '-', LPAD(CONCAT(EXTRACT(MONTH FROM " + column + ")), 2, '0'))"; | |
| 171 | + } | |
| 172 | + } | |
| 173 | + | |
| 174 | + /** | |
| 175 | + * 按天分组;原有片段为 DATE(column),新方言输出 yyyy-MM-dd 字符串 | |
| 176 | + * | |
| 177 | + * 适用于同时兼容日期值与 yyyy-MM-dd 字符串的消费者 | |
| 178 | + * | |
| 179 | + * @param column 列名 | |
| 180 | + * @return SQL 片段 | |
| 181 | + */ | |
| 182 | + public static String date(String column) { | |
| 183 | + return date(getDialect(), column); | |
| 184 | + } | |
| 185 | + | |
| 186 | + /** | |
| 187 | + * 生成按天分组表达式;原路径保留 DATE,新分支返回日期文本。 | |
| 188 | + * | |
| 189 | + * @param dialect 已识别的数据库方言 | |
| 190 | + * @param column 代码中固定的安全列名 | |
| 191 | + * @return SQL 片段 | |
| 192 | + */ | |
| 193 | + static String date(Dialect dialect, String column) { | |
| 194 | + checkColumn(column); | |
| 195 | + if (dialect == Dialect.ORIGINAL) { | |
| 196 | + return "DATE(" + column + ")"; | |
| 197 | + } | |
| 198 | + return formatDate(dialect, column); | |
| 199 | + } | |
| 200 | + | |
| 201 | + // TODO DONE @AI:用不到的,可以先清理掉噢; AI1 首页与交易统计的 String 日期消费者仍在使用,保留。 | |
| 202 | + /** | |
| 203 | + * 按天分组;原有片段为 CAST(column AS DATE),新方言输出 yyyy-MM-dd 字符串 | |
| 204 | + * | |
| 205 | + * 适用于 String 类型的消费者 | |
| 206 | + * | |
| 207 | + * @param column 列名 | |
| 208 | + * @return SQL 片段 | |
| 209 | + */ | |
| 210 | + public static String castDateText(String column) { | |
| 211 | + return castDateText(getDialect(), column); | |
| 212 | + } | |
| 213 | + | |
| 214 | + /** | |
| 215 | + * 生成 String 日期消费者所需的转换表达式,保留原路径 CAST AS DATE。 | |
| 216 | + * | |
| 217 | + * @param dialect 已识别的数据库方言 | |
| 218 | + * @param column 代码中固定的安全列名 | |
| 219 | + * @return SQL 片段 | |
| 220 | + */ | |
| 221 | + static String castDateText(Dialect dialect, String column) { | |
| 222 | + checkColumn(column); | |
| 223 | + if (dialect == Dialect.ORIGINAL) { | |
| 224 | + return "CAST(" + column + " AS DATE)"; | |
| 225 | + } | |
| 226 | + return formatDate(dialect, column); | |
| 227 | + } | |
| 228 | + | |
| 229 | + // ========== 日期值 ========== | |
| 230 | + | |
| 231 | + /** | |
| 232 | + * 按天截断为日期值;原有片段为 CAST(column AS DATE) | |
| 233 | + * | |
| 234 | + * 适用于 LocalDate 类型的消费者 | |
| 235 | + * | |
| 236 | + * @param column 列名 | |
| 237 | + * @return SQL 片段 | |
| 238 | + */ | |
| 239 | + // TODO DONE @AI:多余的是不是去掉? 会员统计的 LocalDate 消费者仍在使用,保留 DATE 类型转换。 | |
| 240 | + public static String castDate(String column) { | |
| 241 | + return castDate(getDialect(), column); | |
| 242 | + } | |
| 243 | + | |
| 244 | + // TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。 | |
| 245 | + /** | |
| 246 | + * 生成 LocalDate 消费者所需的日期值;Oracle 用 TRUNC 去掉时分秒。 | |
| 247 | + * | |
| 248 | + * @param dialect 已识别的数据库方言 | |
| 249 | + * @param column 代码中固定的安全列名 | |
| 250 | + * @return SQL 片段 | |
| 251 | + */ | |
| 252 | + static String castDate(Dialect dialect, String column) { | |
| 253 | + checkColumn(column); | |
| 254 | + if (dialect == Dialect.ORACLE) { | |
| 255 | + // Oracle 的 DATE 包含时分秒,CAST AS DATE 不会截断 | |
| 256 | + return "TRUNC(" + column + ")"; | |
| 257 | + } | |
| 258 | + return "CAST(" + column + " AS DATE)"; | |
| 259 | + } | |
| 260 | + | |
| 261 | + // ========== 日期部件 ========== | |
| 262 | + | |
| 263 | + /** | |
| 264 | + * 月份(1-12);原有片段为 MONTH(column) | |
| 265 | + * | |
| 266 | + * @param column 列名 | |
| 267 | + * @return SQL 片段 | |
| 268 | + */ | |
| 269 | + public static String month(String column) { | |
| 270 | + return month(getDialect(), column); | |
| 271 | + } | |
| 272 | + | |
| 273 | + /** | |
| 274 | + * 按指定方言提取月份。 | |
| 275 | + * | |
| 276 | + * @param dialect 已识别的数据库方言 | |
| 277 | + * @param column 代码中固定的安全列名 | |
| 278 | + * @return 月份 SQL 表达式 | |
| 279 | + */ | |
| 280 | + static String month(Dialect dialect, String column) { | |
| 281 | + return datePart(dialect, column, "MONTH"); | |
| 282 | + } | |
| 283 | + | |
| 284 | + /** | |
| 285 | + * 年份;原有片段为 YEAR(column) | |
| 286 | + * | |
| 287 | + * @param column 列名 | |
| 288 | + * @return SQL 片段 | |
| 289 | + */ | |
| 290 | + // TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。 | |
| 291 | + public static String year(String column) { | |
| 292 | + return year(getDialect(), column); | |
| 293 | + } | |
| 294 | + | |
| 295 | + /** | |
| 296 | + * 按指定方言提取年份,供 CRM 业绩目标筛选使用。 | |
| 297 | + * | |
| 298 | + * @param dialect 已识别的数据库方言 | |
| 299 | + * @param column 代码中固定的安全列名 | |
| 300 | + * @return 年份 SQL 表达式 | |
| 301 | + */ | |
| 302 | + static String year(Dialect dialect, String column) { | |
| 303 | + return datePart(dialect, column, "YEAR"); | |
| 304 | + } | |
| 305 | + | |
| 306 | + /** | |
| 307 | + * 日(1-31);原有片段为 DAYOFMONTH(column) | |
| 308 | + * | |
| 309 | + * @param column 列名 | |
| 310 | + * @return SQL 片段 | |
| 311 | + */ | |
| 312 | + public static String dayOfMonth(String column) { | |
| 313 | + return dayOfMonth(getDialect(), column); | |
| 314 | + } | |
| 315 | + | |
| 316 | + // TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。 | |
| 317 | + /** | |
| 318 | + * 按指定方言提取月内日,原路径保留 DAYOFMONTH。 | |
| 319 | + * | |
| 320 | + * @param dialect 已识别的数据库方言 | |
| 321 | + * @param column 代码中固定的安全列名 | |
| 322 | + * @return 日 SQL 表达式 | |
| 323 | + */ | |
| 324 | + static String dayOfMonth(Dialect dialect, String column) { | |
| 325 | + checkColumn(column); | |
| 326 | + if (dialect == Dialect.ORIGINAL) { | |
| 327 | + return "DAYOFMONTH(" + column + ")"; | |
| 328 | + } | |
| 329 | + return datePart(dialect, column, "DAY"); | |
| 330 | + } | |
| 331 | + | |
| 332 | + /** | |
| 333 | + * 日期部件:PostgreSQL 的 EXTRACT 返回 numeric,需要转为整数;原有片段与 SQL Server 均为 field(column) | |
| 334 | + * | |
| 335 | + * @param dialect 已识别的数据库方言 | |
| 336 | + * @param column 代码中固定的安全列名 | |
| 337 | + * @param field 代码中固定的 YEAR、MONTH 或 DAY | |
| 338 | + * @return 日期部件 SQL 表达式 | |
| 339 | + */ | |
| 340 | + private static String datePart(Dialect dialect, String column, String field) { | |
| 341 | + checkColumn(column); | |
| 342 | + switch (dialect) { | |
| 343 | + case POSTGRESQL: | |
| 344 | + return "CAST(EXTRACT(" + field + " FROM " + column + ") AS INTEGER)"; | |
| 345 | + case ORACLE: | |
| 346 | + return "EXTRACT(" + field + " FROM " + column + ")"; | |
| 347 | + default: | |
| 348 | + return field + "(" + column + ")"; | |
| 349 | + } | |
| 350 | + } | |
| 351 | + | |
| 352 | + // ========== 聚合 ========== | |
| 353 | + | |
| 354 | + /** | |
| 355 | + * 平均成交周期(天):两个时间之间的完整天数(向零截断)求平均,保留 1 位小数(截断),无数据时为 0 | |
| 356 | + * | |
| 357 | + * 原有片段为 COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, begin, end)), 1), 0)。 | |
| 358 | + * 注意:仅 PostgreSQL 输出新片段;Oracle、SQL Server 的完整天数表达式依赖实际列类型(DATE / TIMESTAMP / datetime2 / date 等), | |
| 359 | + * 尚未依据实际 DDL 在真实数据库验收,因此保持原有片段 | |
| 360 | + * | |
| 361 | + * @param beginColumn 开始时间列名 | |
| 362 | + * @param endColumn 结束时间列名 | |
| 363 | + * @return SQL 片段 | |
| 364 | + */ | |
| 365 | + public static String customerDealCycle(String beginColumn, String endColumn) { | |
| 366 | + return customerDealCycle(getDialect(), beginColumn, endColumn); | |
| 367 | + } | |
| 368 | + | |
| 369 | + /** | |
| 370 | + * 按指定方言生成平均完整天数;未验收的数据库继续使用原表达式。 | |
| 371 | + * | |
| 372 | + * @param dialect 已识别的数据库方言 | |
| 373 | + * @param beginColumn 代码中固定的开始时间列名 | |
| 374 | + * @param endColumn 代码中固定的结束时间列名 | |
| 375 | + * @return 平均成交周期 SQL 表达式 | |
| 376 | + */ | |
| 377 | + static String customerDealCycle(Dialect dialect, String beginColumn, String endColumn) { | |
| 378 | + checkColumn(beginColumn); | |
| 379 | + checkColumn(endColumn); | |
| 380 | + if (dialect == Dialect.POSTGRESQL) { | |
| 381 | + return "COALESCE(TRUNC(AVG(CAST(EXTRACT(DAY FROM (CAST(" + endColumn + " AS TIMESTAMP) - CAST(" | |
| 382 | + + beginColumn + " AS TIMESTAMP))) AS NUMERIC)), 1), 0)"; | |
| 383 | + } | |
| 384 | + return "COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, " + beginColumn + ", " + endColumn + ")), 1), 0)"; | |
| 385 | + } | |
| 386 | + | |
| 387 | + // ========== 其它 ========== | |
| 388 | + | |
| 389 | + /** | |
| 390 | + * 顶层 SELECT 无 FROM 时的补充:Oracle 为 FROM DUAL,其它为空 | |
| 391 | + * | |
| 392 | + * @return SQL 片段 | |
| 393 | + */ | |
| 394 | + public static String fromDual() { | |
| 395 | + return fromDual(getDialect()); | |
| 396 | + } | |
| 397 | + | |
| 398 | + /** | |
| 399 | + * 为无顶层 FROM 的标量汇总补充 Oracle DUAL,其他分支保持空文本。 | |
| 400 | + * | |
| 401 | + * @param dialect 已识别的数据库方言 | |
| 402 | + * @return Oracle 为 FROM DUAL,其他为空字符串 | |
| 403 | + */ | |
| 404 | + static String fromDual(Dialect dialect) { | |
| 405 | + return dialect == Dialect.ORACLE ? "FROM DUAL" : ""; | |
| 406 | + } | |
| 407 | + | |
| 408 | + // ========== 行数限制 ========== | |
| 409 | + | |
| 410 | + /** | |
| 411 | + * 只返回前 rows 行,追加在语句末尾(ORDER BY 之后);原有片段为 LIMIT rows | |
| 412 | + * | |
| 413 | + * 仅用于 XML 或注解中手写的 SQL;使用 Wrapper 查询时,使用 BaseMapperX 的 selectListLimitN(queryWrapper, limit) 分页查询 | |
| 414 | + * | |
| 415 | + * 1. Oracle:FETCH FIRST rows ROWS ONLY,要求 Oracle 12c+ | |
| 416 | + * 2. SQL Server 2012+:OFFSET 0 ROWS FETCH NEXT rows ROWS ONLY;OFFSET 必须跟在 ORDER BY 之后, | |
| 417 | + * 语句没有排序时补充 ORDER BY (SELECT NULL),即不保证返回哪几行,与原有 LIMIT 无排序时一致 | |
| 418 | + * | |
| 419 | + * @param rows 行数,不能为负数;SQL Server 的 FETCH 不支持 0 行 | |
| 420 | + * @param ordered 语句是否已有 ORDER BY | |
| 421 | + * @return SQL 片段 | |
| 422 | + */ | |
| 423 | + public static String limitRows(int rows, boolean ordered) { | |
| 424 | + return limitRows(getDialect(), rows, ordered); | |
| 425 | + } | |
| 426 | + | |
| 427 | + /** | |
| 428 | + * 生成固定行数限制;负数拒绝,SQL Server 的 FETCH 不接受 0 行。 | |
| 429 | + * | |
| 430 | + * @param dialect 已识别的数据库方言 | |
| 431 | + * @param rows 固定行数 | |
| 432 | + * @param ordered 原查询是否已有排序 | |
| 433 | + * @return 追加在查询末尾的 SQL 片段 | |
| 434 | + */ | |
| 435 | + static String limitRows(Dialect dialect, int rows, boolean ordered) { | |
| 436 | + if (rows < 0) { | |
| 437 | + throw new IllegalArgumentException("行数不能为负数:" + rows); | |
| 438 | + } | |
| 439 | + if (rows == 0 && dialect == Dialect.SQLSERVER) { | |
| 440 | + throw new IllegalArgumentException("SQL Server 的 FETCH 行数必须大于 0"); | |
| 441 | + } | |
| 442 | + return limit(dialect, String.valueOf(rows), ordered); | |
| 443 | + } | |
| 444 | + | |
| 445 | + /** | |
| 446 | + * 只返回前 #{param} 行,行数使用 #{} 绑定;规则同 {@link #limitRows(int, boolean)} | |
| 447 | + * | |
| 448 | + * 在 XML 或注解中使用:${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@limitParam('limit', true)} | |
| 449 | + * | |
| 450 | + * @param param 行数的参数名,例如 limit | |
| 451 | + * @param ordered 语句是否已有 ORDER BY | |
| 452 | + * @return SQL 片段 | |
| 453 | + */ | |
| 454 | + public static String limitParam(String param, boolean ordered) { | |
| 455 | + return limitParam(getDialect(), param, ordered); | |
| 456 | + } | |
| 457 | + | |
| 458 | + /** | |
| 459 | + * 生成绑定参数的行数限制,参数名通过白名单校验。 | |
| 460 | + * | |
| 461 | + * @param dialect 已识别的数据库方言 | |
| 462 | + * @param param 行数参数名或属性路径 | |
| 463 | + * @param ordered 原查询是否已有排序 | |
| 464 | + * @return 包含 MyBatis 参数占位的 SQL 片段 | |
| 465 | + */ | |
| 466 | + static String limitParam(Dialect dialect, String param, boolean ordered) { | |
| 467 | + checkParam(param); | |
| 468 | + return limit(dialect, "#{" + param + "}", ordered); | |
| 469 | + } | |
| 470 | + | |
| 471 | + /** | |
| 472 | + * 拼装行数限制;rows 仅来自已校验的整数或 MyBatis 参数占位。 | |
| 473 | + * | |
| 474 | + * @param dialect 已识别的数据库方言 | |
| 475 | + * @param rows 固定整数文本或已校验的参数占位 | |
| 476 | + * @param ordered 原查询是否已有排序 | |
| 477 | + * @return 行数限制 SQL 片段 | |
| 478 | + */ | |
| 479 | + private static String limit(Dialect dialect, String rows, boolean ordered) { | |
| 480 | + switch (dialect) { | |
| 481 | + case ORACLE: | |
| 482 | + return "FETCH FIRST " + rows + " ROWS ONLY"; | |
| 483 | + case SQLSERVER: | |
| 484 | + return (ordered ? "" : "ORDER BY (SELECT NULL) ") + "OFFSET 0 ROWS FETCH NEXT " + rows + " ROWS ONLY"; | |
| 485 | + default: | |
| 486 | + return "LIMIT " + rows; | |
| 487 | + } | |
| 488 | + } | |
| 489 | + | |
| 490 | + // ========== 分批删除 ========== | |
| 491 | + | |
| 492 | + /** | |
| 493 | + * 分批删除 column < #{valueParam} 的记录,每次最多删除 #{limitParam} 行; | |
| 494 | + * 原有语句为 DELETE FROM table WHERE column < #{valueParam} LIMIT #{limitParam} | |
| 495 | + * | |
| 496 | + * 1. PostgreSQL:DELETE 不支持 LIMIT,按主键 id 的子查询选出一批 | |
| 497 | + * 2. Oracle:ROWNUM <= #{limitParam} | |
| 498 | + * 3. SQL Server 2012+:按主键 id 的子查询 SELECT TOP (#{limitParam})。 | |
| 499 | + * 不使用 DELETE TOP (n),因为 JSqlParser 无法解析,租户、数据权限插件解析 DELETE 时会失败 | |
| 500 | + * | |
| 501 | + * 要求表的主键列为 id;表名、列名只能是代码中固定的常量,值均使用 #{} 绑定 | |
| 502 | + * | |
| 503 | + * 在注解中使用:${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('infra_job_log', 'create_time', 'createTime', 'limit')} | |
| 504 | + * | |
| 505 | + * @param table 表名 | |
| 506 | + * @param column 比较的列名 | |
| 507 | + * @param valueParam 比较值的参数名 | |
| 508 | + * @param limitParam 每批行数的参数名 | |
| 509 | + * @return SQL 语句 | |
| 510 | + */ | |
| 511 | + public static String deleteBatchByLessThan(String table, String column, String valueParam, String limitParam) { | |
| 512 | + return deleteBatchByLessThan(getDialect(), table, column, valueParam, limitParam); | |
| 513 | + } | |
| 514 | + | |
| 515 | + /** | |
| 516 | + * 按方言生成主键为 id 的分批物理删除;当前用于忽略租户的全局清理任务。 | |
| 517 | + * | |
| 518 | + * @param dialect 已识别的数据库方言 | |
| 519 | + * @param table 代码中固定的表名 | |
| 520 | + * @param column 代码中固定的时间列名 | |
| 521 | + * @param valueParam 时间阈值参数名 | |
| 522 | + * @param limitParam 每批行数参数名 | |
| 523 | + * @return 参数绑定的 DELETE 语句 | |
| 524 | + */ | |
| 525 | + static String deleteBatchByLessThan(Dialect dialect, String table, String column, | |
| 526 | + String valueParam, String limitParam) { | |
| 527 | + checkColumn(table); | |
| 528 | + checkColumn(column); | |
| 529 | + checkParam(valueParam); | |
| 530 | + checkParam(limitParam); | |
| 531 | + String condition = column + " < #{" + valueParam + "}"; | |
| 532 | + String limit = "#{" + limitParam + "}"; | |
| 533 | + switch (dialect) { | |
| 534 | + case POSTGRESQL: | |
| 535 | + return "DELETE FROM " + table + " WHERE id IN (SELECT id FROM " + table | |
| 536 | + + " WHERE " + condition + " LIMIT " + limit + ")"; | |
| 537 | + case ORACLE: | |
| 538 | + return "DELETE FROM " + table + " WHERE " + condition + " AND ROWNUM <= " + limit; | |
| 539 | + case SQLSERVER: | |
| 540 | + return "DELETE FROM " + table + " WHERE id IN (SELECT TOP (" + limit + ") id FROM " + table | |
| 541 | + + " WHERE " + condition + ")"; | |
| 542 | + default: | |
| 543 | + return "DELETE FROM " + table + " WHERE " + condition + " LIMIT " + limit; | |
| 544 | + } | |
| 545 | + } | |
| 546 | + | |
| 547 | + // ========== 方言识别 ========== | |
| 548 | + | |
| 549 | + /** | |
| 550 | + * 取得当前执行方言;事务内复用已绑定连接,非事务按路由后的数据源缓存。 | |
| 551 | + * | |
| 552 | + * @return 当前执行方言 | |
| 553 | + */ | |
| 554 | + static Dialect getDialect() { | |
| 555 | + Connection connection = JdbcUtils.getTransactionalConnection(); | |
| 556 | + if (connection != null) { | |
| 557 | + try { | |
| 558 | + // 使用真正执行 SQL 的事务连接;不能将其方言缓存到事务内新切换的路由键上 | |
| 559 | + return resolveDialect(connection.getMetaData()); | |
| 560 | + } catch (SQLException e) { | |
| 561 | + throw new IllegalStateException("获取数据库方言失败", e); | |
| 562 | + } | |
| 563 | + } | |
| 564 | + return getDialect(JdbcUtils.getDataSource()); | |
| 565 | + } | |
| 566 | + | |
| 567 | + /** | |
| 568 | + * 按数据源实例缓存方言;识别失败时不写入缓存。 | |
| 569 | + * | |
| 570 | + * @param dataSource 实际路由到的数据源 | |
| 571 | + * @return 数据库方言 | |
| 572 | + */ | |
| 573 | + static Dialect getDialect(DataSource dataSource) { | |
| 574 | + return DIALECT_CACHE.computeIfAbsent(dataSource, SqlDialectUtils::detectDialect); | |
| 575 | + } | |
| 576 | + | |
| 577 | + /** | |
| 578 | + * 借用一个连接识别数据源方言,并关闭该次自行获取的连接。 | |
| 579 | + * | |
| 580 | + * @param dataSource 待识别的数据源 | |
| 581 | + * @return 数据库方言 | |
| 582 | + */ | |
| 583 | + private static Dialect detectDialect(DataSource dataSource) { | |
| 584 | + try (Connection conn = dataSource.getConnection()) { | |
| 585 | + return resolveDialect(conn.getMetaData()); | |
| 586 | + } catch (SQLException e) { | |
| 587 | + throw new IllegalStateException("获取数据库方言失败", e); | |
| 588 | + } | |
| 589 | + } | |
| 590 | + | |
| 591 | + /** | |
| 592 | + * 根据数据库元数据识别方言 | |
| 593 | + * | |
| 594 | + * 注意:使用 PostgreSQL 官方驱动(jdbc:postgresql:)连接 openGauss、瀚高等兼容 PostgreSQL 的数据库时, | |
| 595 | + * 产品名与 URL 均与 PostgreSQL 相同,无法区分,会按 PostgreSQL 处理 | |
| 596 | + * | |
| 597 | + * @param metaData 数据库元数据 | |
| 598 | + * @return 方言 | |
| 599 | + * @throws SQLException 读取元数据失败 | |
| 600 | + */ | |
| 601 | + static Dialect resolveDialect(DatabaseMetaData metaData) throws SQLException { | |
| 602 | + String productName = metaData.getDatabaseProductName(); | |
| 603 | + String url = StrUtil.nullToEmpty(metaData.getURL()).toLowerCase(Locale.ROOT); | |
| 604 | + DbType dbType = DbTypeEnum.find(productName); | |
| 605 | + if (dbType == DbType.ORACLE) { | |
| 606 | + return Dialect.ORACLE; | |
| 607 | + } | |
| 608 | + if (dbType == DbType.SQL_SERVER) { | |
| 609 | + return metaData.getDatabaseMajorVersion() >= SQL_SERVER_2012_MAJOR_VERSION | |
| 610 | + ? Dialect.SQLSERVER : Dialect.ORIGINAL; | |
| 611 | + } | |
| 612 | + if (dbType == DbType.POSTGRE_SQL) { | |
| 613 | + if (isPostgreSQLVariantUrl(url)) { | |
| 614 | + return Dialect.ORIGINAL; | |
| 615 | + } | |
| 616 | + if (url.contains(":postgresql:")) { | |
| 617 | + return Dialect.POSTGRESQL; | |
| 618 | + } | |
| 619 | + throw unsupported(productName); | |
| 620 | + } | |
| 621 | + if (ORIGINAL_DB_TYPES.contains(dbType)) { | |
| 622 | + return Dialect.ORIGINAL; | |
| 623 | + } | |
| 624 | + // 产品名未登记时,仅按明确的原生驱动 URL 识别瀚高、MogDB 等,以及 MariaDB | |
| 625 | + if (dbType == null && (isPostgreSQLVariantUrl(url) || url.contains(":mariadb:"))) { | |
| 626 | + return Dialect.ORIGINAL; | |
| 627 | + } | |
| 628 | + throw unsupported(productName); | |
| 629 | + } | |
| 630 | + | |
| 631 | + /** | |
| 632 | + * 判断 URL 是否为需要保留原 SQL 路径的 PostgreSQL 兼容产品驱动。 | |
| 633 | + * | |
| 634 | + * @param url 转为小写的 JDBC URL | |
| 635 | + * @return 是否命中已登记的兼容产品 URL | |
| 636 | + */ | |
| 637 | + private static boolean isPostgreSQLVariantUrl(String url) { | |
| 638 | + return POSTGRESQL_VARIANT_URL_PATTERN.matcher(url).find(); | |
| 639 | + } | |
| 640 | + | |
| 641 | + /** | |
| 642 | + * 创建未识别产品的异常,不将完整 JDBC URL 写入消息。 | |
| 643 | + * | |
| 644 | + * @param productName 数据库产品名 | |
| 645 | + * @return 不支持该产品的异常 | |
| 646 | + */ | |
| 647 | + private static IllegalStateException unsupported(String productName) { | |
| 648 | + return new IllegalStateException("SQL 方言不支持的数据库产品:" + productName); | |
| 649 | + } | |
| 650 | + | |
| 651 | + /** | |
| 652 | + * 校验代码中固定的表名或列名,拒绝表达式与语句片段。 | |
| 653 | + * | |
| 654 | + * @param column 待校验的表名或列名 | |
| 655 | + */ | |
| 656 | + private static void checkColumn(String column) { | |
| 657 | + if (!MyBatisUtils.isSafeColumnName(column)) { | |
| 658 | + throw new IllegalArgumentException("Invalid column name: " + column); | |
| 659 | + } | |
| 660 | + } | |
| 661 | + | |
| 662 | + /** | |
| 663 | + * 校验 MyBatis 参数名或属性路径,拒绝占位符逃逸与 SQL 片段。 | |
| 664 | + * | |
| 665 | + * @param param 待校验的参数名 | |
| 666 | + */ | |
| 667 | + private static void checkParam(String param) { | |
| 668 | + if (param == null || !PARAM_NAME_PATTERN.matcher(param).matches()) { | |
| 669 | + throw new IllegalArgumentException("Invalid param name: " + param); | |
| 670 | + } | |
| 671 | + } | |
| 672 | + | |
| 673 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnumTest.java
| ... | ... | @@ -25,6 +25,15 @@ public class DbTypeEnumTest { |
| 25 | 25 | // 神通 JDBC 驱动(jdbc:oscar:)返回的 ProductName |
| 26 | 26 | assertEquals(DbType.OSCAR, DbTypeEnum.find("OSCAR")); |
| 27 | 27 | assertNull(DbTypeEnum.find("")); |
| 28 | + assertNull(DbTypeEnum.find(null)); | |
| 29 | + } | |
| 30 | + | |
| 31 | + @Test | |
| 32 | + public void testFind_unregistered() { | |
| 33 | + // 未登记的产品名返回 null,而不是 NPE,由调用方给出明确的处理 | |
| 34 | + assertNull(DbTypeEnum.find("HighGo")); | |
| 35 | + assertNull(DbTypeEnum.find("MariaDB")); | |
| 36 | + assertNull(DbTypeEnum.find("SQLite")); | |
| 28 | 37 | } |
| 29 | 38 | |
| 30 | 39 | @Test | ... | ... |
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperXTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.mybatis.core.mapper; | |
| 2 | + | |
| 3 | +import com.baomidou.mybatisplus.annotation.DbType; | |
| 4 | +import com.baomidou.mybatisplus.core.conditions.Wrapper; | |
| 5 | +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | |
| 6 | +import com.baomidou.mybatisplus.core.metadata.IPage; | |
| 7 | +import com.baomidou.mybatisplus.extension.plugins.pagination.DialectFactory; | |
| 8 | +import net.sf.jsqlparser.parser.CCJSqlParserUtil; | |
| 9 | +import org.junit.jupiter.api.Test; | |
| 10 | + | |
| 11 | +import java.lang.reflect.InvocationHandler; | |
| 12 | +import java.lang.reflect.Method; | |
| 13 | +import java.lang.reflect.Proxy; | |
| 14 | +import java.util.Collections; | |
| 15 | +import java.util.List; | |
| 16 | + | |
| 17 | +import static org.junit.jupiter.api.Assertions.*; | |
| 18 | + | |
| 19 | +/** | |
| 20 | + * {@link BaseMapperX} 限量查询的单元测试 | |
| 21 | + * | |
| 22 | + * 不连接数据库:通过 JDK 动态代理执行 default 方法,记录传给 MyBatis Plus 的分页参数 | |
| 23 | + * | |
| 24 | + * @author 芋道源码 | |
| 25 | + */ | |
| 26 | +public class BaseMapperXTest { | |
| 27 | + | |
| 28 | + @Test | |
| 29 | + public void testSelectListLimitN_limit() { | |
| 30 | + FakeMapper fake = new FakeMapper(List.of("a", "b")); | |
| 31 | + QueryWrapper<String> wrapper = new QueryWrapper<String>() | |
| 32 | + .select("id", "name") | |
| 33 | + .eq("status", 1) | |
| 34 | + .orderByDesc("id"); | |
| 35 | + String sqlSegment = wrapper.getSqlSegment(); | |
| 36 | + | |
| 37 | + // 调用 | |
| 38 | + List<String> list = fake.mapper.selectListLimitN(wrapper, 5); | |
| 39 | + // 断言:第 1 页、每页 5 条、不执行 COUNT、不追加排序 | |
| 40 | + assertEquals(List.of("a", "b"), list); | |
| 41 | + assertEquals(1, fake.invocations); | |
| 42 | + assertEquals(1, fake.page.getCurrent()); | |
| 43 | + assertEquals(5, fake.page.getSize()); | |
| 44 | + assertFalse(fake.page.searchCount()); | |
| 45 | + assertTrue(fake.page.orders().isEmpty()); | |
| 46 | + // 断言:条件、排序、投影均不变,行数限制交给分页插件生成 | |
| 47 | + assertSame(wrapper, fake.wrapper); | |
| 48 | + assertEquals(sqlSegment, wrapper.getSqlSegment()); | |
| 49 | + assertEquals("id,name", wrapper.getSqlSelect()); | |
| 50 | + assertFalse(wrapper.getSqlSegment().toUpperCase().contains("LIMIT")); | |
| 51 | + } | |
| 52 | + | |
| 53 | + @Test | |
| 54 | + public void testSelectListLimitN_zero() { | |
| 55 | + FakeMapper fake = new FakeMapper(List.of("a")); | |
| 56 | + | |
| 57 | + // 调用,并断言:不查询,避免分页插件按不分页处理而查询全表 | |
| 58 | + List<String> list = fake.mapper.selectListLimitN(new QueryWrapper<String>().orderByDesc("id"), 0); | |
| 59 | + assertTrue(list.isEmpty()); | |
| 60 | + assertEquals(0, fake.invocations); | |
| 61 | + } | |
| 62 | + | |
| 63 | + @Test | |
| 64 | + public void testSelectListLimitN_negative() { | |
| 65 | + FakeMapper fake = new FakeMapper(List.of("a")); | |
| 66 | + | |
| 67 | + // 调用,并断言:分页插件会把负数的 size 视为不分页,因此直接拒绝 | |
| 68 | + assertThrows(IllegalArgumentException.class, | |
| 69 | + () -> fake.mapper.selectListLimitN(new QueryWrapper<String>().orderByDesc("id"), -1)); | |
| 70 | + assertEquals(0, fake.invocations); | |
| 71 | + } | |
| 72 | + | |
| 73 | + @Test | |
| 74 | + public void testSelectFirstOne() { | |
| 75 | + FakeMapper fake = new FakeMapper(List.of("a", "b")); | |
| 76 | + QueryWrapper<String> wrapper = new QueryWrapper<String>().eq("status", 1).orderByAsc("id"); | |
| 77 | + | |
| 78 | + // 调用,并断言:只取 1 条,返回第 1 条 | |
| 79 | + assertEquals("a", fake.mapper.selectFirstOne(wrapper)); | |
| 80 | + assertEquals(1, fake.invocations); | |
| 81 | + assertEquals(1, fake.page.getCurrent()); | |
| 82 | + assertEquals(1, fake.page.getSize()); | |
| 83 | + assertFalse(fake.page.searchCount()); | |
| 84 | + assertSame(wrapper, fake.wrapper); | |
| 85 | + } | |
| 86 | + | |
| 87 | + @Test | |
| 88 | + public void testSelectFirstOne_empty() { | |
| 89 | + FakeMapper fake = new FakeMapper(Collections.emptyList()); | |
| 90 | + | |
| 91 | + // 调用,并断言:不存在时返回 null | |
| 92 | + assertNull(fake.mapper.selectFirstOne(new QueryWrapper<String>().orderByAsc("id"))); | |
| 93 | + assertEquals(1, fake.invocations); | |
| 94 | + } | |
| 95 | + | |
| 96 | + /** | |
| 97 | + * 分页插件按执行连接识别的数据库类型生成的分页语句(第 1 页的 offset 为 0),均可被 JSqlParser 解析 | |
| 98 | + * | |
| 99 | + * 注意:SQL Server 的 OFFSET FETCH 要求原语句有 ORDER BY,因此调用方必须设置排序 | |
| 100 | + */ | |
| 101 | + @Test | |
| 102 | + public void testPaginationDialect() throws Exception { | |
| 103 | + String sql = "SELECT id, name FROM system_users WHERE status = ? ORDER BY id DESC"; | |
| 104 | + | |
| 105 | + // MySQL、OSCAR 神通等:LIMIT | |
| 106 | + assertEquals(sql + " LIMIT ?", paginate(DbType.MYSQL, sql)); | |
| 107 | + assertEquals(sql + " LIMIT ?", paginate(DbType.OSCAR, sql)); | |
| 108 | + // PostgreSQL、H2、人大金仓、openGauss、瀚高等:LIMIT | |
| 109 | + assertEquals(sql + " LIMIT ?", paginate(DbType.POSTGRE_SQL, sql)); | |
| 110 | + assertEquals(sql + " LIMIT ?", paginate(DbType.H2, sql)); | |
| 111 | + assertEquals(sql + " LIMIT ?", paginate(DbType.KINGBASE_ES, sql)); | |
| 112 | + assertEquals(sql + " LIMIT ?", paginate(DbType.OPENGAUSS, sql)); | |
| 113 | + assertEquals(sql + " LIMIT ?", paginate(DbType.HIGH_GO, sql)); | |
| 114 | + // Oracle、达梦:ROWNUM 包裹原语句,先排序后取行,兼容 Oracle 11g | |
| 115 | + String oracle = "SELECT * FROM ( SELECT TMP.*, ROWNUM ROW_ID FROM ( " + sql | |
| 116 | + + " ) TMP WHERE ROWNUM <=?) WHERE ROW_ID > ?"; | |
| 117 | + assertEquals(oracle, paginate(DbType.ORACLE, sql)); | |
| 118 | + assertEquals(oracle, paginate(DbType.DM, sql)); | |
| 119 | + // SQL Server 2012+:OFFSET FETCH 直接追加在 ORDER BY 之后 | |
| 120 | + assertEquals(sql + " OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", paginate(DbType.SQL_SERVER, sql)); | |
| 121 | + | |
| 122 | + for (DbType dbType : List.of(DbType.MYSQL, DbType.POSTGRE_SQL, DbType.ORACLE, DbType.SQL_SERVER)) { | |
| 123 | + assertNotNull(CCJSqlParserUtil.parse(paginate(dbType, sql)), dbType.name()); | |
| 124 | + } | |
| 125 | + } | |
| 126 | + | |
| 127 | + private static String paginate(DbType dbType, String sql) { | |
| 128 | + return DialectFactory.getDialect(dbType).buildPaginationSql(sql, 0, 5).getDialectSql(); | |
| 129 | + } | |
| 130 | + | |
| 131 | + /** | |
| 132 | + * 基于 JDK 动态代理的 {@link BaseMapperX}:default 方法真实执行,selectList(IPage, Wrapper) 记录参数并返回固定结果 | |
| 133 | + * | |
| 134 | + * @author 芋道源码 | |
| 135 | + */ | |
| 136 | + private static class FakeMapper implements InvocationHandler { | |
| 137 | + | |
| 138 | + private final List<String> records; | |
| 139 | + private final BaseMapperX<String> mapper; | |
| 140 | + private IPage<?> page; | |
| 141 | + private Wrapper<?> wrapper; | |
| 142 | + private int invocations; | |
| 143 | + | |
| 144 | + @SuppressWarnings("unchecked") | |
| 145 | + FakeMapper(List<String> records) { | |
| 146 | + this.records = records; | |
| 147 | + this.mapper = (BaseMapperX<String>) Proxy.newProxyInstance(getClass().getClassLoader(), | |
| 148 | + new Class<?>[]{BaseMapperX.class}, this); | |
| 149 | + } | |
| 150 | + | |
| 151 | + @Override | |
| 152 | + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { | |
| 153 | + if (method.isDefault()) { | |
| 154 | + return InvocationHandler.invokeDefault(proxy, method, args); | |
| 155 | + } | |
| 156 | + if ("selectList".equals(method.getName()) && args != null && args.length == 2 | |
| 157 | + && args[0] instanceof IPage && args[1] instanceof Wrapper) { | |
| 158 | + invocations++; | |
| 159 | + page = (IPage<?>) args[0]; | |
| 160 | + wrapper = (Wrapper<?>) args[1]; | |
| 161 | + return records; | |
| 162 | + } | |
| 163 | + throw new UnsupportedOperationException(method.getName()); | |
| 164 | + } | |
| 165 | + | |
| 166 | + } | |
| 167 | + | |
| 168 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtilsTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.mybatis.core.util; | |
| 2 | + | |
| 3 | +import cn.hutool.core.exceptions.UtilException; | |
| 4 | +import cn.hutool.extra.spring.SpringUtil; | |
| 5 | +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; | |
| 6 | +import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; | |
| 7 | +import com.baomidou.mybatisplus.annotation.DbType; | |
| 8 | +import org.junit.jupiter.api.AfterEach; | |
| 9 | +import org.junit.jupiter.api.BeforeEach; | |
| 10 | +import org.junit.jupiter.api.Test; | |
| 11 | +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; | |
| 12 | +import org.springframework.beans.factory.support.DefaultListableBeanFactory; | |
| 13 | +import org.springframework.jdbc.datasource.ConnectionHolder; | |
| 14 | +import org.springframework.transaction.support.TransactionSynchronizationManager; | |
| 15 | + | |
| 16 | +import javax.sql.DataSource; | |
| 17 | +import java.lang.reflect.InvocationHandler; | |
| 18 | +import java.lang.reflect.Method; | |
| 19 | +import java.lang.reflect.Proxy; | |
| 20 | +import java.sql.Connection; | |
| 21 | +import java.sql.DatabaseMetaData; | |
| 22 | +import java.sql.SQLException; | |
| 23 | +import java.util.Collections; | |
| 24 | + | |
| 25 | +import static org.junit.jupiter.api.Assertions.*; | |
| 26 | + | |
| 27 | +/** | |
| 28 | + * {@link JdbcUtils} 的单元测试 | |
| 29 | + * | |
| 30 | + * @author 芋道源码 | |
| 31 | + */ | |
| 32 | +public class JdbcUtilsTest { | |
| 33 | + | |
| 34 | + private ConfigurableListableBeanFactory originBeanFactory; | |
| 35 | + | |
| 36 | + @BeforeEach | |
| 37 | + public void setUp() { | |
| 38 | + try { | |
| 39 | + originBeanFactory = SpringUtil.getConfigurableBeanFactory(); | |
| 40 | + } catch (UtilException e) { | |
| 41 | + originBeanFactory = null; | |
| 42 | + } | |
| 43 | + } | |
| 44 | + | |
| 45 | + @AfterEach | |
| 46 | + public void tearDown() { | |
| 47 | + DynamicDataSourceContextHolder.clear(); | |
| 48 | + new SpringUtil().postProcessBeanFactory(originBeanFactory); | |
| 49 | + } | |
| 50 | + | |
| 51 | + @Test | |
| 52 | + public void testGetDbType_cachedPerDataSource() { | |
| 53 | + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a"); | |
| 54 | + FakeDataSource sqlServer = new FakeDataSource("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433"); | |
| 55 | + | |
| 56 | + for (int i = 0; i < 3; i++) { | |
| 57 | + assertEquals(DbType.MYSQL, JdbcUtils.getDbType(mysql.getDataSource())); | |
| 58 | + assertEquals(DbType.SQL_SERVER, JdbcUtils.getDbType(sqlServer.getDataSource())); | |
| 59 | + } | |
| 60 | + assertEquals(1, mysql.getConnectionCount()); | |
| 61 | + assertEquals(1, sqlServer.getConnectionCount()); | |
| 62 | + } | |
| 63 | + | |
| 64 | + @Test | |
| 65 | + public void testGetDbType_followsDynamicRouting() { | |
| 66 | + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a"); | |
| 67 | + FakeDataSource oracle = new FakeDataSource("Oracle", "jdbc:oracle:thin:@127.0.0.1:1521:xe"); | |
| 68 | + DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(Collections.emptyList()); | |
| 69 | + dynamicRoutingDataSource.setPrimary("master"); | |
| 70 | + dynamicRoutingDataSource.addDataSource("master", postgresql.getDataSource()); | |
| 71 | + dynamicRoutingDataSource.addDataSource("oracle", oracle.getDataSource()); | |
| 72 | + mockBeanFactory("dynamicRoutingDataSource", dynamicRoutingDataSource); | |
| 73 | + | |
| 74 | + for (int i = 0; i < 3; i++) { | |
| 75 | + assertSame(postgresql.getDataSource(), JdbcUtils.getDataSource()); | |
| 76 | + assertEquals(DbType.POSTGRE_SQL, JdbcUtils.getDbType()); | |
| 77 | + DynamicDataSourceContextHolder.push("oracle"); | |
| 78 | + assertSame(oracle.getDataSource(), JdbcUtils.getDataSource()); | |
| 79 | + assertEquals(DbType.ORACLE, JdbcUtils.getDbType()); | |
| 80 | + DynamicDataSourceContextHolder.poll(); | |
| 81 | + } | |
| 82 | + assertEquals(1, postgresql.getConnectionCount()); | |
| 83 | + assertEquals(1, oracle.getConnectionCount()); | |
| 84 | + } | |
| 85 | + | |
| 86 | + @Test | |
| 87 | + public void testGetDbType_withoutDynamicRoutingDataSource() { | |
| 88 | + FakeDataSource h2 = new FakeDataSource("H2", "jdbc:h2:mem:testdb;MODE=MYSQL"); | |
| 89 | + mockBeanFactory("dataSource", h2.getDataSource()); | |
| 90 | + | |
| 91 | + assertSame(h2.getDataSource(), JdbcUtils.getDataSource()); | |
| 92 | + assertEquals(DbType.H2, JdbcUtils.getDbType()); | |
| 93 | + } | |
| 94 | + | |
| 95 | + @Test | |
| 96 | + public void testGetDbType_transactionConnectionDoesNotBorrowOrClose() throws SQLException { | |
| 97 | + FakeDataSource h2 = new FakeDataSource("H2", "jdbc:h2:mem:single_connection"); | |
| 98 | + mockBeanFactory("dataSource", h2.getDataSource()); | |
| 99 | + Connection held = h2.getDataSource().getConnection(); | |
| 100 | + h2.setFailOnConnection(true); | |
| 101 | + TransactionSynchronizationManager.bindResource(h2.getDataSource(), new ConnectionHolder(held)); | |
| 102 | + try { | |
| 103 | + assertEquals(DbType.H2, JdbcUtils.getDbType()); | |
| 104 | + assertEquals(1, h2.getConnectionCount()); | |
| 105 | + assertEquals(0, h2.closeCount); | |
| 106 | + } finally { | |
| 107 | + TransactionSynchronizationManager.unbindResource(h2.getDataSource()); | |
| 108 | + held.close(); | |
| 109 | + } | |
| 110 | + } | |
| 111 | + | |
| 112 | + @Test | |
| 113 | + public void testGetDbType_transactionUsesBoundConnectionWithoutPoisoningRouteCache() throws SQLException { | |
| 114 | + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://probe/transaction"); | |
| 115 | + FakeDataSource oracle = new FakeDataSource("Oracle", "jdbc:oracle:thin:@probe"); | |
| 116 | + DynamicRoutingDataSource routing = new DynamicRoutingDataSource(Collections.emptyList()); | |
| 117 | + routing.setPrimary("master"); | |
| 118 | + routing.addDataSource("master", postgresql.getDataSource()); | |
| 119 | + routing.addDataSource("oracle", oracle.getDataSource()); | |
| 120 | + mockBeanFactory("dynamicRoutingDataSource", routing); | |
| 121 | + Connection held = postgresql.getDataSource().getConnection(); | |
| 122 | + TransactionSynchronizationManager.bindResource(routing, new ConnectionHolder(held)); | |
| 123 | + DynamicDataSourceContextHolder.push("oracle"); | |
| 124 | + try { | |
| 125 | + assertEquals(DbType.POSTGRE_SQL, JdbcUtils.getDbType()); | |
| 126 | + assertEquals(1, postgresql.getConnectionCount()); | |
| 127 | + assertEquals(0, oracle.getConnectionCount()); | |
| 128 | + assertEquals(0, postgresql.closeCount); | |
| 129 | + } finally { | |
| 130 | + TransactionSynchronizationManager.unbindResource(routing); | |
| 131 | + held.close(); | |
| 132 | + } | |
| 133 | + assertEquals(DbType.ORACLE, JdbcUtils.getDbType()); | |
| 134 | + assertEquals(1, oracle.getConnectionCount()); | |
| 135 | + } | |
| 136 | + | |
| 137 | + @Test | |
| 138 | + public void testGetDbType_unregisteredProductName() { | |
| 139 | + // 产品名未登记时,按明确的原生驱动 URL 识别,与 getDbType(url) 的归类一致 | |
| 140 | + assertEquals(DbType.POSTGRE_SQL, JdbcUtils.getDbType( | |
| 141 | + new FakeDataSource("HighGo", "jdbc:highgo://127.0.0.1:5866/highgo").getDataSource())); | |
| 142 | + assertEquals(DbType.MARIADB, JdbcUtils.getDbType( | |
| 143 | + new FakeDataSource("MariaDB", "jdbc:mariadb://127.0.0.1:3306/a").getDataSource())); | |
| 144 | + | |
| 145 | + // 完全未识别的产品:明确异常,而不是 NPE;异常信息不包含 URL | |
| 146 | + FakeDataSource sqlite = new FakeDataSource("SQLite", "jdbc:sqlite:/tmp/test.db?user=root&password=secret"); | |
| 147 | + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, | |
| 148 | + () -> JdbcUtils.getDbType(sqlite.getDataSource())); | |
| 149 | + assertTrue(ex.getMessage().contains("SQLite")); | |
| 150 | + assertFalse(ex.getMessage().contains("secret")); | |
| 151 | + } | |
| 152 | + | |
| 153 | + @Test | |
| 154 | + public void testGetDbType_blankProductName() { | |
| 155 | + assertNull(JdbcUtils.getDbType(new FakeDataSource("", "jdbc:mysql://127.0.0.1:3306/a").getDataSource())); | |
| 156 | + } | |
| 157 | + | |
| 158 | + @Test | |
| 159 | + public void testGetDbType_failureNotCached() { | |
| 160 | + FakeDataSource broken = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a"); | |
| 161 | + broken.setFailOnConnection(true); | |
| 162 | + assertThrows(IllegalArgumentException.class, () -> JdbcUtils.getDbType(broken.getDataSource())); | |
| 163 | + assertThrows(IllegalArgumentException.class, () -> JdbcUtils.getDbType(broken.getDataSource())); | |
| 164 | + assertEquals(2, broken.getConnectionCount()); | |
| 165 | + | |
| 166 | + broken.setFailOnConnection(false); | |
| 167 | + assertEquals(DbType.MYSQL, JdbcUtils.getDbType(broken.getDataSource())); | |
| 168 | + } | |
| 169 | + | |
| 170 | + private static void mockBeanFactory(String beanName, Object bean) { | |
| 171 | + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); | |
| 172 | + beanFactory.registerSingleton(beanName, bean); | |
| 173 | + new SpringUtil().postProcessBeanFactory(beanFactory); | |
| 174 | + } | |
| 175 | + | |
| 176 | + /** | |
| 177 | + * 基于 JDK 动态代理的数据源,只提供识别 DB 类型所需的元数据,并记录获取连接的次数 | |
| 178 | + */ | |
| 179 | + private static class FakeDataSource implements InvocationHandler { | |
| 180 | + | |
| 181 | + private final String productName; | |
| 182 | + private final String url; | |
| 183 | + private final DataSource dataSource; | |
| 184 | + private int connectionCount; | |
| 185 | + private int closeCount; | |
| 186 | + private boolean failOnConnection; | |
| 187 | + | |
| 188 | + FakeDataSource(String productName, String url) { | |
| 189 | + this.productName = productName; | |
| 190 | + this.url = url; | |
| 191 | + this.dataSource = proxy(DataSource.class); | |
| 192 | + } | |
| 193 | + | |
| 194 | + DataSource getDataSource() { | |
| 195 | + return dataSource; | |
| 196 | + } | |
| 197 | + | |
| 198 | + int getConnectionCount() { | |
| 199 | + return connectionCount; | |
| 200 | + } | |
| 201 | + | |
| 202 | + void setFailOnConnection(boolean failOnConnection) { | |
| 203 | + this.failOnConnection = failOnConnection; | |
| 204 | + } | |
| 205 | + | |
| 206 | + @SuppressWarnings("unchecked") | |
| 207 | + private <T> T proxy(Class<T> type) { | |
| 208 | + return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[]{type}, this); | |
| 209 | + } | |
| 210 | + | |
| 211 | + @Override | |
| 212 | + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { | |
| 213 | + switch (method.getName()) { | |
| 214 | + case "hashCode": | |
| 215 | + return System.identityHashCode(proxy); | |
| 216 | + case "equals": | |
| 217 | + return proxy == args[0]; | |
| 218 | + case "toString": | |
| 219 | + return "FakeDataSource(" + productName + ")"; | |
| 220 | + case "getConnection": | |
| 221 | + connectionCount++; | |
| 222 | + if (failOnConnection) { | |
| 223 | + throw new SQLException("connection refused"); | |
| 224 | + } | |
| 225 | + return proxy(Connection.class); | |
| 226 | + case "getMetaData": | |
| 227 | + return proxy(DatabaseMetaData.class); | |
| 228 | + case "close": | |
| 229 | + closeCount++; | |
| 230 | + return null; | |
| 231 | + case "getDatabaseProductName": | |
| 232 | + return productName; | |
| 233 | + case "getURL": | |
| 234 | + return url; | |
| 235 | + default: | |
| 236 | + throw new UnsupportedOperationException(method.getName()); | |
| 237 | + } | |
| 238 | + } | |
| 239 | + | |
| 240 | + } | |
| 241 | + | |
| 242 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java
| ... | ... | @@ -18,12 +18,57 @@ import static org.junit.jupiter.api.Assertions.assertFalse; |
| 18 | 18 | import static org.junit.jupiter.api.Assertions.assertThrows; |
| 19 | 19 | import static org.junit.jupiter.api.Assertions.assertTrue; |
| 20 | 20 | |
| 21 | +// TODO DONE @AI:参考 system user test 的风格;统一准备参数、调用、断言分区。 | |
| 21 | 22 | /** |
| 22 | 23 | * {@link MyBatisUtils} 的单元测试 |
| 23 | 24 | */ |
| 24 | 25 | public class MyBatisUtilsTest { |
| 25 | 26 | |
| 26 | 27 | @Test |
| 28 | + public void testBuildPage_offset() { | |
| 29 | + // 准备参数 | |
| 30 | + long offset = 3; | |
| 31 | + long size = 10; | |
| 32 | + | |
| 33 | + // 调用 | |
| 34 | + Page<Object> page = MyBatisUtils.buildPage(offset, size, false); | |
| 35 | + | |
| 36 | + // 断言 | |
| 37 | + assertEquals(3L, page.offset()); | |
| 38 | + assertEquals(10L, page.getSize()); | |
| 39 | + assertFalse(page.searchCount()); | |
| 40 | + } | |
| 41 | + | |
| 42 | + @Test | |
| 43 | + public void testBuildPage_negativeOffset() { | |
| 44 | + // 准备参数 | |
| 45 | + long offset = -1; | |
| 46 | + long size = 10; | |
| 47 | + | |
| 48 | + // 调用 | |
| 49 | + Page<Object> page = MyBatisUtils.buildPage(offset, size, true); | |
| 50 | + | |
| 51 | + // 断言 | |
| 52 | + assertEquals(0L, page.offset()); | |
| 53 | + assertEquals(10L, page.getSize()); | |
| 54 | + assertTrue(page.searchCount()); | |
| 55 | + assertEquals(0L, MyBatisUtils.buildPage(0, 10, false).offset()); | |
| 56 | + } | |
| 57 | + | |
| 58 | + @Test | |
| 59 | + public void testBuildPage_largeOffset() { | |
| 60 | + // 准备参数 | |
| 61 | + long offset = (long) Integer.MAX_VALUE + 3; | |
| 62 | + | |
| 63 | + // 调用 | |
| 64 | + Page<Object> page = MyBatisUtils.buildPage(offset, 10, false); | |
| 65 | + | |
| 66 | + // 断言 | |
| 67 | + assertEquals(offset, page.offset()); | |
| 68 | + assertEquals(Long.MAX_VALUE, MyBatisUtils.buildPage(Long.MAX_VALUE, 10, false).offset()); | |
| 69 | + } | |
| 70 | + | |
| 71 | + @Test | |
| 27 | 72 | public void testBuildPage_sortingFields() { |
| 28 | 73 | // 准备参数 |
| 29 | 74 | PageParam pageParam = new PageParam(); |
| ... | ... | @@ -94,15 +139,28 @@ public class MyBatisUtilsTest { |
| 94 | 139 | |
| 95 | 140 | @Test |
| 96 | 141 | public void testOrderDirection() { |
| 97 | - assertTrue(MyBatisUtils.isAscOrder(SortingField.ORDER_ASC)); | |
| 98 | - assertFalse(MyBatisUtils.isAscOrder(SortingField.ORDER_DESC)); | |
| 99 | - assertEquals("ASC", MyBatisUtils.getOrderDirection(SortingField.ORDER_ASC)); | |
| 100 | - assertEquals("DESC", MyBatisUtils.getOrderDirection(SortingField.ORDER_DESC)); | |
| 101 | - assertEquals("DESC", MyBatisUtils.getOrderDirection(null)); | |
| 142 | + // 准备参数 | |
| 143 | + String ascOrder = SortingField.ORDER_ASC; | |
| 144 | + String descOrder = SortingField.ORDER_DESC; | |
| 145 | + | |
| 146 | + // 调用 | |
| 147 | + boolean asc = MyBatisUtils.isAscOrder(ascOrder); | |
| 148 | + boolean desc = MyBatisUtils.isAscOrder(descOrder); | |
| 149 | + String ascDirection = MyBatisUtils.getOrderDirection(ascOrder); | |
| 150 | + String descDirection = MyBatisUtils.getOrderDirection(descOrder); | |
| 151 | + String defaultDirection = MyBatisUtils.getOrderDirection(null); | |
| 152 | + | |
| 153 | + // 断言 | |
| 154 | + assertTrue(asc); | |
| 155 | + assertFalse(desc); | |
| 156 | + assertEquals("ASC", ascDirection); | |
| 157 | + assertEquals("DESC", descDirection); | |
| 158 | + assertEquals("DESC", defaultDirection); | |
| 102 | 159 | } |
| 103 | 160 | |
| 104 | 161 | @Test |
| 105 | 162 | public void testFindInSet() { |
| 163 | + // 调用,并断言各数据库模板保持原有参数序号和列名 | |
| 106 | 164 | assertEquals("FIND_IN_SET({0}, websites) <> 0", |
| 107 | 165 | MyBatisUtils.findInSet(DbType.MYSQL, "websites", 0)); |
| 108 | 166 | assertEquals("POSITION(',' || CAST({0} AS VARCHAR) || ',' IN ',' || websites || ',') > 0", |
| ... | ... | @@ -121,6 +179,7 @@ public class MyBatisUtilsTest { |
| 121 | 179 | |
| 122 | 180 | @Test |
| 123 | 181 | public void testFindInSet_invalidColumnName() { |
| 182 | + // 调用,并断言异常 | |
| 124 | 183 | assertThrows(IllegalArgumentException.class, |
| 125 | 184 | () -> MyBatisUtils.findInSet(DbType.MYSQL, "websites;drop table system_tenant", 0)); |
| 126 | 185 | assertThrows(IllegalArgumentException.class, |
| ... | ... | @@ -129,12 +188,14 @@ public class MyBatisUtilsTest { |
| 129 | 188 | |
| 130 | 189 | @Test |
| 131 | 190 | public void testFindInSet_invalidParamIndex() { |
| 191 | + // 调用,并断言异常 | |
| 132 | 192 | assertThrows(IllegalArgumentException.class, |
| 133 | 193 | () -> MyBatisUtils.findInSet(DbType.MYSQL, "websites", -1)); |
| 134 | 194 | } |
| 135 | 195 | |
| 136 | 196 | @Test |
| 137 | 197 | public void testFindInSet_multipleValues() { |
| 198 | + // 调用,并断言多个值使用独立参数序号;空集合拒绝生成条件 | |
| 138 | 199 | assertEquals("FIND_IN_SET({0}, tag_ids) <> 0 OR FIND_IN_SET({1}, tag_ids) <> 0", |
| 139 | 200 | MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", Arrays.asList(1L, 2L))); |
| 140 | 201 | assertThrows(IllegalArgumentException.class, | ... | ... |
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/SqlDialectUtilsTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.mybatis.core.util; | |
| 2 | + | |
| 3 | +import cn.hutool.core.exceptions.UtilException; | |
| 4 | +import cn.hutool.extra.spring.SpringUtil; | |
| 5 | +import cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils.Dialect; | |
| 6 | +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; | |
| 7 | +import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; | |
| 8 | +import net.sf.jsqlparser.parser.CCJSqlParserUtil; | |
| 9 | +import org.apache.ibatis.mapping.BoundSql; | |
| 10 | +import org.apache.ibatis.mapping.ParameterMapping; | |
| 11 | +import org.apache.ibatis.mapping.SqlSource; | |
| 12 | +import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; | |
| 13 | +import org.apache.ibatis.session.Configuration; | |
| 14 | +import org.junit.jupiter.api.AfterEach; | |
| 15 | +import org.junit.jupiter.api.BeforeEach; | |
| 16 | +import org.junit.jupiter.api.Test; | |
| 17 | +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; | |
| 18 | +import org.springframework.beans.factory.support.DefaultListableBeanFactory; | |
| 19 | +import org.springframework.jdbc.datasource.ConnectionHolder; | |
| 20 | +import org.springframework.transaction.support.TransactionSynchronizationManager; | |
| 21 | + | |
| 22 | +import javax.sql.DataSource; | |
| 23 | +import java.lang.reflect.InvocationHandler; | |
| 24 | +import java.lang.reflect.Method; | |
| 25 | +import java.lang.reflect.Proxy; | |
| 26 | +import java.sql.Connection; | |
| 27 | +import java.sql.DatabaseMetaData; | |
| 28 | +import java.sql.SQLException; | |
| 29 | +import java.time.LocalDateTime; | |
| 30 | +import java.util.Collections; | |
| 31 | +import java.util.HashMap; | |
| 32 | +import java.util.List; | |
| 33 | +import java.util.Map; | |
| 34 | +import java.util.function.BiFunction; | |
| 35 | + | |
| 36 | +import static org.junit.jupiter.api.Assertions.*; | |
| 37 | + | |
| 38 | +/** | |
| 39 | + * {@link SqlDialectUtils} 的单元测试 | |
| 40 | + * | |
| 41 | + * @author 芋道源码 | |
| 42 | + */ | |
| 43 | +public class SqlDialectUtilsTest { | |
| 44 | + | |
| 45 | + private static final String P = "${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@"; | |
| 46 | + | |
| 47 | + private ConfigurableListableBeanFactory originBeanFactory; | |
| 48 | + | |
| 49 | + @BeforeEach | |
| 50 | + public void setUp() { | |
| 51 | + try { | |
| 52 | + originBeanFactory = SpringUtil.getConfigurableBeanFactory(); | |
| 53 | + } catch (UtilException e) { | |
| 54 | + originBeanFactory = null; | |
| 55 | + } | |
| 56 | + } | |
| 57 | + | |
| 58 | + @AfterEach | |
| 59 | + public void tearDown() { | |
| 60 | + DynamicDataSourceContextHolder.clear(); | |
| 61 | + new SpringUtil().postProcessBeanFactory(originBeanFactory); | |
| 62 | + } | |
| 63 | + | |
| 64 | + // ========== SQL 片段 ========== | |
| 65 | + | |
| 66 | + @Test | |
| 67 | + public void testFormatDate() { | |
| 68 | + assertEquals("DATE_FORMAT(create_time, '%Y-%m-%d')", SqlDialectUtils.formatDate(Dialect.ORIGINAL, "create_time")); | |
| 69 | + assertEquals("TO_CHAR(t.create_time, 'YYYY-MM-DD')", SqlDialectUtils.formatDate(Dialect.POSTGRESQL, "t.create_time")); | |
| 70 | + assertEquals("TO_CHAR(create_time, 'YYYY-MM-DD')", SqlDialectUtils.formatDate(Dialect.ORACLE, "create_time")); | |
| 71 | + assertEquals("CONVERT(varchar(10), create_time, 23)", SqlDialectUtils.formatDate(Dialect.SQLSERVER, "create_time")); | |
| 72 | + } | |
| 73 | + | |
| 74 | + @Test | |
| 75 | + public void testFormatYearMonth() { | |
| 76 | + assertEquals("DATE_FORMAT(order_date, '%Y%m')", SqlDialectUtils.formatYearMonth(Dialect.ORIGINAL, "order_date")); | |
| 77 | + assertEquals("TO_CHAR(order_date, 'YYYYMM')", SqlDialectUtils.formatYearMonth(Dialect.POSTGRESQL, "order_date")); | |
| 78 | + assertEquals("TO_CHAR(order_date, 'YYYYMM')", SqlDialectUtils.formatYearMonth(Dialect.ORACLE, "order_date")); | |
| 79 | + assertEquals("CONVERT(varchar(6), order_date, 112)", SqlDialectUtils.formatYearMonth(Dialect.SQLSERVER, "order_date")); | |
| 80 | + } | |
| 81 | + | |
| 82 | + @Test | |
| 83 | + public void testFormatYearDashMonth() { | |
| 84 | + assertEquals("CONCAT(EXTRACT(YEAR FROM pay_time), '-', LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0'))", | |
| 85 | + SqlDialectUtils.formatYearDashMonth(Dialect.ORIGINAL, "pay_time")); | |
| 86 | + assertEquals("TO_CHAR(pay_time, 'YYYY-MM')", SqlDialectUtils.formatYearDashMonth(Dialect.POSTGRESQL, "pay_time")); | |
| 87 | + assertEquals("TO_CHAR(pay_time, 'YYYY-MM')", SqlDialectUtils.formatYearDashMonth(Dialect.ORACLE, "pay_time")); | |
| 88 | + assertEquals("CONVERT(varchar(7), pay_time, 120)", SqlDialectUtils.formatYearDashMonth(Dialect.SQLSERVER, "pay_time")); | |
| 89 | + } | |
| 90 | + | |
| 91 | + @Test | |
| 92 | + public void testDate() { | |
| 93 | + assertEquals("DATE(send_time)", SqlDialectUtils.date(Dialect.ORIGINAL, "send_time")); | |
| 94 | + assertEquals("TO_CHAR(send_time, 'YYYY-MM-DD')", SqlDialectUtils.date(Dialect.POSTGRESQL, "send_time")); | |
| 95 | + assertEquals("TO_CHAR(send_time, 'YYYY-MM-DD')", SqlDialectUtils.date(Dialect.ORACLE, "send_time")); | |
| 96 | + assertEquals("CONVERT(varchar(10), send_time, 23)", SqlDialectUtils.date(Dialect.SQLSERVER, "send_time")); | |
| 97 | + } | |
| 98 | + | |
| 99 | + @Test | |
| 100 | + public void testCastDateText() { | |
| 101 | + assertEquals("CAST(create_time AS DATE)", SqlDialectUtils.castDateText(Dialect.ORIGINAL, "create_time")); | |
| 102 | + assertEquals("TO_CHAR(create_time, 'YYYY-MM-DD')", SqlDialectUtils.castDateText(Dialect.POSTGRESQL, "create_time")); | |
| 103 | + assertEquals("TO_CHAR(create_time, 'YYYY-MM-DD')", SqlDialectUtils.castDateText(Dialect.ORACLE, "create_time")); | |
| 104 | + assertEquals("CONVERT(varchar(10), create_time, 23)", SqlDialectUtils.castDateText(Dialect.SQLSERVER, "create_time")); | |
| 105 | + } | |
| 106 | + | |
| 107 | + @Test | |
| 108 | + public void testCastDate() { | |
| 109 | + assertEquals("CAST(create_time AS DATE)", SqlDialectUtils.castDate(Dialect.ORIGINAL, "create_time")); | |
| 110 | + assertEquals("CAST(create_time AS DATE)", SqlDialectUtils.castDate(Dialect.POSTGRESQL, "create_time")); | |
| 111 | + assertEquals("TRUNC(create_time)", SqlDialectUtils.castDate(Dialect.ORACLE, "create_time")); | |
| 112 | + assertEquals("CAST(create_time AS DATE)", SqlDialectUtils.castDate(Dialect.SQLSERVER, "create_time")); | |
| 113 | + } | |
| 114 | + | |
| 115 | + @Test | |
| 116 | + public void testMonth() { | |
| 117 | + assertEquals("MONTH(order_date)", SqlDialectUtils.month(Dialect.ORIGINAL, "order_date")); | |
| 118 | + assertEquals("CAST(EXTRACT(MONTH FROM order_date) AS INTEGER)", SqlDialectUtils.month(Dialect.POSTGRESQL, "order_date")); | |
| 119 | + assertEquals("EXTRACT(MONTH FROM order_date)", SqlDialectUtils.month(Dialect.ORACLE, "order_date")); | |
| 120 | + assertEquals("MONTH(order_date)", SqlDialectUtils.month(Dialect.SQLSERVER, "order_date")); | |
| 121 | + } | |
| 122 | + | |
| 123 | + @Test | |
| 124 | + public void testYear() { | |
| 125 | + assertEquals("YEAR(return_time)", SqlDialectUtils.year(Dialect.ORIGINAL, "return_time")); | |
| 126 | + assertEquals("CAST(EXTRACT(YEAR FROM return_time) AS INTEGER)", SqlDialectUtils.year(Dialect.POSTGRESQL, "return_time")); | |
| 127 | + assertEquals("EXTRACT(YEAR FROM return_time)", SqlDialectUtils.year(Dialect.ORACLE, "return_time")); | |
| 128 | + assertEquals("YEAR(return_time)", SqlDialectUtils.year(Dialect.SQLSERVER, "return_time")); | |
| 129 | + } | |
| 130 | + | |
| 131 | + @Test | |
| 132 | + public void testDayOfMonth() { | |
| 133 | + assertEquals("DAYOFMONTH(birthday)", SqlDialectUtils.dayOfMonth(Dialect.ORIGINAL, "birthday")); | |
| 134 | + assertEquals("CAST(EXTRACT(DAY FROM birthday) AS INTEGER)", SqlDialectUtils.dayOfMonth(Dialect.POSTGRESQL, "birthday")); | |
| 135 | + assertEquals("EXTRACT(DAY FROM birthday)", SqlDialectUtils.dayOfMonth(Dialect.ORACLE, "birthday")); | |
| 136 | + assertEquals("DAY(birthday)", SqlDialectUtils.dayOfMonth(Dialect.SQLSERVER, "birthday")); | |
| 137 | + } | |
| 138 | + | |
| 139 | + @Test | |
| 140 | + public void testCustomerDealCycle() { | |
| 141 | + String original = "COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0)"; | |
| 142 | + assertEquals(original, SqlDialectUtils.customerDealCycle(Dialect.ORIGINAL, "customer.create_time", "contract.order_date")); | |
| 143 | + // PostgreSQL:两个 TIMESTAMP 相减为 interval,EXTRACT(DAY) 即完整天数(向零截断、保留负号),TRUNC 截断保留 1 位 | |
| 144 | + assertEquals("COALESCE(TRUNC(AVG(CAST(EXTRACT(DAY FROM (CAST(contract.order_date AS TIMESTAMP) - " | |
| 145 | + + "CAST(customer.create_time AS TIMESTAMP))) AS NUMERIC)), 1), 0)", | |
| 146 | + SqlDialectUtils.customerDealCycle(Dialect.POSTGRESQL, "customer.create_time", "contract.order_date")); | |
| 147 | + // Oracle、SQL Server:完整天数表达式依赖实际列类型,尚未验收,保持原有片段 | |
| 148 | + assertEquals(original, SqlDialectUtils.customerDealCycle(Dialect.ORACLE, "customer.create_time", "contract.order_date")); | |
| 149 | + assertEquals(original, SqlDialectUtils.customerDealCycle(Dialect.SQLSERVER, "customer.create_time", "contract.order_date")); | |
| 150 | + } | |
| 151 | + | |
| 152 | + @Test | |
| 153 | + public void testFromDual() { | |
| 154 | + assertEquals("", SqlDialectUtils.fromDual(Dialect.ORIGINAL)); | |
| 155 | + assertEquals("", SqlDialectUtils.fromDual(Dialect.POSTGRESQL)); | |
| 156 | + assertEquals("FROM DUAL", SqlDialectUtils.fromDual(Dialect.ORACLE)); | |
| 157 | + assertEquals("", SqlDialectUtils.fromDual(Dialect.SQLSERVER)); | |
| 158 | + } | |
| 159 | + | |
| 160 | + @Test | |
| 161 | + public void testInvalidColumnName() { | |
| 162 | + List<BiFunction<Dialect, String, String>> fragments = List.of(SqlDialectUtils::formatDate, | |
| 163 | + SqlDialectUtils::formatYearMonth, SqlDialectUtils::formatYearDashMonth, SqlDialectUtils::date, | |
| 164 | + SqlDialectUtils::castDateText, SqlDialectUtils::castDate, SqlDialectUtils::month, | |
| 165 | + SqlDialectUtils::year, SqlDialectUtils::dayOfMonth); | |
| 166 | + for (BiFunction<Dialect, String, String> fragment : fragments) { | |
| 167 | + for (Dialect dialect : Dialect.values()) { | |
| 168 | + assertThrows(IllegalArgumentException.class, () -> fragment.apply(dialect, "create_time) OR (1=1")); | |
| 169 | + assertThrows(IllegalArgumentException.class, () -> fragment.apply(dialect, "create_time;drop table t")); | |
| 170 | + assertThrows(IllegalArgumentException.class, () -> fragment.apply(dialect, "'2026-01-01'")); | |
| 171 | + assertThrows(IllegalArgumentException.class, () -> fragment.apply(dialect, "")); | |
| 172 | + assertThrows(IllegalArgumentException.class, () -> fragment.apply(dialect, null)); | |
| 173 | + } | |
| 174 | + } | |
| 175 | + for (Dialect dialect : Dialect.values()) { | |
| 176 | + assertThrows(IllegalArgumentException.class, | |
| 177 | + () -> SqlDialectUtils.customerDealCycle(dialect, "customer.create_time", "NOW()")); | |
| 178 | + assertThrows(IllegalArgumentException.class, | |
| 179 | + () -> SqlDialectUtils.customerDealCycle(dialect, "1) - (1", "contract.order_date")); | |
| 180 | + } | |
| 181 | + } | |
| 182 | + | |
| 183 | + /** | |
| 184 | + * MyBatis Plus 租户等插件会使用 JSqlParser 解析最终 SQL,所有方言的片段均需可被解析 | |
| 185 | + */ | |
| 186 | + @Test | |
| 187 | + public void testFragments_parsedByJSqlParser() throws Exception { | |
| 188 | + for (Dialect dialect : Dialect.values()) { | |
| 189 | + List<String> expressions = List.of( | |
| 190 | + SqlDialectUtils.formatDate(dialect, "t.create_time"), | |
| 191 | + SqlDialectUtils.formatYearMonth(dialect, "t.create_time"), | |
| 192 | + SqlDialectUtils.formatYearDashMonth(dialect, "t.create_time"), | |
| 193 | + SqlDialectUtils.date(dialect, "t.create_time"), | |
| 194 | + SqlDialectUtils.castDateText(dialect, "t.create_time"), | |
| 195 | + SqlDialectUtils.castDate(dialect, "t.create_time"), | |
| 196 | + SqlDialectUtils.month(dialect, "t.create_time"), | |
| 197 | + SqlDialectUtils.year(dialect, "t.create_time"), | |
| 198 | + SqlDialectUtils.dayOfMonth(dialect, "t.create_time")); | |
| 199 | + for (String expression : expressions) { | |
| 200 | + String sql = "SELECT " + expression + " AS statDate, COUNT(1) AS total FROM crm_customer t" | |
| 201 | + + " WHERE t.create_time >= ? GROUP BY " + expression + " ORDER BY statDate"; | |
| 202 | + assertNotNull(CCJSqlParserUtil.parse(sql), dialect + " " + expression); | |
| 203 | + } | |
| 204 | + String dealCycleSql = "SELECT customer.owner_user_id, " | |
| 205 | + + SqlDialectUtils.customerDealCycle(dialect, "customer.create_time", "contract.order_date") | |
| 206 | + + " AS customer_deal_cycle FROM crm_customer customer" | |
| 207 | + + " LEFT JOIN crm_contract contract ON contract.customer_id = customer.id" | |
| 208 | + + " GROUP BY customer.owner_user_id"; | |
| 209 | + assertNotNull(CCJSqlParserUtil.parse(dealCycleSql), dialect + " customerDealCycle"); | |
| 210 | + String summarySql = "SELECT (SELECT COUNT(1) FROM ai1_agent) AS agentCount, " | |
| 211 | + + "(SELECT COUNT(1) FROM ai1_model) AS modelCount " + SqlDialectUtils.fromDual(dialect); | |
| 212 | + assertNotNull(CCJSqlParserUtil.parse(summarySql), dialect + " fromDual"); | |
| 213 | + } | |
| 214 | + } | |
| 215 | + | |
| 216 | + // ========== 行数限制 ========== | |
| 217 | + | |
| 218 | + @Test | |
| 219 | + public void testLimitRows() { | |
| 220 | + assertEquals("LIMIT 1", SqlDialectUtils.limitRows(Dialect.ORIGINAL, 1, false)); | |
| 221 | + assertEquals("LIMIT 10", SqlDialectUtils.limitRows(Dialect.POSTGRESQL, 10, true)); | |
| 222 | + assertEquals("FETCH FIRST 1 ROWS ONLY", SqlDialectUtils.limitRows(Dialect.ORACLE, 1, false)); | |
| 223 | + assertEquals("FETCH FIRST 10 ROWS ONLY", SqlDialectUtils.limitRows(Dialect.ORACLE, 10, true)); | |
| 224 | + // SQL Server:OFFSET 必须跟在 ORDER BY 之后,无排序时补充常量排序 | |
| 225 | + assertEquals("OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY", SqlDialectUtils.limitRows(Dialect.SQLSERVER, 10, true)); | |
| 226 | + assertEquals("ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY", | |
| 227 | + SqlDialectUtils.limitRows(Dialect.SQLSERVER, 1, false)); | |
| 228 | + } | |
| 229 | + | |
| 230 | + @Test | |
| 231 | + public void testLimitRows_zeroAndNegative() { | |
| 232 | + assertEquals("LIMIT 0", SqlDialectUtils.limitRows(Dialect.ORIGINAL, 0, true)); | |
| 233 | + assertEquals("LIMIT 0", SqlDialectUtils.limitRows(Dialect.POSTGRESQL, 0, true)); | |
| 234 | + assertEquals("FETCH FIRST 0 ROWS ONLY", SqlDialectUtils.limitRows(Dialect.ORACLE, 0, true)); | |
| 235 | + // SQL Server 的 FETCH 不支持 0 行 | |
| 236 | + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitRows(Dialect.SQLSERVER, 0, true)); | |
| 237 | + for (Dialect dialect : Dialect.values()) { | |
| 238 | + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitRows(dialect, -1, true)); | |
| 239 | + } | |
| 240 | + } | |
| 241 | + | |
| 242 | + @Test | |
| 243 | + public void testLimitParam() { | |
| 244 | + assertEquals("LIMIT #{limit}", SqlDialectUtils.limitParam(Dialect.ORIGINAL, "limit", true)); | |
| 245 | + assertEquals("LIMIT #{reqVO.limit}", SqlDialectUtils.limitParam(Dialect.POSTGRESQL, "reqVO.limit", true)); | |
| 246 | + assertEquals("FETCH FIRST #{limit} ROWS ONLY", SqlDialectUtils.limitParam(Dialect.ORACLE, "limit", false)); | |
| 247 | + assertEquals("OFFSET 0 ROWS FETCH NEXT #{limit} ROWS ONLY", SqlDialectUtils.limitParam(Dialect.SQLSERVER, "limit", true)); | |
| 248 | + assertEquals("ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT #{limit} ROWS ONLY", | |
| 249 | + SqlDialectUtils.limitParam(Dialect.SQLSERVER, "limit", false)); | |
| 250 | + // 参数名只能是标识符或属性路径 | |
| 251 | + for (Dialect dialect : Dialect.values()) { | |
| 252 | + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitParam(dialect, "limit} OR 1=1 #{a", true)); | |
| 253 | + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitParam(dialect, "1", true)); | |
| 254 | + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitParam(dialect, "", true)); | |
| 255 | + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitParam(dialect, null, true)); | |
| 256 | + } | |
| 257 | + } | |
| 258 | + | |
| 259 | + // ========== 分批删除 ========== | |
| 260 | + | |
| 261 | + @Test | |
| 262 | + public void testDeleteBatchByLessThan() { | |
| 263 | + // 原有语句保持不变 | |
| 264 | + assertEquals("DELETE FROM infra_job_log WHERE create_time < #{createTime} LIMIT #{limit}", | |
| 265 | + SqlDialectUtils.deleteBatchByLessThan(Dialect.ORIGINAL, "infra_job_log", "create_time", "createTime", "limit")); | |
| 266 | + assertEquals("DELETE FROM infra_job_log WHERE id IN (SELECT id FROM infra_job_log" | |
| 267 | + + " WHERE create_time < #{createTime} LIMIT #{limit})", | |
| 268 | + SqlDialectUtils.deleteBatchByLessThan(Dialect.POSTGRESQL, "infra_job_log", "create_time", "createTime", "limit")); | |
| 269 | + assertEquals("DELETE FROM system_oauth2_access_token WHERE expires_time < #{expiresTime} AND ROWNUM <= #{limit}", | |
| 270 | + SqlDialectUtils.deleteBatchByLessThan(Dialect.ORACLE, "system_oauth2_access_token", "expires_time", "expiresTime", "limit")); | |
| 271 | + assertEquals("DELETE FROM system_oauth2_access_token WHERE id IN (SELECT TOP (#{limit}) id FROM system_oauth2_access_token" | |
| 272 | + + " WHERE expires_time < #{expiresTime})", | |
| 273 | + SqlDialectUtils.deleteBatchByLessThan(Dialect.SQLSERVER, "system_oauth2_access_token", "expires_time", "expiresTime", "limit")); | |
| 274 | + } | |
| 275 | + | |
| 276 | + @Test | |
| 277 | + public void testDeleteBatchByLessThan_invalidName() { | |
| 278 | + for (Dialect dialect : Dialect.values()) { | |
| 279 | + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.deleteBatchByLessThan(dialect, | |
| 280 | + "infra_job_log; DROP TABLE t", "create_time", "createTime", "limit")); | |
| 281 | + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.deleteBatchByLessThan(dialect, | |
| 282 | + "infra_job_log", "create_time) OR (1=1", "createTime", "limit")); | |
| 283 | + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.deleteBatchByLessThan(dialect, | |
| 284 | + "infra_job_log", "create_time", "createTime} OR 1=1 OR #{a", "limit")); | |
| 285 | + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.deleteBatchByLessThan(dialect, | |
| 286 | + "infra_job_log", "create_time", "createTime", null)); | |
| 287 | + } | |
| 288 | + } | |
| 289 | + | |
| 290 | + /** | |
| 291 | + * MyBatis Plus 租户、数据权限插件会使用 JSqlParser 解析最终 SQL,所有方言的语句均需可被解析 | |
| 292 | + * | |
| 293 | + * 注意:JSqlParser 5.2 不支持 DELETE TOP (?),因此 SQL Server 使用主键子查询 | |
| 294 | + */ | |
| 295 | + @Test | |
| 296 | + public void testLimitAndDelete_parsedByJSqlParser() throws Exception { | |
| 297 | + for (Dialect dialect : Dialect.values()) { | |
| 298 | + String deleteSql = SqlDialectUtils.deleteBatchByLessThan(dialect, "infra_job_log", "create_time", "createTime", "limit"); | |
| 299 | + assertNotNull(CCJSqlParserUtil.parse(bind(deleteSql)), dialect + " " + deleteSql); | |
| 300 | + | |
| 301 | + String orderedSql = "SELECT user_id AS userId, COUNT(*) AS messageCount FROM im_private_message" | |
| 302 | + + " WHERE send_time >= ? GROUP BY user_id ORDER BY messageCount DESC " | |
| 303 | + + SqlDialectUtils.limitParam(dialect, "limit", true); | |
| 304 | + assertNotNull(CCJSqlParserUtil.parse(bind(orderedSql)), dialect + " " + orderedSql); | |
| 305 | + String unorderedSql = "SELECT * FROM hrm_salary_option WHERE code = ? " | |
| 306 | + + SqlDialectUtils.limitRows(dialect, 1, false); | |
| 307 | + assertNotNull(CCJSqlParserUtil.parse(unorderedSql), dialect + " " + unorderedSql); | |
| 308 | + } | |
| 309 | + } | |
| 310 | + | |
| 311 | + // ========== 方言识别 ========== | |
| 312 | + | |
| 313 | + @Test | |
| 314 | + public void testResolveDialect_accepted() throws SQLException { | |
| 315 | + assertEquals(Dialect.POSTGRESQL, resolve("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro", 17)); | |
| 316 | + assertEquals(Dialect.POSTGRESQL, resolve("PostgreSQL", "jdbc:p6spy:postgresql://127.0.0.1:5432/ruoyi-vue-pro", 17)); | |
| 317 | + assertEquals(Dialect.ORACLE, resolve("Oracle", "jdbc:oracle:thin:@127.0.0.1:1521:xe", 21)); | |
| 318 | + assertEquals(Dialect.SQLSERVER, resolve("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 16)); | |
| 319 | + assertEquals(Dialect.SQLSERVER, resolve("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 11)); | |
| 320 | + } | |
| 321 | + | |
| 322 | + @Test | |
| 323 | + public void testResolveDialect_original() throws SQLException { | |
| 324 | + // MySQL 族 | |
| 325 | + assertEquals(Dialect.ORIGINAL, resolve("MySQL", "jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro", 8)); | |
| 326 | + assertEquals(Dialect.ORIGINAL, resolve("MariaDB", "jdbc:mariadb://127.0.0.1:3306/ruoyi-vue-pro", 11)); | |
| 327 | + assertEquals(Dialect.ORIGINAL, resolve("OceanBase", "jdbc:oceanbase://127.0.0.1:2881/ruoyi-vue-pro", 4)); | |
| 328 | + // 已有旧路径:H2、达梦、神通、人大金仓 | |
| 329 | + assertEquals(Dialect.ORIGINAL, resolve("H2", "jdbc:h2:mem:testdb;MODE=MYSQL", 2)); | |
| 330 | + assertEquals(Dialect.ORIGINAL, resolve("DM DBMS", "jdbc:dm://127.0.0.1:5236", 8)); | |
| 331 | + assertEquals(Dialect.ORIGINAL, resolve("OSCAR", "jdbc:oscar://127.0.0.1:2003/osrdb", 7)); | |
| 332 | + assertEquals(Dialect.ORIGINAL, resolve("KingbaseES", "jdbc:kingbase8://127.0.0.1:54321/test", 8)); | |
| 333 | + // 未验收的 PostgreSQL 变种:原生产品名 | |
| 334 | + assertEquals(Dialect.ORIGINAL, resolve("openGauss", "jdbc:opengauss://127.0.0.1:5432/test", 5)); | |
| 335 | + assertEquals(Dialect.ORIGINAL, resolve("GaussDB", "jdbc:gaussdb://127.0.0.1:8000/test", 5)); | |
| 336 | + // 未验收的 PostgreSQL 变种:产品名未登记或与 PostgreSQL 相同,按原生驱动 URL 识别 | |
| 337 | + assertEquals(Dialect.ORIGINAL, resolve("HighGo", "jdbc:highgo://127.0.0.1:5866/highgo", 6)); | |
| 338 | + assertEquals(Dialect.ORIGINAL, resolve("PostgreSQL", "jdbc:highgo://127.0.0.1:5866/highgo", 6)); | |
| 339 | + assertEquals(Dialect.ORIGINAL, resolve("PostgreSQL", "jdbc:opengauss://127.0.0.1:5432/test", 9)); | |
| 340 | + assertEquals(Dialect.ORIGINAL, resolve("PostgreSQL", "jdbc:mogdb://127.0.0.1:5432/test", 9)); | |
| 341 | + assertEquals(Dialect.ORIGINAL, resolve("PostgreSQL", "jdbc:gaussdb://127.0.0.1:8000/test", 9)); | |
| 342 | + assertEquals(Dialect.ORIGINAL, resolve("PostgreSQL", "jdbc:kingbase8://127.0.0.1:54321/test", 12)); | |
| 343 | + // 未验收的 SQL Server 版本 | |
| 344 | + assertEquals(Dialect.ORIGINAL, resolve("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 10)); | |
| 345 | + assertEquals(Dialect.ORIGINAL, resolve("Microsoft SQL Server 2005", "jdbc:jtds:sqlserver://127.0.0.1:1433", 9)); | |
| 346 | + } | |
| 347 | + | |
| 348 | + @Test | |
| 349 | + public void testResolveDialect_unsupported() { | |
| 350 | + String secretUrl = "jdbc:sqlite:/tmp/test.db?user=root&password=secret"; | |
| 351 | + IllegalStateException ex = assertThrows(IllegalStateException.class, () -> resolve("SQLite", secretUrl, 3)); | |
| 352 | + assertTrue(ex.getMessage().contains("SQLite")); | |
| 353 | + assertFalse(ex.getMessage().contains("secret")); | |
| 354 | + assertFalse(ex.getMessage().contains("jdbc:")); | |
| 355 | + // 产品名为 PostgreSQL,但 URL 不是 PostgreSQL 或已识别的变种 | |
| 356 | + assertThrows(IllegalStateException.class, () -> resolve("PostgreSQL", "jdbc:vastbase://127.0.0.1:5432/test", 9)); | |
| 357 | + // 产品名为空 | |
| 358 | + assertThrows(IllegalStateException.class, () -> resolve(null, "jdbc:unknown://127.0.0.1/test", 1)); | |
| 359 | + } | |
| 360 | + | |
| 361 | + @Test | |
| 362 | + public void testGetDialect_cachedPerDataSource() { | |
| 363 | + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a", 17); | |
| 364 | + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a", 8); | |
| 365 | + | |
| 366 | + assertEquals(Dialect.POSTGRESQL, SqlDialectUtils.getDialect(postgresql.getDataSource())); | |
| 367 | + assertEquals(Dialect.ORIGINAL, SqlDialectUtils.getDialect(mysql.getDataSource())); | |
| 368 | + assertEquals(Dialect.POSTGRESQL, SqlDialectUtils.getDialect(postgresql.getDataSource())); | |
| 369 | + assertEquals(Dialect.ORIGINAL, SqlDialectUtils.getDialect(mysql.getDataSource())); | |
| 370 | + assertEquals(1, postgresql.getConnectionCount()); | |
| 371 | + assertEquals(1, mysql.getConnectionCount()); | |
| 372 | + } | |
| 373 | + | |
| 374 | + @Test | |
| 375 | + public void testGetDialect_failureNotCached() { | |
| 376 | + FakeDataSource broken = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a", 17); | |
| 377 | + broken.setFailOnConnection(true); | |
| 378 | + assertThrows(IllegalStateException.class, () -> SqlDialectUtils.getDialect(broken.getDataSource())); | |
| 379 | + assertThrows(IllegalStateException.class, () -> SqlDialectUtils.getDialect(broken.getDataSource())); | |
| 380 | + assertEquals(2, broken.getConnectionCount()); | |
| 381 | + | |
| 382 | + // 恢复后可以正常识别 | |
| 383 | + broken.setFailOnConnection(false); | |
| 384 | + assertEquals(Dialect.POSTGRESQL, SqlDialectUtils.getDialect(broken.getDataSource())); | |
| 385 | + } | |
| 386 | + | |
| 387 | + @Test | |
| 388 | + public void testFormatDate_transactionConnectionDoesNotBorrowOrClose() throws SQLException { | |
| 389 | + FakeDataSource h2 = new FakeDataSource("H2", "jdbc:h2:mem:single_connection", 2); | |
| 390 | + Connection held = h2.getDataSource().getConnection(); | |
| 391 | + h2.setFailOnConnection(true); // 当前事务已占用唯一连接,任何额外借用都会失败 | |
| 392 | + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); | |
| 393 | + beanFactory.registerSingleton("dataSource", h2.getDataSource()); | |
| 394 | + new SpringUtil().postProcessBeanFactory(beanFactory); | |
| 395 | + TransactionSynchronizationManager.bindResource(h2.getDataSource(), new ConnectionHolder(held)); | |
| 396 | + try { | |
| 397 | + assertEquals("DATE_FORMAT(create_time, '%Y-%m-%d')", SqlDialectUtils.formatDate("create_time")); | |
| 398 | + assertEquals(1, h2.getConnectionCount()); | |
| 399 | + assertEquals(0, h2.closeCount); | |
| 400 | + } finally { | |
| 401 | + TransactionSynchronizationManager.unbindResource(h2.getDataSource()); | |
| 402 | + held.close(); | |
| 403 | + } | |
| 404 | + } | |
| 405 | + | |
| 406 | + @Test | |
| 407 | + public void testGetDialect_transactionUsesBoundConnectionWithoutPoisoningRouteCache() throws SQLException { | |
| 408 | + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://probe/transaction", 17); | |
| 409 | + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://probe/other", 8); | |
| 410 | + mockDynamicRoutingDataSource(Map.of("master", postgresql, "mysql", mysql)); | |
| 411 | + DynamicRoutingDataSource routing = SpringUtil.getBean(DynamicRoutingDataSource.class); | |
| 412 | + Connection held = postgresql.getDataSource().getConnection(); | |
| 413 | + TransactionSynchronizationManager.bindResource(routing, new ConnectionHolder(held)); | |
| 414 | + DynamicDataSourceContextHolder.push("mysql"); | |
| 415 | + try { | |
| 416 | + // Spring 事务的执行连接仍为 PG,切换线程路由键不会改变它 | |
| 417 | + assertEquals("CAST(EXTRACT(MONTH FROM birthday) AS INTEGER)", SqlDialectUtils.month("birthday")); | |
| 418 | + assertEquals(1, postgresql.getConnectionCount()); | |
| 419 | + assertEquals(0, mysql.getConnectionCount()); | |
| 420 | + assertEquals(0, postgresql.closeCount); | |
| 421 | + } finally { | |
| 422 | + TransactionSynchronizationManager.unbindResource(routing); | |
| 423 | + held.close(); | |
| 424 | + } | |
| 425 | + // 事务结束后该路由真正使用 MySQL,不能命中错误的 PG 缓存 | |
| 426 | + assertEquals("MONTH(birthday)", SqlDialectUtils.month("birthday")); | |
| 427 | + assertEquals(1, mysql.getConnectionCount()); | |
| 428 | + } | |
| 429 | + | |
| 430 | + @Test | |
| 431 | + public void testFormatDate_followsDynamicRouting() { | |
| 432 | + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a", 17); | |
| 433 | + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a", 8); | |
| 434 | + FakeDataSource oracle = new FakeDataSource("Oracle", "jdbc:oracle:thin:@127.0.0.1:1521:xe", 21); | |
| 435 | + mockDynamicRoutingDataSource(Map.of("master", postgresql, "mysql", mysql, "oracle", oracle)); | |
| 436 | + | |
| 437 | + for (int i = 0; i < 3; i++) { | |
| 438 | + // 未切换时为主库 | |
| 439 | + assertEquals("TO_CHAR(create_time, 'YYYY-MM-DD')", SqlDialectUtils.formatDate("create_time")); | |
| 440 | + DynamicDataSourceContextHolder.push("mysql"); | |
| 441 | + assertEquals("DATE_FORMAT(create_time, '%Y-%m-%d')", SqlDialectUtils.formatDate("create_time")); | |
| 442 | + DynamicDataSourceContextHolder.push("oracle"); | |
| 443 | + assertEquals("TO_CHAR(create_time, 'YYYY-MM-DD')", SqlDialectUtils.formatDate("create_time")); | |
| 444 | + assertEquals("FROM DUAL", SqlDialectUtils.fromDual()); | |
| 445 | + DynamicDataSourceContextHolder.poll(); | |
| 446 | + assertEquals("", SqlDialectUtils.fromDual()); | |
| 447 | + DynamicDataSourceContextHolder.poll(); | |
| 448 | + } | |
| 449 | + assertEquals(1, postgresql.getConnectionCount()); | |
| 450 | + assertEquals(1, mysql.getConnectionCount()); | |
| 451 | + assertEquals(1, oracle.getConnectionCount()); | |
| 452 | + } | |
| 453 | + | |
| 454 | + @Test | |
| 455 | + public void testBoundSql_xmlScript() { | |
| 456 | + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a", 17); | |
| 457 | + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a", 8); | |
| 458 | + mockDynamicRoutingDataSource(Map.of("master", postgresql, "mysql", mysql)); | |
| 459 | + SqlSource sqlSource = new XMLLanguageDriver().createSqlSource(new Configuration(), | |
| 460 | + "<script>SELECT " + P + "formatDate('create_time')} AS time, COUNT(*) AS customerCreateCount" | |
| 461 | + + " FROM crm_customer WHERE create_time BETWEEN #{beginTime} AND #{endTime}" | |
| 462 | + + " GROUP BY " + P + "formatDate('create_time')}</script>", Map.class); | |
| 463 | + Map<String, Object> params = new HashMap<>(); | |
| 464 | + params.put("beginTime", LocalDateTime.of(2026, 1, 1, 0, 0)); | |
| 465 | + params.put("endTime", LocalDateTime.of(2026, 1, 31, 23, 59, 59)); | |
| 466 | + | |
| 467 | + // 主库:PostgreSQL | |
| 468 | + BoundSql boundSql = sqlSource.getBoundSql(params); | |
| 469 | + assertEquals("SELECT TO_CHAR(create_time, 'YYYY-MM-DD') AS time, COUNT(*) AS customerCreateCount" | |
| 470 | + + " FROM crm_customer WHERE create_time BETWEEN ? AND ?" | |
| 471 | + + " GROUP BY TO_CHAR(create_time, 'YYYY-MM-DD')", normalize(boundSql.getSql())); | |
| 472 | + assertParameters(boundSql, "beginTime", "endTime"); | |
| 473 | + | |
| 474 | + // 同一个 SqlSource 在下一次执行时,跟随路由切换到 MySQL | |
| 475 | + DynamicDataSourceContextHolder.push("mysql"); | |
| 476 | + boundSql = sqlSource.getBoundSql(params); | |
| 477 | + assertEquals("SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, COUNT(*) AS customerCreateCount" | |
| 478 | + + " FROM crm_customer WHERE create_time BETWEEN ? AND ?" | |
| 479 | + + " GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d')", normalize(boundSql.getSql())); | |
| 480 | + assertParameters(boundSql, "beginTime", "endTime"); | |
| 481 | + } | |
| 482 | + | |
| 483 | + @Test | |
| 484 | + public void testBoundSql_annotation() { | |
| 485 | + FakeDataSource sqlServer = new FakeDataSource("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 16); | |
| 486 | + FakeDataSource h2 = new FakeDataSource("H2", "jdbc:h2:mem:testdb;MODE=MYSQL", 2); | |
| 487 | + mockDynamicRoutingDataSource(Map.of("master", sqlServer, "h2", h2)); | |
| 488 | + // 与 @Select 注解相同:不使用 <script>,仅包含 ${} 时按动态 SQL 处理 | |
| 489 | + SqlSource sqlSource = new XMLLanguageDriver().createSqlSource(new Configuration(), | |
| 490 | + "SELECT " + P + "date('send_time')} AS statDate, COUNT(*) AS count FROM im_private_message" | |
| 491 | + + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}" | |
| 492 | + + " GROUP BY " + P + "date('send_time')}", Map.class); | |
| 493 | + Map<String, Object> params = new HashMap<>(); | |
| 494 | + params.put("beginTime", LocalDateTime.of(2026, 1, 1, 0, 0)); | |
| 495 | + params.put("endTime", LocalDateTime.of(2026, 2, 1, 0, 0)); | |
| 496 | + | |
| 497 | + BoundSql boundSql = sqlSource.getBoundSql(params); | |
| 498 | + assertEquals("SELECT CONVERT(varchar(10), send_time, 23) AS statDate, COUNT(*) AS count FROM im_private_message" | |
| 499 | + + " WHERE send_time >= ? AND send_time < ?" | |
| 500 | + + " GROUP BY CONVERT(varchar(10), send_time, 23)", normalize(boundSql.getSql())); | |
| 501 | + assertParameters(boundSql, "beginTime", "endTime"); | |
| 502 | + | |
| 503 | + DynamicDataSourceContextHolder.push("h2"); | |
| 504 | + boundSql = sqlSource.getBoundSql(params); | |
| 505 | + assertEquals("SELECT DATE(send_time) AS statDate, COUNT(*) AS count FROM im_private_message" | |
| 506 | + + " WHERE send_time >= ? AND send_time < ?" | |
| 507 | + + " GROUP BY DATE(send_time)", normalize(boundSql.getSql())); | |
| 508 | + assertParameters(boundSql, "beginTime", "endTime"); | |
| 509 | + } | |
| 510 | + | |
| 511 | + @Test | |
| 512 | + public void testBoundSql_deleteBatch() { | |
| 513 | + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a", 17); | |
| 514 | + FakeDataSource sqlServer = new FakeDataSource("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 16); | |
| 515 | + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a", 8); | |
| 516 | + FakeDataSource oracle = new FakeDataSource("Oracle", "jdbc:oracle:thin:@127.0.0.1:1521:xe", 21); | |
| 517 | + mockDynamicRoutingDataSource(Map.of("master", postgresql, "sqlserver", sqlServer, "mysql", mysql, "oracle", oracle)); | |
| 518 | + // 与 @Delete 注解相同 | |
| 519 | + SqlSource sqlSource = new XMLLanguageDriver().createSqlSource(new Configuration(), | |
| 520 | + P + "deleteBatchByLessThan('infra_job_log', 'create_time', 'createTime', 'limit')}", Map.class); | |
| 521 | + Map<String, Object> params = new HashMap<>(); | |
| 522 | + params.put("createTime", LocalDateTime.of(2026, 1, 1, 0, 0)); | |
| 523 | + params.put("limit", 100); | |
| 524 | + | |
| 525 | + // 主库:PostgreSQL | |
| 526 | + BoundSql boundSql = sqlSource.getBoundSql(params); | |
| 527 | + assertEquals("DELETE FROM infra_job_log WHERE id IN (SELECT id FROM infra_job_log WHERE create_time < ? LIMIT ?)", | |
| 528 | + normalize(boundSql.getSql())); | |
| 529 | + assertParameters(boundSql, "createTime", "limit"); | |
| 530 | + | |
| 531 | + DynamicDataSourceContextHolder.push("sqlserver"); | |
| 532 | + boundSql = sqlSource.getBoundSql(params); | |
| 533 | + assertEquals("DELETE FROM infra_job_log WHERE id IN (SELECT TOP (?) id FROM infra_job_log WHERE create_time < ?)", | |
| 534 | + normalize(boundSql.getSql())); | |
| 535 | + assertParameters(boundSql, "limit", "createTime"); | |
| 536 | + | |
| 537 | + DynamicDataSourceContextHolder.push("oracle"); | |
| 538 | + boundSql = sqlSource.getBoundSql(params); | |
| 539 | + assertEquals("DELETE FROM infra_job_log WHERE create_time < ? AND ROWNUM <= ?", normalize(boundSql.getSql())); | |
| 540 | + assertParameters(boundSql, "createTime", "limit"); | |
| 541 | + | |
| 542 | + DynamicDataSourceContextHolder.push("mysql"); | |
| 543 | + boundSql = sqlSource.getBoundSql(params); | |
| 544 | + assertEquals("DELETE FROM infra_job_log WHERE create_time < ? LIMIT ?", normalize(boundSql.getSql())); | |
| 545 | + assertParameters(boundSql, "createTime", "limit"); | |
| 546 | + } | |
| 547 | + | |
| 548 | + @Test | |
| 549 | + public void testBoundSql_limit() { | |
| 550 | + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a", 8); | |
| 551 | + FakeDataSource oracle = new FakeDataSource("Oracle", "jdbc:oracle:thin:@127.0.0.1:1521:xe", 21); | |
| 552 | + FakeDataSource sqlServer = new FakeDataSource("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 16); | |
| 553 | + mockDynamicRoutingDataSource(Map.of("master", mysql, "oracle", oracle, "sqlserver", sqlServer)); | |
| 554 | + // 与 @Select 注解相同:已有排序时使用参数绑定的行数,无排序时使用固定行数 | |
| 555 | + SqlSource orderedSource = new XMLLanguageDriver().createSqlSource(new Configuration(), | |
| 556 | + "SELECT user_id, COUNT(*) AS messageCount FROM im_private_message WHERE send_time >= #{beginTime}" | |
| 557 | + + " GROUP BY user_id ORDER BY messageCount DESC " + P + "limitParam('limit', true)}", Map.class); | |
| 558 | + SqlSource unorderedSource = new XMLLanguageDriver().createSqlSource(new Configuration(), | |
| 559 | + "SELECT * FROM hrm_salary_option WHERE code = #{code} " + P + "limitRows(1, false)}", Map.class); | |
| 560 | + Map<String, Object> params = new HashMap<>(); | |
| 561 | + params.put("beginTime", LocalDateTime.of(2026, 1, 1, 0, 0)); | |
| 562 | + params.put("limit", 10); | |
| 563 | + params.put("code", 1); | |
| 564 | + String ordered = "SELECT user_id, COUNT(*) AS messageCount FROM im_private_message WHERE send_time >= ?" | |
| 565 | + + " GROUP BY user_id ORDER BY messageCount DESC "; | |
| 566 | + | |
| 567 | + // 主库:MySQL,保持原有 LIMIT | |
| 568 | + BoundSql boundSql = orderedSource.getBoundSql(params); | |
| 569 | + assertEquals(ordered + "LIMIT ?", normalize(boundSql.getSql())); | |
| 570 | + assertParameters(boundSql, "beginTime", "limit"); | |
| 571 | + assertEquals("SELECT * FROM hrm_salary_option WHERE code = ? LIMIT 1", | |
| 572 | + normalize(unorderedSource.getBoundSql(params).getSql())); | |
| 573 | + | |
| 574 | + DynamicDataSourceContextHolder.push("oracle"); | |
| 575 | + boundSql = orderedSource.getBoundSql(params); | |
| 576 | + assertEquals(ordered + "FETCH FIRST ? ROWS ONLY", normalize(boundSql.getSql())); | |
| 577 | + assertParameters(boundSql, "beginTime", "limit"); | |
| 578 | + assertEquals("SELECT * FROM hrm_salary_option WHERE code = ? FETCH FIRST 1 ROWS ONLY", | |
| 579 | + normalize(unorderedSource.getBoundSql(params).getSql())); | |
| 580 | + | |
| 581 | + DynamicDataSourceContextHolder.push("sqlserver"); | |
| 582 | + boundSql = orderedSource.getBoundSql(params); | |
| 583 | + assertEquals(ordered + "OFFSET 0 ROWS FETCH NEXT ? ROWS ONLY", normalize(boundSql.getSql())); | |
| 584 | + assertParameters(boundSql, "beginTime", "limit"); | |
| 585 | + boundSql = unorderedSource.getBoundSql(params); | |
| 586 | + assertEquals("SELECT * FROM hrm_salary_option WHERE code = ? ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY", | |
| 587 | + normalize(boundSql.getSql())); | |
| 588 | + assertParameters(boundSql, "code"); | |
| 589 | + } | |
| 590 | + | |
| 591 | + @Test | |
| 592 | + public void testBoundSql_unsupportedProduct() { | |
| 593 | + FakeDataSource sqlite = new FakeDataSource("SQLite", "jdbc:sqlite:/tmp/test.db", 3); | |
| 594 | + mockDynamicRoutingDataSource(Map.of("master", sqlite)); | |
| 595 | + SqlSource sqlSource = new XMLLanguageDriver().createSqlSource(new Configuration(), | |
| 596 | + "SELECT " + P + "month('order_date')} AS month FROM crm_contract" | |
| 597 | + + " WHERE " + P + "year('order_date')} = #{year}", Map.class); | |
| 598 | + | |
| 599 | + // OGNL 会包装异常,因此按异常链的信息断言 | |
| 600 | + Exception ex = assertThrows(Exception.class, () -> sqlSource.getBoundSql(Map.of("year", 2026))); | |
| 601 | + assertTrue(causeMessages(ex).contains("不支持的数据库产品:SQLite"), causeMessages(ex)); | |
| 602 | + } | |
| 603 | + | |
| 604 | + // ========== 工具方法 ========== | |
| 605 | + | |
| 606 | + private static Dialect resolve(String productName, String url, int majorVersion) throws SQLException { | |
| 607 | + return SqlDialectUtils.resolveDialect(new FakeDataSource(productName, url, majorVersion).getMetaData()); | |
| 608 | + } | |
| 609 | + | |
| 610 | + private static void mockDynamicRoutingDataSource(Map<String, FakeDataSource> dataSources) { | |
| 611 | + DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(Collections.emptyList()); | |
| 612 | + dynamicRoutingDataSource.setPrimary("master"); | |
| 613 | + dataSources.forEach((name, dataSource) -> dynamicRoutingDataSource.addDataSource(name, dataSource.getDataSource())); | |
| 614 | + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); | |
| 615 | + beanFactory.registerSingleton("dynamicRoutingDataSource", dynamicRoutingDataSource); | |
| 616 | + new SpringUtil().postProcessBeanFactory(beanFactory); | |
| 617 | + } | |
| 618 | + | |
| 619 | + private static String normalize(String sql) { | |
| 620 | + return sql.replaceAll("\\s+", " ").trim(); | |
| 621 | + } | |
| 622 | + | |
| 623 | + /** | |
| 624 | + * 将 #{} 参数替换为 JDBC 占位符,模拟 MyBatis 生成的最终 SQL | |
| 625 | + */ | |
| 626 | + private static String bind(String sql) { | |
| 627 | + return sql.replaceAll("#\\{[^}]+}", "?"); | |
| 628 | + } | |
| 629 | + | |
| 630 | + private static void assertParameters(BoundSql boundSql, String... properties) { | |
| 631 | + List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); | |
| 632 | + assertEquals(properties.length, parameterMappings.size()); | |
| 633 | + for (int i = 0; i < properties.length; i++) { | |
| 634 | + assertEquals(properties[i], parameterMappings.get(i).getProperty()); | |
| 635 | + } | |
| 636 | + } | |
| 637 | + | |
| 638 | + private static String causeMessages(Throwable ex) { | |
| 639 | + StringBuilder messages = new StringBuilder(); | |
| 640 | + for (Throwable cause = ex; cause != null && cause != cause.getCause(); cause = cause.getCause()) { | |
| 641 | + messages.append(cause).append('\n'); | |
| 642 | + } | |
| 643 | + return messages.toString(); | |
| 644 | + } | |
| 645 | + | |
| 646 | + /** | |
| 647 | + * 基于 JDK 动态代理的数据源,只提供识别方言所需的元数据,并记录获取连接的次数 | |
| 648 | + */ | |
| 649 | + private static class FakeDataSource implements InvocationHandler { | |
| 650 | + | |
| 651 | + private final String productName; | |
| 652 | + private final String url; | |
| 653 | + private final int majorVersion; | |
| 654 | + private final DataSource dataSource; | |
| 655 | + private int connectionCount; | |
| 656 | + private int closeCount; | |
| 657 | + private boolean failOnConnection; | |
| 658 | + | |
| 659 | + FakeDataSource(String productName, String url, int majorVersion) { | |
| 660 | + this.productName = productName; | |
| 661 | + this.url = url; | |
| 662 | + this.majorVersion = majorVersion; | |
| 663 | + this.dataSource = proxy(DataSource.class); | |
| 664 | + } | |
| 665 | + | |
| 666 | + DataSource getDataSource() { | |
| 667 | + return dataSource; | |
| 668 | + } | |
| 669 | + | |
| 670 | + DatabaseMetaData getMetaData() { | |
| 671 | + return proxy(DatabaseMetaData.class); | |
| 672 | + } | |
| 673 | + | |
| 674 | + int getConnectionCount() { | |
| 675 | + return connectionCount; | |
| 676 | + } | |
| 677 | + | |
| 678 | + void setFailOnConnection(boolean failOnConnection) { | |
| 679 | + this.failOnConnection = failOnConnection; | |
| 680 | + } | |
| 681 | + | |
| 682 | + @SuppressWarnings("unchecked") | |
| 683 | + private <T> T proxy(Class<T> type) { | |
| 684 | + return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[]{type}, this); | |
| 685 | + } | |
| 686 | + | |
| 687 | + @Override | |
| 688 | + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { | |
| 689 | + switch (method.getName()) { | |
| 690 | + case "hashCode": | |
| 691 | + return System.identityHashCode(proxy); | |
| 692 | + case "equals": | |
| 693 | + return proxy == args[0]; | |
| 694 | + case "toString": | |
| 695 | + return "FakeDataSource(" + productName + ")"; | |
| 696 | + case "getConnection": | |
| 697 | + connectionCount++; | |
| 698 | + if (failOnConnection) { | |
| 699 | + throw new SQLException("connection refused"); | |
| 700 | + } | |
| 701 | + return proxy(Connection.class); | |
| 702 | + case "getMetaData": | |
| 703 | + return proxy(DatabaseMetaData.class); | |
| 704 | + case "close": | |
| 705 | + closeCount++; | |
| 706 | + return null; | |
| 707 | + case "getDatabaseProductName": | |
| 708 | + return productName; | |
| 709 | + case "getURL": | |
| 710 | + return url; | |
| 711 | + case "getDatabaseMajorVersion": | |
| 712 | + return majorVersion; | |
| 713 | + default: | |
| 714 | + throw new UnsupportedOperationException(method.getName()); | |
| 715 | + } | |
| 716 | + } | |
| 717 | + | |
| 718 | + } | |
| 719 | + | |
| 720 | +} | ... | ... |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiApiKeyMapper.java
| ... | ... | @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.dal.mysql.model; |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 5 | 5 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 6 | -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; | |
| 7 | 6 | import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; |
| 8 | 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; |
| 9 | 8 | import org.apache.ibatis.annotations.Mapper; |
| ... | ... | @@ -25,11 +24,10 @@ public interface AiApiKeyMapper extends BaseMapperX<AiApiKeyDO> { |
| 25 | 24 | } |
| 26 | 25 | |
| 27 | 26 | default AiApiKeyDO selectFirstByPlatformAndStatus(String platform, Integer status) { |
| 28 | - return selectOne(new QueryWrapperX<AiApiKeyDO>() | |
| 29 | - .eq("platform", platform) | |
| 30 | - .eq("status", status) | |
| 31 | - .limitN(1) | |
| 32 | - .orderByAsc("id")); | |
| 27 | + return selectFirstOne(new LambdaQueryWrapperX<AiApiKeyDO>() | |
| 28 | + .eq(AiApiKeyDO::getPlatform, platform) | |
| 29 | + .eq(AiApiKeyDO::getStatus, status) | |
| 30 | + .orderByAsc(AiApiKeyDO::getId)); | |
| 33 | 31 | } |
| 34 | 32 | |
| 35 | 33 | } |
| 36 | 34 | \ No newline at end of file | ... | ... |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatMapper.java
| ... | ... | @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.dal.mysql.model; |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 5 | 5 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 6 | -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; | |
| 7 | 6 | import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO; |
| 8 | 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; |
| 9 | 8 | import org.apache.ibatis.annotations.Mapper; |
| ... | ... | @@ -20,11 +19,10 @@ import java.util.List; |
| 20 | 19 | public interface AiChatMapper extends BaseMapperX<AiModelDO> { |
| 21 | 20 | |
| 22 | 21 | default AiModelDO selectFirstByStatus(Integer type, Integer status) { |
| 23 | - return selectOne(new QueryWrapperX<AiModelDO>() | |
| 24 | - .eq("type", type) | |
| 25 | - .eq("status", status) | |
| 26 | - .limitN(1) | |
| 27 | - .orderByAsc("sort")); | |
| 22 | + return selectFirstOne(new LambdaQueryWrapperX<AiModelDO>() | |
| 23 | + .eq(AiModelDO::getType, type) | |
| 24 | + .eq(AiModelDO::getStatus, status) | |
| 25 | + .orderByAsc(AiModelDO::getSort)); | |
| 28 | 26 | } |
| 29 | 27 | |
| 30 | 28 | default PageResult<AiModelDO> selectPage(AiModelPageReqVO reqVO) { | ... | ... |
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/home/Ai1HomeMapper.java
| ... | ... | @@ -23,16 +23,17 @@ public interface Ai1HomeMapper { |
| 23 | 23 | @Select("SELECT (SELECT COUNT(1) FROM ai1_agent WHERE deleted = 0) AS agentCount, " + |
| 24 | 24 | "(SELECT COUNT(1) FROM ai1_skill WHERE deleted = 0) AS skillCount, " + |
| 25 | 25 | "(SELECT COUNT(1) FROM ai1_mcp WHERE deleted = 0) AS mcpCount, " + |
| 26 | - "(SELECT COUNT(1) FROM ai1_model WHERE deleted = 0) AS modelCount") | |
| 26 | + "(SELECT COUNT(1) FROM ai1_model WHERE deleted = 0) AS modelCount " + | |
| 27 | + "${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@fromDual()}") | |
| 27 | 28 | Ai1HomeSummaryRespVO selectSummary(); |
| 28 | 29 | |
| 29 | 30 | /** |
| 30 | - * 按天统计消息数量,仅返回有消息的日期;使用 CAST AS DATE 兼容 MySQL 与 H2 | |
| 31 | + * 按天统计消息数量,仅返回有消息的日期;日期按方言输出 yyyy-MM-dd,MySQL、H2 等保持 CAST AS DATE | |
| 31 | 32 | */ |
| 32 | - @Select("SELECT CAST(create_time AS DATE) AS statDate, COUNT(1) AS count " + | |
| 33 | + @Select("SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDateText('create_time')} AS statDate, COUNT(1) AS count " + | |
| 33 | 34 | "FROM ai1_session_message " + |
| 34 | 35 | "WHERE create_time BETWEEN #{beginTime} AND #{endTime} AND deleted = 0 " + |
| 35 | - "GROUP BY CAST(create_time AS DATE) " + | |
| 36 | + "GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDateText('create_time')} " + | |
| 36 | 37 | "ORDER BY statDate") |
| 37 | 38 | @Results({ |
| 38 | 39 | @Result(column = "statDate", property = "date"), | ... | ... |
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/session/Ai1SessionMessageMapper.java
| ... | ... | @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai1.dal.mysql.session; |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 5 | 5 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 6 | -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; | |
| 7 | 6 | import cn.iocoder.yudao.module.ai1.controller.admin.session.vo.message.Ai1SessionMessagePageReqVO; |
| 8 | 7 | import cn.iocoder.yudao.module.ai1.dal.dataobject.session.Ai1SessionMessageDO; |
| 9 | 8 | import org.apache.ibatis.annotations.Mapper; |
| ... | ... | @@ -49,23 +48,21 @@ public interface Ai1SessionMessageMapper extends BaseMapperX<Ai1SessionMessageDO |
| 49 | 48 | * 按编号倒序,取指定消息之前最近的 limit 条 |
| 50 | 49 | */ |
| 51 | 50 | default List<Ai1SessionMessageDO> selectListBySessionIdAndIdLessThan(Long sessionId, Long id, Integer limit) { |
| 52 | - return selectList(new QueryWrapperX<Ai1SessionMessageDO>() | |
| 53 | - .limitN(limit) | |
| 54 | - .eq("session_id", sessionId) | |
| 55 | - .lt("id", id) | |
| 56 | - .orderByDesc("id")); | |
| 51 | + return selectListLimitN(new LambdaQueryWrapperX<Ai1SessionMessageDO>() | |
| 52 | + .eq(Ai1SessionMessageDO::getSessionId, sessionId) | |
| 53 | + .lt(Ai1SessionMessageDO::getId, id) | |
| 54 | + .orderByDesc(Ai1SessionMessageDO::getId), limit); | |
| 57 | 55 | } |
| 58 | 56 | |
| 59 | 57 | /** |
| 60 | 58 | * 获得多个会话中,编号大于 id 的第一条指定状态的消息 |
| 61 | 59 | */ |
| 62 | 60 | default Ai1SessionMessageDO selectFirstBySessionIdsAndStatusAndIdGt(Collection<Long> sessionIds, Integer status, Long id) { |
| 63 | - return selectOne(new QueryWrapperX<Ai1SessionMessageDO>() | |
| 64 | - .limitN(1) | |
| 65 | - .in("session_id", sessionIds) | |
| 66 | - .eq("status", status) | |
| 67 | - .gt("id", id) | |
| 68 | - .orderByAsc("id")); | |
| 61 | + return selectFirstOne(new LambdaQueryWrapperX<Ai1SessionMessageDO>() | |
| 62 | + .in(Ai1SessionMessageDO::getSessionId, sessionIds) | |
| 63 | + .eq(Ai1SessionMessageDO::getStatus, status) | |
| 64 | + .gt(Ai1SessionMessageDO::getId, id) | |
| 65 | + .orderByAsc(Ai1SessionMessageDO::getId)); | |
| 69 | 66 | } |
| 70 | 67 | |
| 71 | 68 | /** |
| ... | ... | @@ -73,13 +70,12 @@ public interface Ai1SessionMessageMapper extends BaseMapperX<Ai1SessionMessageDO |
| 73 | 70 | */ |
| 74 | 71 | default List<Ai1SessionMessageDO> selectListBySessionIdsAndStatusAndIdBetween(Collection<Long> sessionIds, Integer status, |
| 75 | 72 | Long minId, Long maxId, Integer limit) { |
| 76 | - return selectList(new QueryWrapperX<Ai1SessionMessageDO>() | |
| 77 | - .limitN(limit) | |
| 78 | - .in("session_id", sessionIds) | |
| 79 | - .eq("status", status) | |
| 80 | - .gt("id", minId) | |
| 81 | - .lt(maxId != null, "id", maxId) | |
| 82 | - .orderByAsc("id")); | |
| 73 | + return selectListLimitN(new LambdaQueryWrapperX<Ai1SessionMessageDO>() | |
| 74 | + .in(Ai1SessionMessageDO::getSessionId, sessionIds) | |
| 75 | + .eq(Ai1SessionMessageDO::getStatus, status) | |
| 76 | + .gt(Ai1SessionMessageDO::getId, minId) | |
| 77 | + .lt(maxId != null, Ai1SessionMessageDO::getId, maxId) | |
| 78 | + .orderByAsc(Ai1SessionMessageDO::getId), limit); | |
| 83 | 79 | } |
| 84 | 80 | |
| 85 | 81 | default Long selectCountBySessionId(Long sessionId) { | ... | ... |
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/CrmContractConfigMapper.java
| 1 | 1 | package cn.iocoder.yudao.module.crm.dal.mysql.contract; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 4 | -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; | |
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | |
| 5 | 5 | import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractConfigDO; |
| 6 | 6 | import org.apache.ibatis.annotations.Mapper; |
| 7 | 7 | |
| ... | ... | @@ -14,7 +14,7 @@ import org.apache.ibatis.annotations.Mapper; |
| 14 | 14 | public interface CrmContractConfigMapper extends BaseMapperX<CrmContractConfigDO> { |
| 15 | 15 | |
| 16 | 16 | default CrmContractConfigDO selectOne() { |
| 17 | - return selectOne(new QueryWrapperX<CrmContractConfigDO>().limitN(1)); | |
| 17 | + return selectFirstOne(new LambdaQueryWrapperX<CrmContractConfigDO>().orderByAsc(CrmContractConfigDO::getId)); | |
| 18 | 18 | } |
| 19 | 19 | |
| 20 | 20 | } |
| 21 | 21 | \ No newline at end of file | ... | ... |
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerPoolConfigMapper.java
| ... | ... | @@ -14,7 +14,8 @@ import org.apache.ibatis.annotations.Mapper; |
| 14 | 14 | public interface CrmCustomerPoolConfigMapper extends BaseMapperX<CrmCustomerPoolConfigDO> { |
| 15 | 15 | |
| 16 | 16 | default CrmCustomerPoolConfigDO selectOne() { |
| 17 | - return selectOne(new LambdaQueryWrapperX<CrmCustomerPoolConfigDO>().last("LIMIT 1")); | |
| 17 | + return selectFirstOne(new LambdaQueryWrapperX<CrmCustomerPoolConfigDO>() | |
| 18 | + .orderByAsc(CrmCustomerPoolConfigDO::getId)); | |
| 18 | 19 | } |
| 19 | 20 | |
| 20 | 21 | } | ... | ... |
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivablePlanMapper.java
| ... | ... | @@ -25,10 +25,9 @@ import java.util.Objects; |
| 25 | 25 | public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO> { |
| 26 | 26 | |
| 27 | 27 | default CrmReceivablePlanDO selectMaxPeriodByContractId(Long contractId) { |
| 28 | - return selectOne(new LambdaQueryWrapperX<CrmReceivablePlanDO>() | |
| 28 | + return selectFirstOne(new LambdaQueryWrapperX<CrmReceivablePlanDO>() | |
| 29 | 29 | .eq(CrmReceivablePlanDO::getContractId, contractId) |
| 30 | - .orderByDesc(CrmReceivablePlanDO::getPeriod) | |
| 31 | - .last("LIMIT 1")); | |
| 30 | + .orderByDesc(CrmReceivablePlanDO::getPeriod)); | |
| 32 | 31 | } |
| 33 | 32 | |
| 34 | 33 | default PageResult<CrmReceivablePlanDO> selectPageByCustomerId(CrmReceivablePlanPageReqVO reqVO) { | ... | ... |
yudao-module-crm/src/main/resources/mapper/permission/CrmOwnerRecordMapper.xml
| ... | ... | @@ -5,7 +5,7 @@ |
| 5 | 5 | |
| 6 | 6 | <select id="selectPoolCustomerPutCountByDate" |
| 7 | 7 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO"> |
| 8 | - SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, | |
| 8 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time, | |
| 9 | 9 | COUNT(*) AS customer_put_count |
| 10 | 10 | FROM crm_owner_record |
| 11 | 11 | WHERE deleted = 0 |
| ... | ... | @@ -16,12 +16,12 @@ |
| 16 | 16 | </foreach> |
| 17 | 17 | AND post_owner_user_id IS NULL |
| 18 | 18 | AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} |
| 19 | - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') | |
| 19 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} | |
| 20 | 20 | </select> |
| 21 | 21 | |
| 22 | 22 | <select id="selectPoolCustomerTakeCountByDate" |
| 23 | 23 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO"> |
| 24 | - SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, | |
| 24 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time, | |
| 25 | 25 | COUNT(*) AS customer_take_count |
| 26 | 26 | FROM crm_owner_record |
| 27 | 27 | WHERE deleted = 0 |
| ... | ... | @@ -32,7 +32,7 @@ |
| 32 | 32 | #{userId} |
| 33 | 33 | </foreach> |
| 34 | 34 | AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} |
| 35 | - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') | |
| 35 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} | |
| 36 | 36 | </select> |
| 37 | 37 | |
| 38 | 38 | <select id="selectPoolCustomerPutCountByUser" | ... | ... |
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
| ... | ... | @@ -4,7 +4,7 @@ |
| 4 | 4 | |
| 5 | 5 | <select id="selectCustomerCreateCountGroupByDate" |
| 6 | 6 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO"> |
| 7 | - SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, | |
| 7 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time, | |
| 8 | 8 | COUNT(*) AS customerCreateCount |
| 9 | 9 | FROM crm_customer |
| 10 | 10 | WHERE deleted = 0 |
| ... | ... | @@ -13,12 +13,12 @@ |
| 13 | 13 | #{userId} |
| 14 | 14 | </foreach> |
| 15 | 15 | AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} |
| 16 | - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') | |
| 16 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} | |
| 17 | 17 | </select> |
| 18 | 18 | |
| 19 | 19 | <select id="selectCustomerDealCountGroupByDate" |
| 20 | 20 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO"> |
| 21 | - SELECT DATE_FORMAT(customer.create_time, '%Y-%m-%d') AS time, | |
| 21 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('customer.create_time')} AS time, | |
| 22 | 22 | COUNT(DISTINCT customer.id) AS customer_deal_count |
| 23 | 23 | FROM crm_customer customer |
| 24 | 24 | LEFT JOIN crm_contract contract ON contract.customer_id = customer.id |
| ... | ... | @@ -30,7 +30,7 @@ |
| 30 | 30 | #{userId} |
| 31 | 31 | </foreach> |
| 32 | 32 | AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} |
| 33 | - GROUP BY DATE_FORMAT(customer.create_time, '%Y-%m-%d') | |
| 33 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('customer.create_time')} | |
| 34 | 34 | </select> |
| 35 | 35 | |
| 36 | 36 | <select id="selectCustomerCreateCountGroupByUser" |
| ... | ... | @@ -96,7 +96,7 @@ |
| 96 | 96 | |
| 97 | 97 | <select id="selectFollowUpRecordCountGroupByDate" |
| 98 | 98 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsFollowUpSummaryByDateRespVO"> |
| 99 | - SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, | |
| 99 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time, | |
| 100 | 100 | COUNT(*) AS follow_up_record_count |
| 101 | 101 | FROM crm_follow_up_record |
| 102 | 102 | WHERE creator IN |
| ... | ... | @@ -105,12 +105,12 @@ |
| 105 | 105 | </foreach> |
| 106 | 106 | AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} |
| 107 | 107 | AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type} |
| 108 | - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') | |
| 108 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} | |
| 109 | 109 | </select> |
| 110 | 110 | |
| 111 | 111 | <select id="selectFollowUpCustomerCountGroupByDate" |
| 112 | 112 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsFollowUpSummaryByDateRespVO"> |
| 113 | - SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, | |
| 113 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time, | |
| 114 | 114 | COUNT(DISTINCT biz_id) AS follow_up_customer_count |
| 115 | 115 | FROM crm_follow_up_record |
| 116 | 116 | WHERE creator IN |
| ... | ... | @@ -119,7 +119,7 @@ |
| 119 | 119 | </foreach> |
| 120 | 120 | AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} |
| 121 | 121 | AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type} |
| 122 | - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') | |
| 122 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} | |
| 123 | 123 | </select> |
| 124 | 124 | |
| 125 | 125 | <select id="selectFollowUpRecordCountGroupByUser" |
| ... | ... | @@ -189,11 +189,11 @@ |
| 189 | 189 | AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} |
| 190 | 190 | </select> |
| 191 | 191 | |
| 192 | - <!-- TIMESTAMPDIFF 用于求差值;AVG 求平均;TRUNCATE 去掉小数点、只保留整数 --> | |
| 192 | + <!-- 成交周期:客户创建到合同下单的完整天数(向零截断)求平均,截断保留 1 位小数,无数据时为 0;表达式按方言见 SqlDialectUtils.customerDealCycle --> | |
| 193 | 193 | <select id="selectCustomerDealCycleGroupByDate" |
| 194 | 194 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByDateRespVO"> |
| 195 | - SELECT DATE_FORMAT(contract.order_date, '%Y-%m-%d') AS time, | |
| 196 | - COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle | |
| 195 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('contract.order_date')} AS time, | |
| 196 | + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@customerDealCycle('customer.create_time', 'contract.order_date')} AS customer_deal_cycle | |
| 197 | 197 | FROM crm_customer customer |
| 198 | 198 | LEFT JOIN crm_contract contract ON contract.customer_id = customer.id |
| 199 | 199 | WHERE customer.deleted = 0 AND contract.deleted = 0 |
| ... | ... | @@ -203,13 +203,13 @@ |
| 203 | 203 | #{userId} |
| 204 | 204 | </foreach> |
| 205 | 205 | AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} |
| 206 | - GROUP BY DATE_FORMAT(contract.order_date, '%Y-%m-%d') | |
| 206 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('contract.order_date')} | |
| 207 | 207 | </select> |
| 208 | 208 | |
| 209 | 209 | <select id="selectCustomerDealCycleGroupByUser" |
| 210 | 210 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByUserRespVO"> |
| 211 | 211 | SELECT customer.owner_user_id, |
| 212 | - COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle | |
| 212 | + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@customerDealCycle('customer.create_time', 'contract.order_date')} AS customer_deal_cycle | |
| 213 | 213 | FROM crm_customer customer |
| 214 | 214 | LEFT JOIN crm_contract contract ON contract.customer_id = customer.id |
| 215 | 215 | WHERE customer.deleted = 0 AND contract.deleted = 0 |
| ... | ... | @@ -225,7 +225,7 @@ |
| 225 | 225 | <select id="selectCustomerDealCycleGroupByAreaId" |
| 226 | 226 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByAreaRespVO"> |
| 227 | 227 | SELECT customer.area_id AS area_id, |
| 228 | - COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle, | |
| 228 | + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@customerDealCycle('customer.create_time', 'contract.order_date')} AS customer_deal_cycle, | |
| 229 | 229 | COUNT(DISTINCT customer.id) AS customer_deal_count |
| 230 | 230 | FROM crm_customer customer |
| 231 | 231 | LEFT JOIN crm_contract contract ON customer.id = contract.customer_id |
| ... | ... | @@ -244,7 +244,7 @@ |
| 244 | 244 | <select id="selectCustomerDealCycleGroupByProductId" |
| 245 | 245 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByProductRespVO"> |
| 246 | 246 | SELECT product.name AS product_name, |
| 247 | - COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle, | |
| 247 | + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@customerDealCycle('customer.create_time', 'contract.order_date')} AS customer_deal_cycle, | |
| 248 | 248 | COUNT(DISTINCT customer.id) AS customer_deal_count |
| 249 | 249 | FROM crm_customer customer |
| 250 | 250 | LEFT JOIN crm_contract contract ON customer.id = contract.customer_id | ... | ... |
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml
| ... | ... | @@ -86,7 +86,7 @@ |
| 86 | 86 | <select id="selectBusinessSummaryGroupByDate" |
| 87 | 87 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO"> |
| 88 | 88 | SELECT |
| 89 | - DATE_FORMAT(create_time, '%Y-%m-%d') AS time, | |
| 89 | + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time, | |
| 90 | 90 | COUNT(*) AS businessCreateCount, |
| 91 | 91 | COALESCE(SUM(total_price), 0) AS totalPrice |
| 92 | 92 | FROM crm_business |
| ... | ... | @@ -100,13 +100,13 @@ |
| 100 | 100 | </foreach> |
| 101 | 101 | AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND |
| 102 | 102 | #{times[1],javaType=java.time.LocalDateTime} |
| 103 | - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') | |
| 103 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} | |
| 104 | 104 | </select> |
| 105 | 105 | |
| 106 | 106 | <select id="selectBusinessInversionRateSummaryByDate" |
| 107 | 107 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessInversionRateSummaryByDateRespVO"> |
| 108 | 108 | SELECT |
| 109 | - DATE_FORMAT(create_time, '%Y-%m-%d') AS time, | |
| 109 | + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time, | |
| 110 | 110 | COUNT(*) AS businessCount, |
| 111 | 111 | SUM(CASE WHEN end_status = 1 THEN 1 ELSE 0 END) AS businessWinCount |
| 112 | 112 | FROM crm_business |
| ... | ... | @@ -120,7 +120,7 @@ |
| 120 | 120 | </foreach> |
| 121 | 121 | AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND |
| 122 | 122 | #{times[1],javaType=java.time.LocalDateTime} |
| 123 | - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') | |
| 123 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} | |
| 124 | 124 | </select> |
| 125 | 125 | |
| 126 | 126 | </mapper> | ... | ... |
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml
| ... | ... | @@ -5,7 +5,7 @@ |
| 5 | 5 | <select id="selectContractCountPerformance" |
| 6 | 6 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO"> |
| 7 | 7 | SELECT |
| 8 | - DATE_FORMAT(order_date, '%Y%m') AS time, | |
| 8 | + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('order_date')} AS time, | |
| 9 | 9 | COUNT(1) AS currentMonthCount |
| 10 | 10 | FROM crm_contract |
| 11 | 11 | WHERE deleted = 0 |
| ... | ... | @@ -16,13 +16,13 @@ |
| 16 | 16 | </foreach> |
| 17 | 17 | AND order_date between #{times[0],javaType=java.time.LocalDateTime} and |
| 18 | 18 | #{times[1],javaType=java.time.LocalDateTime} |
| 19 | - GROUP BY DATE_FORMAT(order_date, '%Y%m') | |
| 19 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('order_date')} | |
| 20 | 20 | </select> |
| 21 | 21 | |
| 22 | 22 | <select id="selectContractPricePerformance" |
| 23 | 23 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO"> |
| 24 | 24 | SELECT |
| 25 | - DATE_FORMAT(order_date, '%Y%m') AS time, | |
| 25 | + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('order_date')} AS time, | |
| 26 | 26 | COALESCE(SUM(total_price), 0) AS currentMonthCount |
| 27 | 27 | FROM crm_contract |
| 28 | 28 | WHERE deleted = 0 |
| ... | ... | @@ -33,13 +33,13 @@ |
| 33 | 33 | </foreach> |
| 34 | 34 | AND order_date between #{times[0],javaType=java.time.LocalDateTime} and |
| 35 | 35 | #{times[1],javaType=java.time.LocalDateTime} |
| 36 | - GROUP BY DATE_FORMAT(order_date, '%Y%m') | |
| 36 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('order_date')} | |
| 37 | 37 | </select> |
| 38 | 38 | |
| 39 | 39 | <select id="selectReceivablePricePerformance" |
| 40 | 40 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO"> |
| 41 | 41 | SELECT |
| 42 | - DATE_FORMAT(return_time, '%Y%m') AS time, | |
| 42 | + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('return_time')} AS time, | |
| 43 | 43 | COALESCE(SUM(price), 0) AS currentMonthCount |
| 44 | 44 | FROM crm_receivable |
| 45 | 45 | WHERE deleted = 0 |
| ... | ... | @@ -50,12 +50,12 @@ |
| 50 | 50 | </foreach> |
| 51 | 51 | AND return_time between #{times[0],javaType=java.time.LocalDateTime} and |
| 52 | 52 | #{times[1],javaType=java.time.LocalDateTime} |
| 53 | - GROUP BY DATE_FORMAT(return_time, '%Y%m') | |
| 53 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('return_time')} | |
| 54 | 54 | </select> |
| 55 | 55 | |
| 56 | 56 | <select id="selectContractSummary" |
| 57 | 57 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceSummaryRespVO"> |
| 58 | - SELECT DATE_FORMAT(contract.order_date, '%Y%m') AS time, | |
| 58 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('contract.order_date')} AS time, | |
| 59 | 59 | COUNT(DISTINCT contract.id) AS contract_count, |
| 60 | 60 | COALESCE(SUM(contract.total_price), 0) AS contract_price, |
| 61 | 61 | COALESCE(SUM(receivable.receivable_price), 0) AS receivable_price, |
| ... | ... | @@ -77,7 +77,7 @@ |
| 77 | 77 | </foreach> |
| 78 | 78 | AND contract.order_date between #{times[0],javaType=java.time.LocalDateTime} and |
| 79 | 79 | #{times[1],javaType=java.time.LocalDateTime} |
| 80 | - GROUP BY DATE_FORMAT(contract.order_date, '%Y%m') | |
| 80 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('contract.order_date')} | |
| 81 | 81 | </select> |
| 82 | 82 | |
| 83 | 83 | </mapper> | ... | ... |
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceTargetMapper.xml
| ... | ... | @@ -10,7 +10,7 @@ |
| 10 | 10 | |
| 11 | 11 | <select id="selectContractActualByMonth" |
| 12 | 12 | resultMap="CrmStatisticsPerformanceTargetActualResultMap"> |
| 13 | - SELECT MONTH(order_date) AS month, | |
| 13 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@month('order_date')} AS month, | |
| 14 | 14 | COALESCE(SUM(total_price), 0) AS current_price |
| 15 | 15 | FROM crm_contract |
| 16 | 16 | WHERE deleted = 0 |
| ... | ... | @@ -19,13 +19,13 @@ |
| 19 | 19 | <foreach collection="userIds" item="userId" open="(" close=")" separator=","> |
| 20 | 20 | #{userId} |
| 21 | 21 | </foreach> |
| 22 | - AND YEAR(order_date) = #{year} | |
| 23 | - GROUP BY MONTH(order_date) | |
| 22 | + AND ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@year('order_date')} = #{year} | |
| 23 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@month('order_date')} | |
| 24 | 24 | </select> |
| 25 | 25 | |
| 26 | 26 | <select id="selectReceivableActualByMonth" |
| 27 | 27 | resultMap="CrmStatisticsPerformanceTargetActualResultMap"> |
| 28 | - SELECT MONTH(return_time) AS month, | |
| 28 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@month('return_time')} AS month, | |
| 29 | 29 | COALESCE(SUM(price), 0) AS current_price |
| 30 | 30 | FROM crm_receivable |
| 31 | 31 | WHERE deleted = 0 |
| ... | ... | @@ -34,8 +34,8 @@ |
| 34 | 34 | <foreach collection="userIds" item="userId" open="(" close=")" separator=","> |
| 35 | 35 | #{userId} |
| 36 | 36 | </foreach> |
| 37 | - AND YEAR(return_time) = #{year} | |
| 38 | - GROUP BY MONTH(return_time) | |
| 37 | + AND ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@year('return_time')} = #{year} | |
| 38 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@month('return_time')} | |
| 39 | 39 | </select> |
| 40 | 40 | |
| 41 | 41 | </mapper> | ... | ... |
yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml
yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/info/HrmEmployeeMapper.java
| ... | ... | @@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.util.object.PageUtils; |
| 9 | 9 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 10 | 10 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 11 | 11 | import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; |
| 12 | +import cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils; | |
| 12 | 13 | import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeePageReqVO; |
| 13 | 14 | import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeChangeRecordDO; |
| 14 | 15 | import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; |
| ... | ... | @@ -286,11 +287,11 @@ public interface HrmEmployeeMapper extends BaseMapperX<HrmEmployeeDO> { |
| 286 | 287 | } else if (HrmEmployeeTodoTypeEnum.PENDING_ENTRY.getType().equals(todoType)) { |
| 287 | 288 | query.betweenIfPresent(HrmEmployeeDO::getEntryTime, monthTimes); |
| 288 | 289 | } else if (HrmEmployeeTodoTypeEnum.BIRTHDAY.getType().equals(todoType)) { |
| 289 | - // MySQL 和 H2 MySQL 模式均支持 MONTH,用于跨年筛选当月生日。 | |
| 290 | - query.apply("MONTH(birthday) = {0}", now.getMonthValue()); | |
| 290 | + // 按当前路由数据源的方言取月份,用于跨年筛选当月生日;MySQL、H2 等保持 MONTH。 | |
| 291 | + query.apply(SqlDialectUtils.month("birthday") + " = {0}", now.getMonthValue()); | |
| 291 | 292 | if (now.getMonthValue() == 2 && !now.isLeapYear()) { |
| 292 | 293 | // 非闰年不存在 2 月 29 日,与首页日历和年龄计算保持一致。 |
| 293 | - query.apply("DAYOFMONTH(birthday) <> 29"); | |
| 294 | + query.apply(SqlDialectUtils.dayOfMonth("birthday") + " <> 29"); | |
| 294 | 295 | } |
| 295 | 296 | } |
| 296 | 297 | } | ... | ... |
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/recruit/candidate/HrmRecruitInterviewMapper.java
| ... | ... | @@ -21,11 +21,10 @@ public interface HrmRecruitInterviewMapper extends BaseMapperX<HrmRecruitIntervi |
| 21 | 21 | } |
| 22 | 22 | |
| 23 | 23 | default HrmRecruitInterviewDO selectLatestByCandidateId(Long candidateId) { |
| 24 | - return selectOne(new LambdaQueryWrapperX<HrmRecruitInterviewDO>() | |
| 24 | + return selectFirstOne(new LambdaQueryWrapperX<HrmRecruitInterviewDO>() | |
| 25 | 25 | .eq(HrmRecruitInterviewDO::getCandidateId, candidateId) |
| 26 | 26 | .orderByDesc(HrmRecruitInterviewDO::getStageNumber) |
| 27 | - .orderByDesc(HrmRecruitInterviewDO::getId) | |
| 28 | - .last("LIMIT 1")); | |
| 27 | + .orderByDesc(HrmRecruitInterviewDO::getId)); | |
| 29 | 28 | } |
| 30 | 29 | |
| 31 | 30 | default void deleteByCandidateId(Long candidateId) { | ... | ... |
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/config/HrmSalaryConfigMapper.java
| ... | ... | @@ -9,9 +9,8 @@ import org.apache.ibatis.annotations.Mapper; |
| 9 | 9 | public interface HrmSalaryConfigMapper extends BaseMapperX<HrmSalaryConfigDO> { |
| 10 | 10 | |
| 11 | 11 | default HrmSalaryConfigDO selectFirst() { |
| 12 | - return selectOne(new LambdaQueryWrapperX<HrmSalaryConfigDO>() | |
| 13 | - .orderByAsc(HrmSalaryConfigDO::getId) | |
| 14 | - .last("LIMIT 1")); | |
| 12 | + return selectFirstOne(new LambdaQueryWrapperX<HrmSalaryConfigDO>() | |
| 13 | + .orderByAsc(HrmSalaryConfigDO::getId)); | |
| 15 | 14 | } |
| 16 | 15 | |
| 17 | 16 | } | ... | ... |
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/config/HrmSalaryOptionMapper.java
| ... | ... | @@ -17,7 +17,8 @@ public interface HrmSalaryOptionMapper extends BaseMapperX<HrmSalaryOptionDO> { |
| 17 | 17 | return selectFirstOne(HrmSalaryOptionDO::getCode, code); |
| 18 | 18 | } |
| 19 | 19 | |
| 20 | - @Select("SELECT * FROM hrm_salary_option WHERE code = #{code} LIMIT 1") | |
| 20 | + @Select("SELECT * FROM hrm_salary_option WHERE code = #{code} " | |
| 21 | + + "${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@limitRows(1, false)}") | |
| 21 | 22 | HrmSalaryOptionDO selectByCodeIncludeDeleted(@Param("code") Integer code); |
| 22 | 23 | |
| 23 | 24 | default HrmSalaryOptionDO selectByCodeForUpdate(Integer code) { | ... | ... |
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/monthrecord/HrmSalaryMonthRecordMapper.java
| ... | ... | @@ -24,10 +24,9 @@ public interface HrmSalaryMonthRecordMapper extends BaseMapperX<HrmSalaryMonthRe |
| 24 | 24 | } |
| 25 | 25 | |
| 26 | 26 | default HrmSalaryMonthRecordDO selectLast() { |
| 27 | - return selectOne(new LambdaQueryWrapperX<HrmSalaryMonthRecordDO>() | |
| 27 | + return selectFirstOne(new LambdaQueryWrapperX<HrmSalaryMonthRecordDO>() | |
| 28 | 28 | .orderByDesc(HrmSalaryMonthRecordDO::getYear) |
| 29 | - .orderByDesc(HrmSalaryMonthRecordDO::getMonth) | |
| 30 | - .last("LIMIT 1")); | |
| 29 | + .orderByDesc(HrmSalaryMonthRecordDO::getMonth)); | |
| 31 | 30 | } |
| 32 | 31 | |
| 33 | 32 | default PageResult<HrmSalaryMonthRecordDO> selectPage(HrmSalaryMonthRecordPageReqVO reqVO) { | ... | ... |
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/slip/HrmSalarySlipTemplateMapper.java
| ... | ... | @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.hrm.dal.mysql.salary.slip; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 4 | 4 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 5 | -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; | |
| 5 | +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; | |
| 6 | 6 | import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.slip.HrmSalarySlipTemplateDO; |
| 7 | 7 | import org.apache.ibatis.annotations.Mapper; |
| 8 | 8 | |
| ... | ... | @@ -25,11 +25,10 @@ public interface HrmSalarySlipTemplateMapper extends BaseMapperX<HrmSalarySlipTe |
| 25 | 25 | * @return 平台级默认工资条模板 |
| 26 | 26 | */ |
| 27 | 27 | default HrmSalarySlipTemplateDO selectGlobalDefaultTemplate() { |
| 28 | - return selectOne(new QueryWrapperX<HrmSalarySlipTemplateDO>() | |
| 29 | - .eq("tenant_id", 0L) | |
| 30 | - .eq("default_status", true) | |
| 31 | - .orderByDesc("id") | |
| 32 | - .limitN(1)); | |
| 28 | + return selectFirstOne(new LambdaQueryWrapperX<HrmSalarySlipTemplateDO>() | |
| 29 | + .apply("tenant_id = {0}", TenantUtils.GLOBAL_TENANT_ID) | |
| 30 | + .eq(HrmSalarySlipTemplateDO::getDefaultStatus, true) | |
| 31 | + .orderByDesc(HrmSalarySlipTemplateDO::getId)); | |
| 33 | 32 | } |
| 34 | 33 | |
| 35 | 34 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ImConversationReadMapper.java
| ... | ... | @@ -57,8 +57,7 @@ public interface ImConversationReadMapper extends BaseMapperX<ImConversationRead |
| 57 | 57 | query.and(w -> w.gt(ImConversationReadDO::getUpdateTime, lastTime) |
| 58 | 58 | .or(n -> n.eq(ImConversationReadDO::getUpdateTime, lastTime).gt(ImConversationReadDO::getId, lastId))); |
| 59 | 59 | } |
| 60 | - return selectList(query.orderByAsc(ImConversationReadDO::getUpdateTime).orderByAsc(ImConversationReadDO::getId) | |
| 61 | - .last("LIMIT " + limit)); | |
| 60 | + return selectListLimitN(query.orderByAsc(ImConversationReadDO::getUpdateTime).orderByAsc(ImConversationReadDO::getId), limit); | |
| 62 | 61 | } |
| 63 | 62 | |
| 64 | 63 | /** | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java
| ... | ... | @@ -50,8 +50,7 @@ public interface ImFriendMapper extends BaseMapperX<ImFriendDO> { |
| 50 | 50 | query.and(w -> w.gt(ImFriendDO::getUpdateTime, lastTime) |
| 51 | 51 | .or(n -> n.eq(ImFriendDO::getUpdateTime, lastTime).gt(ImFriendDO::getId, lastId))); |
| 52 | 52 | } |
| 53 | - return selectList(query.orderByAsc(ImFriendDO::getUpdateTime).orderByAsc(ImFriendDO::getId) | |
| 54 | - .last("LIMIT " + limit)); | |
| 53 | + return selectListLimitN(query.orderByAsc(ImFriendDO::getUpdateTime).orderByAsc(ImFriendDO::getId), limit); | |
| 55 | 54 | } |
| 56 | 55 | |
| 57 | 56 | default List<ImFriendDO> selectListByUserIdAndStatus(Long userId, Integer status) { | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java
| ... | ... | @@ -47,9 +47,8 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> { |
| 47 | 47 | .lt(ImFriendRequestDO::getId, maxId))); |
| 48 | 48 | } |
| 49 | 49 | wrapper.orderByDesc(ImFriendRequestDO::getUpdateTime) |
| 50 | - .orderByDesc(ImFriendRequestDO::getId) | |
| 51 | - .last("LIMIT " + limit); | |
| 52 | - return selectList(wrapper); | |
| 50 | + .orderByDesc(ImFriendRequestDO::getId); | |
| 51 | + return selectListLimitN(wrapper, limit); | |
| 53 | 52 | } |
| 54 | 53 | |
| 55 | 54 | /** |
| ... | ... | @@ -70,8 +69,8 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> { |
| 70 | 69 | query.and(w -> w.gt(ImFriendRequestDO::getUpdateTime, lastTime) |
| 71 | 70 | .or(n -> n.eq(ImFriendRequestDO::getUpdateTime, lastTime).gt(ImFriendRequestDO::getId, lastId))); |
| 72 | 71 | } |
| 73 | - return selectList(query.orderByAsc(ImFriendRequestDO::getUpdateTime).orderByAsc(ImFriendRequestDO::getId) | |
| 74 | - .last("LIMIT " + limit)); | |
| 72 | + return selectListLimitN(query.orderByAsc(ImFriendRequestDO::getUpdateTime).orderByAsc(ImFriendRequestDO::getId), | |
| 73 | + limit); | |
| 75 | 74 | } |
| 76 | 75 | |
| 77 | 76 | default int updateByIdAndHandleResult(Long id, Integer handleResult, ImFriendRequestDO updateObj) { | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java
| ... | ... | @@ -62,8 +62,7 @@ public interface ImGroupRequestMapper extends BaseMapperX<ImGroupRequestDO> { |
| 62 | 62 | query.and(w -> w.gt(ImGroupRequestDO::getUpdateTime, lastTime) |
| 63 | 63 | .or(n -> n.eq(ImGroupRequestDO::getUpdateTime, lastTime).gt(ImGroupRequestDO::getId, lastId))); |
| 64 | 64 | } |
| 65 | - return selectList(query.orderByAsc(ImGroupRequestDO::getUpdateTime).orderByAsc(ImGroupRequestDO::getId) | |
| 66 | - .last("LIMIT " + limit)); | |
| 65 | + return selectListLimitN(query.orderByAsc(ImGroupRequestDO::getUpdateTime).orderByAsc(ImGroupRequestDO::getId), limit); | |
| 67 | 66 | } |
| 68 | 67 | |
| 69 | 68 | default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) { | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImChannelMessageMapper.java
| ... | ... | @@ -30,13 +30,12 @@ public interface ImChannelMessageMapper extends BaseMapperX<ImChannelMessageDO> |
| 30 | 30 | * @return 频道消息列表;按 id 升序 |
| 31 | 31 | */ |
| 32 | 32 | default List<ImChannelMessageDO> selectListByUserAndMinId(Long userId, Long minId, Integer size) { |
| 33 | - return selectList(new LambdaQueryWrapperX<ImChannelMessageDO>() | |
| 33 | + return selectListLimitN(new LambdaQueryWrapperX<ImChannelMessageDO>() | |
| 34 | 34 | .gt(ImChannelMessageDO::getId, minId) |
| 35 | 35 | .and(w -> w.isNull(ImChannelMessageDO::getReceiverUserIds) |
| 36 | 36 | .or().eq(ImChannelMessageDO::getReceiverUserIds, Collections.emptyList()) |
| 37 | 37 | .or().apply(MyBatisUtils.findInSet("receiver_user_ids"), userId)) |
| 38 | - .orderByAsc(ImChannelMessageDO::getId) | |
| 39 | - .last("LIMIT " + size)); | |
| 38 | + .orderByAsc(ImChannelMessageDO::getId), size); | |
| 40 | 39 | } |
| 41 | 40 | |
| 42 | 41 | default Long selectCountByMaterialId(Long materialId) { | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapper.java
| ... | ... | @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.im.dal.mysql.message; |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 5 | 5 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 6 | -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; | |
| 7 | 6 | import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; |
| 8 | 7 | import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO; |
| 9 | 8 | import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; |
| ... | ... | @@ -35,14 +34,13 @@ public interface ImGroupMessageMapper extends BaseMapperX<ImGroupMessageDO> { |
| 35 | 34 | */ |
| 36 | 35 | default List<ImGroupMessageDO> selectListByMinId(Long userId, Collection<Long> groupIds, Long minId, |
| 37 | 36 | LocalDateTime minSendTime, Integer size) { |
| 38 | - QueryWrapperX<ImGroupMessageDO> wrapper = new QueryWrapperX<>(); | |
| 39 | - wrapper.in("group_id", groupIds) | |
| 40 | - .gt("id", minId) | |
| 41 | - .gt("send_time", minSendTime) | |
| 37 | + LambdaQueryWrapperX<ImGroupMessageDO> wrapper = new LambdaQueryWrapperX<>(); | |
| 38 | + wrapper.in(ImGroupMessageDO::getGroupId, groupIds) | |
| 39 | + .gt(ImGroupMessageDO::getId, minId) | |
| 40 | + .gt(ImGroupMessageDO::getSendTime, minSendTime) | |
| 42 | 41 | .apply(MyBatisUtils.findInSet("receiver_user_ids"), userId) |
| 43 | - .orderByAsc("id"); | |
| 44 | - wrapper.limitN(size); | |
| 45 | - return selectList(wrapper); | |
| 42 | + .orderByAsc(ImGroupMessageDO::getId); | |
| 43 | + return selectListLimitN(wrapper, size); | |
| 46 | 44 | } |
| 47 | 45 | |
| 48 | 46 | /** |
| ... | ... | @@ -55,13 +53,12 @@ public interface ImGroupMessageMapper extends BaseMapperX<ImGroupMessageDO> { |
| 55 | 53 | * @return 消息列表(按 id 倒序) |
| 56 | 54 | */ |
| 57 | 55 | default List<ImGroupMessageDO> selectHistoryListByUser(Long userId, Long groupId, Long maxId, Integer limit) { |
| 58 | - QueryWrapperX<ImGroupMessageDO> wrapper = new QueryWrapperX<>(); | |
| 59 | - wrapper.eq("group_id", groupId) | |
| 60 | - .lt(maxId != null, "id", maxId) | |
| 56 | + LambdaQueryWrapperX<ImGroupMessageDO> wrapper = new LambdaQueryWrapperX<>(); | |
| 57 | + wrapper.eq(ImGroupMessageDO::getGroupId, groupId) | |
| 58 | + .lt(maxId != null, ImGroupMessageDO::getId, maxId) | |
| 61 | 59 | .apply(MyBatisUtils.findInSet("receiver_user_ids"), userId) |
| 62 | - .orderByDesc("id"); | |
| 63 | - wrapper.limitN(limit); | |
| 64 | - return selectList(wrapper); | |
| 60 | + .orderByDesc(ImGroupMessageDO::getId); | |
| 61 | + return selectListLimitN(wrapper, limit); | |
| 65 | 62 | } |
| 66 | 63 | |
| 67 | 64 | default ImGroupMessageDO selectBySenderIdAndClientMessageId(Long senderId, String clientMessageId) { | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapper.java
| ... | ... | @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.im.dal.mysql.message; |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 5 | 5 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 6 | -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; | |
| 7 | 6 | import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO; |
| 8 | 7 | import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; |
| 9 | 8 | import org.apache.ibatis.annotations.Mapper; |
| ... | ... | @@ -30,15 +29,14 @@ public interface ImPrivateMessageMapper extends BaseMapperX<ImPrivateMessageDO> |
| 30 | 29 | */ |
| 31 | 30 | default List<ImPrivateMessageDO> selectListByMinId(Long userId, Long minId, |
| 32 | 31 | LocalDateTime minSendTime, Integer size) { |
| 33 | - QueryWrapperX<ImPrivateMessageDO> wrapper = new QueryWrapperX<>(); | |
| 34 | - wrapper.and(w -> w.eq("sender_id", userId) | |
| 32 | + LambdaQueryWrapperX<ImPrivateMessageDO> wrapper = new LambdaQueryWrapperX<>(); | |
| 33 | + wrapper.and(w -> w.eq(ImPrivateMessageDO::getSenderId, userId) | |
| 35 | 34 | .or() |
| 36 | - .eq("receiver_id", userId)) | |
| 37 | - .gt("id", minId) | |
| 38 | - .gt("send_time", minSendTime) | |
| 39 | - .orderByAsc("id"); | |
| 40 | - wrapper.limitN(size); | |
| 41 | - return selectList(wrapper); | |
| 35 | + .eq(ImPrivateMessageDO::getReceiverId, userId)) | |
| 36 | + .gt(ImPrivateMessageDO::getId, minId) | |
| 37 | + .gt(ImPrivateMessageDO::getSendTime, minSendTime) | |
| 38 | + .orderByAsc(ImPrivateMessageDO::getId); | |
| 39 | + return selectListLimitN(wrapper, size); | |
| 42 | 40 | } |
| 43 | 41 | |
| 44 | 42 | /** |
| ... | ... | @@ -51,14 +49,13 @@ public interface ImPrivateMessageMapper extends BaseMapperX<ImPrivateMessageDO> |
| 51 | 49 | * @return 消息列表(按 id 倒序) |
| 52 | 50 | */ |
| 53 | 51 | default List<ImPrivateMessageDO> selectHistoryList(Long userId, Long receiverId, Long maxId, Integer limit) { |
| 54 | - QueryWrapperX<ImPrivateMessageDO> wrapper = new QueryWrapperX<>(); | |
| 55 | - wrapper.and(w -> w.eq("sender_id", userId).eq("receiver_id", receiverId) | |
| 52 | + LambdaQueryWrapperX<ImPrivateMessageDO> wrapper = new LambdaQueryWrapperX<>(); | |
| 53 | + wrapper.and(w -> w.eq(ImPrivateMessageDO::getSenderId, userId).eq(ImPrivateMessageDO::getReceiverId, receiverId) | |
| 56 | 54 | .or() |
| 57 | - .eq("sender_id", receiverId).eq("receiver_id", userId)) | |
| 58 | - .lt(maxId != null, "id", maxId) | |
| 59 | - .orderByDesc("id"); | |
| 60 | - wrapper.limitN(limit); | |
| 61 | - return selectList(wrapper); | |
| 55 | + .eq(ImPrivateMessageDO::getSenderId, receiverId).eq(ImPrivateMessageDO::getReceiverId, userId)) | |
| 56 | + .lt(maxId != null, ImPrivateMessageDO::getId, maxId) | |
| 57 | + .orderByDesc(ImPrivateMessageDO::getId); | |
| 58 | + return selectListLimitN(wrapper, limit); | |
| 62 | 59 | } |
| 63 | 60 | |
| 64 | 61 | default ImPrivateMessageDO selectBySenderIdAndClientMessageId(Long senderId, String clientMessageId) { | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java
| ... | ... | @@ -58,9 +58,9 @@ public interface ImStatisticsManagerMapper { |
| 58 | 58 | * |
| 59 | 59 | * @return [{statDate: "yyyy-MM-dd", count: 123}, ...] |
| 60 | 60 | */ |
| 61 | - @Select("SELECT DATE(create_time) AS statDate, COUNT(*) AS count FROM system_users " + | |
| 61 | + @Select("SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('create_time')} AS statDate, COUNT(*) AS count FROM system_users " + | |
| 62 | 62 | "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime} " + |
| 63 | - "GROUP BY DATE(create_time)") | |
| 63 | + "GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('create_time')}") | |
| 64 | 64 | @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) |
| 65 | 65 | List<Map<String, Object>> selectNewUserDailyCount(@Param("beginTime") LocalDateTime beginTime, |
| 66 | 66 | @Param("endTime") LocalDateTime endTime); |
| ... | ... | @@ -69,11 +69,11 @@ public interface ImStatisticsManagerMapper { |
| 69 | 69 | * 区间内每日活跃用户数(按天分组、跨私聊+群聊去重) |
| 70 | 70 | */ |
| 71 | 71 | @Select("SELECT day AS statDate, COUNT(DISTINCT user_id) AS count FROM (" + |
| 72 | - " SELECT DATE(send_time) AS day, sender_id AS user_id FROM im_private_message " + | |
| 72 | + " SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')} AS day, sender_id AS user_id FROM im_private_message " + | |
| 73 | 73 | " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + |
| 74 | 74 | " AND send_time >= #{beginTime} AND send_time < #{endTime}" + |
| 75 | 75 | " UNION ALL " + |
| 76 | - " SELECT DATE(send_time) AS day, sender_id AS user_id FROM im_group_message " + | |
| 76 | + " SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')} AS day, sender_id AS user_id FROM im_group_message " + | |
| 77 | 77 | " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + |
| 78 | 78 | " AND send_time >= #{beginTime} AND send_time < #{endTime}" + |
| 79 | 79 | ") t GROUP BY day") |
| ... | ... | @@ -143,10 +143,10 @@ public interface ImStatisticsManagerMapper { |
| 143 | 143 | /** |
| 144 | 144 | * 区间内每日私聊消息数(按天分组) |
| 145 | 145 | */ |
| 146 | - @Select("SELECT DATE(send_time) AS statDate, COUNT(*) AS count FROM im_private_message " + | |
| 146 | + @Select("SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')} AS statDate, COUNT(*) AS count FROM im_private_message " + | |
| 147 | 147 | "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + |
| 148 | 148 | " AND send_time >= #{beginTime} AND send_time < #{endTime} " + |
| 149 | - "GROUP BY DATE(send_time)") | |
| 149 | + "GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')}") | |
| 150 | 150 | @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) |
| 151 | 151 | List<Map<String, Object>> selectPrivateMessageDailyCount(@Param("beginTime") LocalDateTime beginTime, |
| 152 | 152 | @Param("endTime") LocalDateTime endTime); |
| ... | ... | @@ -154,10 +154,10 @@ public interface ImStatisticsManagerMapper { |
| 154 | 154 | /** |
| 155 | 155 | * 区间内每日群聊消息数(按天分组) |
| 156 | 156 | */ |
| 157 | - @Select("SELECT DATE(send_time) AS statDate, COUNT(*) AS count FROM im_group_message " + | |
| 157 | + @Select("SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')} AS statDate, COUNT(*) AS count FROM im_group_message " + | |
| 158 | 158 | "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + |
| 159 | 159 | " AND send_time >= #{beginTime} AND send_time < #{endTime} " + |
| 160 | - "GROUP BY DATE(send_time)") | |
| 160 | + "GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')}") | |
| 161 | 161 | @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) |
| 162 | 162 | List<Map<String, Object>> selectGroupMessageDailyCount(@Param("beginTime") LocalDateTime beginTime, |
| 163 | 163 | @Param("endTime") LocalDateTime endTime); |
| ... | ... | @@ -193,7 +193,8 @@ public interface ImStatisticsManagerMapper { |
| 193 | 193 | " SELECT sender_id AS user_id FROM im_group_message " + |
| 194 | 194 | " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + |
| 195 | 195 | " AND send_time >= #{beginTime} AND send_time < #{endTime}" + |
| 196 | - ") t GROUP BY user_id ORDER BY messageCount DESC LIMIT #{limit}") | |
| 196 | + ") t GROUP BY user_id ORDER BY messageCount DESC " + | |
| 197 | + "${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@limitParam('limit', true)}") | |
| 197 | 198 | @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) |
| 198 | 199 | List<Map<String, Object>> selectTopSenders(@Param("beginTime") LocalDateTime beginTime, |
| 199 | 200 | @Param("endTime") LocalDateTime endTime, | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobLogMapper.java
| ... | ... | @@ -37,7 +37,7 @@ public interface JobLogMapper extends BaseMapperX<JobLogDO> { |
| 37 | 37 | * @param limit 删除条数,防止一次删除太多 |
| 38 | 38 | * @return 删除条数 |
| 39 | 39 | */ |
| 40 | - @Delete("DELETE FROM infra_job_log WHERE create_time < #{createTime} LIMIT #{limit}") | |
| 40 | + @Delete("${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('infra_job_log', 'create_time', 'createTime', 'limit')}") | |
| 41 | 41 | Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit); |
| 42 | 42 | |
| 43 | 43 | } | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiAccessLogMapper.java
| ... | ... | @@ -39,7 +39,7 @@ public interface ApiAccessLogMapper extends BaseMapperX<ApiAccessLogDO> { |
| 39 | 39 | * @param limit 删除条数,防止一次删除太多 |
| 40 | 40 | * @return 删除条数 |
| 41 | 41 | */ |
| 42 | - @Delete("DELETE FROM infra_api_access_log WHERE create_time < #{createTime} LIMIT #{limit}") | |
| 42 | + @Delete("${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('infra_api_access_log', 'create_time', 'createTime', 'limit')}") | |
| 43 | 43 | Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit); |
| 44 | 44 | |
| 45 | 45 | } | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiErrorLogMapper.java
| ... | ... | @@ -38,7 +38,7 @@ public interface ApiErrorLogMapper extends BaseMapperX<ApiErrorLogDO> { |
| 38 | 38 | * @param limit 删除条数,防止一次删除太多 |
| 39 | 39 | * @return 删除条数 |
| 40 | 40 | */ |
| 41 | - @Delete("DELETE FROM infra_api_error_log WHERE create_time < #{createTime} LIMIT #{limit}") | |
| 41 | + @Delete("${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('infra_api_error_log', 'create_time', 'createTime', 'limit')}") | |
| 42 | 42 | Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit); |
| 43 | 43 | |
| 44 | 44 | } | ... | ... |
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/bargain/BargainActivityMapper.java
| ... | ... | @@ -76,11 +76,11 @@ public interface BargainActivityMapper extends BaseMapperX<BargainActivityDO> { |
| 76 | 76 | * @return 活动分页 |
| 77 | 77 | */ |
| 78 | 78 | default List<BargainActivityDO> selectList(Integer count, Integer status, LocalDateTime now) { |
| 79 | - return selectList(new LambdaQueryWrapperX<BargainActivityDO>() | |
| 79 | + return selectListLimitN(new LambdaQueryWrapperX<BargainActivityDO>() | |
| 80 | 80 | .eq(BargainActivityDO::getStatus, status) |
| 81 | 81 | .le(BargainActivityDO::getStartTime, now) |
| 82 | 82 | .ge(BargainActivityDO::getEndTime, now) |
| 83 | - .last("LIMIT " + count)); | |
| 83 | + .orderByAsc(BargainActivityDO::getId), count); | |
| 84 | 84 | } |
| 85 | 85 | |
| 86 | 86 | default BargainActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) { | ... | ... |
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/bargain/BargainRecordMapper.java
| ... | ... | @@ -43,11 +43,10 @@ public interface BargainRecordMapper extends BaseMapperX<BargainRecordDO> { |
| 43 | 43 | } |
| 44 | 44 | |
| 45 | 45 | default BargainRecordDO selectLastByUserIdAndActivityId(Long userId, Long activityId) { |
| 46 | - return selectOne(new LambdaQueryWrapper<>(BargainRecordDO.class) | |
| 46 | + return selectFirstOne(new LambdaQueryWrapper<>(BargainRecordDO.class) | |
| 47 | 47 | .eq(BargainRecordDO::getUserId, userId) |
| 48 | 48 | .eq(BargainRecordDO::getActivityId, activityId) |
| 49 | - .orderByDesc(BargainRecordDO::getId) | |
| 50 | - .last("LIMIT 1")); | |
| 49 | + .orderByDesc(BargainRecordDO::getId)); | |
| 51 | 50 | } |
| 52 | 51 | |
| 53 | 52 | default Long selectCountByUserIdAndActivityIdAndStatus( |
| ... | ... | @@ -104,9 +103,9 @@ public interface BargainRecordMapper extends BaseMapperX<BargainRecordDO> { |
| 104 | 103 | } |
| 105 | 104 | |
| 106 | 105 | default List<BargainRecordDO> selectListByStatusAndCount(Integer status, Integer count) { |
| 107 | - return selectList(new LambdaQueryWrapper<>(BargainRecordDO.class) | |
| 106 | + return selectListLimitN(new LambdaQueryWrapper<>(BargainRecordDO.class) | |
| 108 | 107 | .eq(BargainRecordDO::getStatus, status) |
| 109 | - .last("LIMIT " + count)); | |
| 108 | + .orderByAsc(BargainRecordDO::getId), count); | |
| 110 | 109 | } |
| 111 | 110 | |
| 112 | 111 | /** | ... | ... |
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationRecordMapper.java
| ... | ... | @@ -63,19 +63,17 @@ public interface CombinationRecordMapper extends BaseMapperX<CombinationRecordDO |
| 63 | 63 | * @return 拼团记录列表 |
| 64 | 64 | */ |
| 65 | 65 | default List<CombinationRecordDO> selectLatestList(int count) { |
| 66 | - return selectList(new LambdaQueryWrapperX<CombinationRecordDO>() | |
| 67 | - .orderByDesc(CombinationRecordDO::getId) | |
| 68 | - .last("LIMIT " + count)); | |
| 66 | + return selectListLimitN(new LambdaQueryWrapperX<CombinationRecordDO>() | |
| 67 | + .orderByDesc(CombinationRecordDO::getId), count); | |
| 69 | 68 | } |
| 70 | 69 | |
| 71 | 70 | default List<CombinationRecordDO> selectListByActivityIdAndStatusAndHeadId(Long activityId, Integer status, |
| 72 | 71 | Long headId, Integer count) { |
| 73 | - return selectList(new LambdaQueryWrapperX<CombinationRecordDO>() | |
| 72 | + return selectListLimitN(new LambdaQueryWrapperX<CombinationRecordDO>() | |
| 74 | 73 | .eqIfPresent(CombinationRecordDO::getActivityId, activityId) |
| 75 | 74 | .eqIfPresent(CombinationRecordDO::getStatus, status) |
| 76 | 75 | .eq(CombinationRecordDO::getHeadId, headId) |
| 77 | - .orderByDesc(CombinationRecordDO::getId) | |
| 78 | - .last("LIMIT " + count)); | |
| 76 | + .orderByDesc(CombinationRecordDO::getId), count); | |
| 79 | 77 | } |
| 80 | 78 | |
| 81 | 79 | default Map<Long, Integer> selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(Collection<Long> activityIds, | ... | ... |
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java
100755 → 100644
| ... | ... | @@ -60,13 +60,12 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> { |
| 60 | 60 | default List<CouponTemplateDO> selectList(List<Integer> canTakeTypes, Integer productScope, Long productScopeValue, Integer count) { |
| 61 | 61 | // 构建可领取的查询条件 |
| 62 | 62 | Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = buildCanTakeQueryConsumer(canTakeTypes); |
| 63 | - return selectList(new LambdaQueryWrapperX<CouponTemplateDO>() | |
| 63 | + return selectListLimitN(new LambdaQueryWrapperX<CouponTemplateDO>() | |
| 64 | 64 | .eqIfPresent(CouponTemplateDO::getProductScope, productScope) |
| 65 | 65 | .and(productScopeValue != null, w -> w.apply(MyBatisUtils.findInSet("product_scope_values"), |
| 66 | 66 | productScopeValue)) |
| 67 | 67 | .and(canTakeConsumer != null, canTakeConsumer) |
| 68 | - .last(" LIMIT " + count) | |
| 69 | - .orderByDesc(CouponTemplateDO::getId)); | |
| 68 | + .orderByDesc(CouponTemplateDO::getId), count); | |
| 70 | 69 | } |
| 71 | 70 | |
| 72 | 71 | static Consumer<LambdaQueryWrapper<CouponTemplateDO>> buildCanTakeQueryConsumer(List<Integer> canTakeTypes) { | ... | ... |
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/kefu/KeFuMessageMapper.java
| 1 | 1 | package cn.iocoder.yudao.module.promotion.dal.mysql.kefu; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 4 | -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; | |
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | |
| 5 | 5 | import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageListReqVO; |
| 6 | 6 | import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO; |
| 7 | 7 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| ... | ... | @@ -28,11 +28,10 @@ public interface KeFuMessageMapper extends BaseMapperX<KeFuMessageDO> { |
| 28 | 28 | * @return 消息列表 |
| 29 | 29 | */ |
| 30 | 30 | default List<KeFuMessageDO> selectList(KeFuMessageListReqVO reqVO) { |
| 31 | - return selectList(new QueryWrapperX<KeFuMessageDO>() | |
| 32 | - .eqIfPresent("conversation_id", reqVO.getConversationId()) | |
| 33 | - .ltIfPresent("create_time", reqVO.getCreateTime()) | |
| 34 | - .orderByDesc("create_time") | |
| 35 | - .limitN(reqVO.getLimit())); | |
| 31 | + return selectListLimitN(new LambdaQueryWrapperX<KeFuMessageDO>() | |
| 32 | + .eqIfPresent(KeFuMessageDO::getConversationId, reqVO.getConversationId()) | |
| 33 | + .ltIfPresent(KeFuMessageDO::getCreateTime, reqVO.getCreateTime()) | |
| 34 | + .orderByDesc(KeFuMessageDO::getCreateTime), reqVO.getLimit()); | |
| 36 | 35 | } |
| 37 | 36 | |
| 38 | 37 | default List<KeFuMessageDO> selectListByConversationIdAndUserTypeAndReadStatus(Long conversationId, Integer userType, | ... | ... |
yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/member/MemberStatisticsMapper.xml
| ... | ... | @@ -57,7 +57,7 @@ |
| 57 | 57 | </resultMap> |
| 58 | 58 | |
| 59 | 59 | <select id="selectListByCreateTimeBetween" resultMap="MemberRegisterCountRespVOMap"> |
| 60 | - SELECT CAST(create_time AS DATE) AS statDate, | |
| 60 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDate('create_time')} AS statDate, | |
| 61 | 61 | count(1) AS count |
| 62 | 62 | FROM member_user |
| 63 | 63 | WHERE create_time BETWEEN #{beginTime} AND #{endTime} |
| ... | ... | @@ -65,7 +65,7 @@ |
| 65 | 65 | <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> |
| 66 | 66 | AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} |
| 67 | 67 | </if> |
| 68 | - GROUP BY CAST(create_time AS DATE) | |
| 68 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDate('create_time')} | |
| 69 | 69 | ORDER BY statDate |
| 70 | 70 | </select> |
| 71 | 71 | ... | ... |
yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml
| ... | ... | @@ -99,7 +99,7 @@ |
| 99 | 99 | </resultMap> |
| 100 | 100 | |
| 101 | 101 | <select id="selectListByPayTimeBetweenAndGroupByDay" resultMap="TradeOrderTrendRespVOMap"> |
| 102 | - SELECT CAST(pay_time AS DATE) AS statDate, | |
| 102 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDateText('pay_time')} AS statDate, | |
| 103 | 103 | COUNT(1) AS order_pay_count, |
| 104 | 104 | SUM(pay_price) AS order_pay_price |
| 105 | 105 | FROM trade_order |
| ... | ... | @@ -109,13 +109,12 @@ |
| 109 | 109 | <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> |
| 110 | 110 | AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} |
| 111 | 111 | </if> |
| 112 | - GROUP BY CAST(pay_time AS DATE) | |
| 112 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDateText('pay_time')} | |
| 113 | 113 | ORDER BY statDate |
| 114 | 114 | </select> |
| 115 | 115 | |
| 116 | 116 | <select id="selectListByPayTimeBetweenAndGroupByMonth" resultMap="TradeOrderTrendRespVOMap"> |
| 117 | - SELECT CONCAT(EXTRACT(YEAR FROM pay_time), '-', | |
| 118 | - LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0')) AS statDate, | |
| 117 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearDashMonth('pay_time')} AS statDate, | |
| 119 | 118 | COUNT(1) AS order_pay_count, |
| 120 | 119 | SUM(pay_price) AS order_pay_price |
| 121 | 120 | FROM trade_order |
| ... | ... | @@ -125,8 +124,7 @@ |
| 125 | 124 | <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> |
| 126 | 125 | AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} |
| 127 | 126 | </if> |
| 128 | - GROUP BY CONCAT(EXTRACT(YEAR FROM pay_time), '-', | |
| 129 | - LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0')) | |
| 127 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearDashMonth('pay_time')} | |
| 130 | 128 | ORDER BY statDate |
| 131 | 129 | </select> |
| 132 | 130 | ... | ... |
yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/signin/MemberSignInRecordMapper.java
| ... | ... | @@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 6 | 6 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 7 | 7 | import cn.iocoder.yudao.module.member.controller.admin.signin.vo.record.MemberSignInRecordPageReqVO; |
| 8 | 8 | import cn.iocoder.yudao.module.member.dal.dataobject.signin.MemberSignInRecordDO; |
| 9 | -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | |
| 10 | 9 | import org.apache.ibatis.annotations.Mapper; |
| 11 | 10 | |
| 12 | 11 | import java.util.List; |
| ... | ... | @@ -42,10 +41,9 @@ public interface MemberSignInRecordMapper extends BaseMapperX<MemberSignInRecord |
| 42 | 41 | * @return 签到记录列表 |
| 43 | 42 | */ |
| 44 | 43 | default MemberSignInRecordDO selectLastRecordByUserId(Long userId) { |
| 45 | - return selectOne(new QueryWrapper<MemberSignInRecordDO>() | |
| 46 | - .eq("user_id", userId) | |
| 47 | - .orderByDesc("create_time") | |
| 48 | - .last("limit 1")); | |
| 44 | + return selectFirstOne(new LambdaQueryWrapperX<MemberSignInRecordDO>() | |
| 45 | + .eq(MemberSignInRecordDO::getUserId, userId) | |
| 46 | + .orderByDesc(MemberSignInRecordDO::getCreateTime)); | |
| 49 | 47 | } |
| 50 | 48 | |
| 51 | 49 | default Long selectCountByUserId(Long userId) { | ... | ... |
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java
| ... | ... | @@ -133,8 +133,7 @@ public interface MesWmBatchMapper extends BaseMapperX<MesWmBatchDO> { |
| 133 | 133 | |
| 134 | 134 | // 返回 ID 最小的批次 |
| 135 | 135 | query.orderByAsc(MesWmBatchDO::getId); |
| 136 | - query.last("LIMIT 1"); | |
| 137 | - return selectOne(query); | |
| 136 | + return selectFirstOne(query); | |
| 138 | 137 | } |
| 139 | 138 | |
| 140 | 139 | /** | ... | ... |
yudao-module-mes/src/main/resources/mapper/home/MesHomeStatisticsMapper.xml
| ... | ... | @@ -52,7 +52,7 @@ |
| 52 | 52 | |
| 53 | 53 | <!-- 按天聚合报工产量 --> |
| 54 | 54 | <select id="selectDailyFeedbackTrend" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap"> |
| 55 | - SELECT DATE_FORMAT(feedback_time, '%Y-%m-%d') AS statDate, | |
| 55 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('feedback_time')} AS statDate, | |
| 56 | 56 | COALESCE(SUM(feedback_quantity), 0) AS quantity, |
| 57 | 57 | COALESCE(SUM(qualified_quantity), 0) AS qualifiedQuantity, |
| 58 | 58 | COALESCE(SUM(unqualified_quantity), 0) AS unqualifiedQuantity |
| ... | ... | @@ -63,7 +63,7 @@ |
| 63 | 63 | <if test="endTime != null"> |
| 64 | 64 | AND feedback_time < #{endTime} |
| 65 | 65 | </if> |
| 66 | - GROUP BY DATE_FORMAT(feedback_time, '%Y-%m-%d') | |
| 66 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('feedback_time')} | |
| 67 | 67 | ORDER BY statDate |
| 68 | 68 | </select> |
| 69 | 69 | ... | ... |
yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml
| ... | ... | @@ -166,7 +166,8 @@ |
| 166 | 166 | ) t |
| 167 | 167 | <where> |
| 168 | 168 | <if test="sourceDocCode != null and sourceDocCode != ''"> |
| 169 | - AND t.sourceDocCode LIKE CONCAT('%', #{sourceDocCode}, '%') | |
| 169 | + <bind name="sourceDocCodeLike" value="'%' + sourceDocCode + '%'"/> | |
| 170 | + AND t.sourceDocCode LIKE #{sourceDocCodeLike} | |
| 170 | 171 | </if> |
| 171 | 172 | <if test="qcType != null"> |
| 172 | 173 | AND t.qcType = #{qcType} | ... | ... |
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemActivityMapper.java
| ... | ... | @@ -19,11 +19,10 @@ public interface PmsWorkItemActivityMapper extends BaseMapperX<PmsWorkItemActivi |
| 19 | 19 | } |
| 20 | 20 | |
| 21 | 21 | default List<PmsWorkItemActivityDO> selectListByWorkItemIds(Collection<Long> workItemIds, int limit) { |
| 22 | - return selectList(new LambdaQueryWrapperX<PmsWorkItemActivityDO>() | |
| 22 | + return selectListLimitN(new LambdaQueryWrapperX<PmsWorkItemActivityDO>() | |
| 23 | 23 | .in(PmsWorkItemActivityDO::getWorkItemId, workItemIds) |
| 24 | 24 | .orderByDesc(PmsWorkItemActivityDO::getCreateTime) |
| 25 | - .orderByDesc(PmsWorkItemActivityDO::getId) | |
| 26 | - .last("LIMIT " + limit)); | |
| 25 | + .orderByDesc(PmsWorkItemActivityDO::getId), limit); | |
| 27 | 26 | } |
| 28 | 27 | |
| 29 | 28 | default void deleteByWorkItemId(Long workItemId) { | ... | ... |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyMessageMapper.java
| ... | ... | @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.dal.mysql.notify; |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 5 | 5 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 6 | -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; | |
| 7 | 6 | import cn.iocoder.yudao.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; |
| 8 | 7 | import cn.iocoder.yudao.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; |
| 9 | 8 | import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyMessageDO; |
| ... | ... | @@ -53,11 +52,11 @@ public interface NotifyMessageMapper extends BaseMapperX<NotifyMessageDO> { |
| 53 | 52 | } |
| 54 | 53 | |
| 55 | 54 | default List<NotifyMessageDO> selectUnreadListByUserIdAndUserType(Long userId, Integer userType, Integer size) { |
| 56 | - return selectList(new QueryWrapperX<NotifyMessageDO>() // 由于要使用 limitN 语句,所以只能用 QueryWrapperX | |
| 57 | - .eq("user_id", userId) | |
| 58 | - .eq("user_type", userType) | |
| 59 | - .eq("read_status", false) | |
| 60 | - .orderByDesc("id").limitN(size)); | |
| 55 | + return selectListLimitN(new LambdaQueryWrapperX<NotifyMessageDO>() | |
| 56 | + .eq(NotifyMessageDO::getUserId, userId) | |
| 57 | + .eq(NotifyMessageDO::getUserType, userType) | |
| 58 | + .eq(NotifyMessageDO::getReadStatus, false) | |
| 59 | + .orderByDesc(NotifyMessageDO::getId), size); | |
| 61 | 60 | } |
| 62 | 61 | |
| 63 | 62 | default Long selectUnreadCountByUserIdAndUserType(Long userId, Integer userType) { | ... | ... |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java
| ... | ... | @@ -46,7 +46,7 @@ public interface OAuth2AccessTokenMapper extends BaseMapperX<OAuth2AccessTokenDO |
| 46 | 46 | * @param limit 删除条数,防止一次删除太多 |
| 47 | 47 | * @return 删除条数 |
| 48 | 48 | */ |
| 49 | - @Delete("DELETE FROM system_oauth2_access_token WHERE expires_time < #{expiresTime} LIMIT #{limit}") | |
| 49 | + @Delete("${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('system_oauth2_access_token', 'expires_time', 'expiresTime', 'limit')}") | |
| 50 | 50 | Integer deleteByExpiresTimeLt(@Param("expiresTime") LocalDateTime expiresTime, @Param("limit") Integer limit); |
| 51 | 51 | |
| 52 | 52 | } | ... | ... |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java
| ... | ... | @@ -30,6 +30,6 @@ public interface OAuth2RefreshTokenMapper extends BaseMapperX<OAuth2RefreshToken |
| 30 | 30 | * @param limit 删除条数,防止一次删除太多 |
| 31 | 31 | * @return 删除条数 |
| 32 | 32 | */ |
| 33 | - @Delete("DELETE FROM system_oauth2_refresh_token WHERE expires_time < #{expiresTime} LIMIT #{limit}") | |
| 33 | + @Delete("${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('system_oauth2_refresh_token', 'expires_time', 'expiresTime', 'limit')}") | |
| 34 | 34 | Integer deleteByExpiresTimeLt(@Param("expiresTime") LocalDateTime expiresTime, @Param("limit") Integer limit); |
| 35 | 35 | } | ... | ... |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsCodeMapper.java
| 1 | 1 | package cn.iocoder.yudao.module.system.dal.mysql.sms; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 4 | -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; | |
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | |
| 5 | 5 | import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsCodeDO; |
| 6 | 6 | import org.apache.ibatis.annotations.Mapper; |
| 7 | 7 | |
| ... | ... | @@ -17,12 +17,11 @@ public interface SmsCodeMapper extends BaseMapperX<SmsCodeDO> { |
| 17 | 17 | * @return 手机验证码 |
| 18 | 18 | */ |
| 19 | 19 | default SmsCodeDO selectLastByMobile(String mobile, String code, Integer scene) { |
| 20 | - return selectOne(new QueryWrapperX<SmsCodeDO>() | |
| 21 | - .eq("mobile", mobile) | |
| 22 | - .eqIfPresent("scene", scene) | |
| 23 | - .eqIfPresent("code", code) | |
| 24 | - .orderByDesc("id") | |
| 25 | - .limitN(1)); | |
| 20 | + return selectFirstOne(new LambdaQueryWrapperX<SmsCodeDO>() | |
| 21 | + .eq(SmsCodeDO::getMobile, mobile) | |
| 22 | + .eq(scene != null, SmsCodeDO::getScene, scene) | |
| 23 | + .eq(code != null, SmsCodeDO::getCode, code) | |
| 24 | + .orderByDesc(SmsCodeDO::getId)); | |
| 26 | 25 | } |
| 27 | 26 | |
| 28 | 27 | } | ... | ... |
yudao-module-wms/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml
| ... | ... | @@ -54,7 +54,7 @@ |
| 54 | 54 | <select id="selectDailyOrderTrend" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap"> |
| 55 | 55 | SELECT statDate, orderType, SUM(count) AS count |
| 56 | 56 | FROM ( |
| 57 | - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, | |
| 57 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} AS statDate, | |
| 58 | 58 | ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@RECEIPT.type} AS orderType, |
| 59 | 59 | COUNT(*) AS count |
| 60 | 60 | FROM wms_receipt_order |
| ... | ... | @@ -64,9 +64,9 @@ |
| 64 | 64 | <if test="warehouseId != null"> |
| 65 | 65 | AND warehouse_id = #{warehouseId} |
| 66 | 66 | </if> |
| 67 | - GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') | |
| 67 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} | |
| 68 | 68 | UNION ALL |
| 69 | - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, | |
| 69 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} AS statDate, | |
| 70 | 70 | ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@SHIPMENT.type} AS orderType, |
| 71 | 71 | COUNT(*) AS count |
| 72 | 72 | FROM wms_shipment_order |
| ... | ... | @@ -76,9 +76,9 @@ |
| 76 | 76 | <if test="warehouseId != null"> |
| 77 | 77 | AND warehouse_id = #{warehouseId} |
| 78 | 78 | </if> |
| 79 | - GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') | |
| 79 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} | |
| 80 | 80 | UNION ALL |
| 81 | - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, | |
| 81 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} AS statDate, | |
| 82 | 82 | ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@MOVEMENT.type} AS orderType, |
| 83 | 83 | COUNT(*) AS count |
| 84 | 84 | FROM wms_movement_order |
| ... | ... | @@ -88,9 +88,9 @@ |
| 88 | 88 | <if test="warehouseId != null"> |
| 89 | 89 | AND (source_warehouse_id = #{warehouseId} OR target_warehouse_id = #{warehouseId}) |
| 90 | 90 | </if> |
| 91 | - GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') | |
| 91 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} | |
| 92 | 92 | UNION ALL |
| 93 | - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, | |
| 93 | + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} AS statDate, | |
| 94 | 94 | ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@CHECK.type} AS orderType, |
| 95 | 95 | COUNT(*) AS count |
| 96 | 96 | FROM wms_check_order |
| ... | ... | @@ -100,7 +100,7 @@ |
| 100 | 100 | <if test="warehouseId != null"> |
| 101 | 101 | AND warehouse_id = #{warehouseId} |
| 102 | 102 | </if> |
| 103 | - GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') | |
| 103 | + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} | |
| 104 | 104 | ) t |
| 105 | 105 | GROUP BY statDate, orderType |
| 106 | 106 | ORDER BY statDate, orderType |
| ... | ... | @@ -131,7 +131,7 @@ |
| 131 | 131 | </if> |
| 132 | 132 | GROUP BY item.id, item.name |
| 133 | 133 | ORDER BY quantity DESC, item.id ASC |
| 134 | - LIMIT #{limit} | |
| 134 | + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@limitParam('limit', true)} | |
| 135 | 135 | </select> |
| 136 | 136 | |
| 137 | 137 | <!-- 按仓库统计库存数量排行 --> |
| ... | ... | @@ -148,7 +148,7 @@ |
| 148 | 148 | </if> |
| 149 | 149 | GROUP BY warehouse.id, warehouse.name |
| 150 | 150 | ORDER BY quantity DESC, warehouse.id ASC |
| 151 | - LIMIT #{limit} | |
| 151 | + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@limitParam('limit', true)} | |
| 152 | 152 | </select> |
| 153 | 153 | |
| 154 | 154 | </mapper> | ... | ... |