# MyBatis-Plus高阶实战与信创改造指南
> 🎯 从熟练到精通:MyBatis-Plus进阶 + 分库分表 + 信创数据库迁移
> 涵盖:高级查询、性能优化、分库分表实战、MySQL迁移达梦数据库
---
## 📚 目录
1. [MyBatis-Plus高阶使用技巧](#一mybatis-plus高阶使用技巧)
2. [分库分表实战](#二分库分表实战)
3. [分表数据一致性保证](#三分表数据一致性保证)
4. [数据库切换实战](#四数据库切换实战)
5. [信创达梦数据库改造](#五信创达梦数据库改造)
6. [面试常见问题](#六面试常见问题)
---
# 一、MyBatis-Plus高阶使用技巧
## 1.1 链式查询(LambdaQueryWrapper)
**基础用法:**
```java
// ❌ 字符串方式(容易写错,不利于重构)
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "张三").gt("age", 18);
// ✅ Lambda方式(类型安全,支持重构)
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "张三")
.gt(User::getAge, 18);
// ✅ 更简洁的写法
List<User> users = userMapper.selectList(
Wrappers.<User>lambdaQuery()
.eq(User::getName, "张三")
.gt(User::getAge, 18)
);
```
**企业级复杂查询场景:订单查询**
```java
@Service
public class OrderQueryService {
@Autowired
private OrderMapper orderMapper;
/**
* 多条件动态查询
*
* 业务场景:订单列表查询,支持按状态、时间、用户等多维度筛选
*/
public Page<Order> queryOrders(OrderQueryRequest request) {
LambdaQueryWrapper<Order> wrapper = Wrappers.<Order>lambdaQuery()
// 1. 状态(可选)
.eq(request.getStatus() != null, Order::getStatus, request.getStatus())
// 2. 用户ID(可选)
.eq(request.getUserId() != null, Order::getUserId, request.getUserId())
// 3. 订单号模糊查询
.like(StrUtil.isNotBlank(request.getOrderNo()),
Order::getOrderNo, request.getOrderNo())
// 4. 时间范围
.between(request.getStartTime() != null && request.getEndTime() != null,
Order::getCreateTime, request.getStartTime(), request.getEndTime())
// 5. 金额范围
.ge(request.getMinAmount() != null, Order::getTotalAmount, request.getMinAmount())
.le(request.getMaxAmount() != null, Order::getTotalAmount, request.getMaxAmount())
// 6. 排序
.orderByDesc(Order::getCreateTime);
// 分页查询
Page<Order> page = new Page<>(request.getPageNum(), request.getPageSize());
return orderMapper.selectPage(page, wrapper);
}
/**
* IN查询优化
*
* 业务场景:根据多个订单号批量查询
*/
public List<Order> batchQueryByOrderNos(List<String> orderNos) {
if (CollectionUtils.isEmpty(orderNos)) {
return Collections.emptyList();
}
// MyBatis-Plus会自动分批查询(默认1000条一批)
return orderMapper.selectList(
Wrappers.<Order>lambdaQuery()
.in(Order::getOrderNo, orderNos)
);
}
/**
* 复杂嵌套查询
*
* SQL: WHERE (status = 1 OR status = 2) AND (user_id IN (...) AND create_time > ...)
*/
public List<Order> complexQuery(List<Long> userIds, Date startTime) {
return orderMapper.selectList(
Wrappers.<Order>lambdaQuery()
.and(wrapper -> wrapper
.eq(Order::getStatus, 1)
.or()
.eq(Order::getStatus, 2)
)
.and(wrapper -> wrapper
.in(Order::getUserId, userIds)
.gt(Order::getCreateTime, startTime)
)
);
}
}
```
## 1.2 自定义SQL增强
**场景:需要自定义SQL,但想用MyBatis-Plus的分页**
```java
// Mapper接口
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
/**
* 自定义SQL + MyBatis-Plus分页
*
* 注意:
* 1. 第一个参数必须是Page
* 2. 返回类型是IPage
* 3. Wrapper可以动态拼接条件
*/
IPage<OrderVO> selectOrderPage(
Page<OrderVO> page,
@Param(Constants.WRAPPER) Wrapper<Order> wrapper
);
/**
* 关联查询(订单 + 用户信息)
*/
List<OrderDetailVO> selectOrderWithUser(@Param("orderIds") List<Long> orderIds);
}
```
```xml
<!-- OrderMapper.xml -->
<mapper namespace="cn.ufood.fny.mall.mapper.OrderMapper">
<!-- 自定义分页查询 -->
<select id="selectOrderPage" resultType="cn.ufood.fny.mall.vo.OrderVO">
SELECT
o.id,
o.order_no,
o.total_amount,
o.status,
u.name as user_name,
u.phone as user_phone
FROM orders o
LEFT JOIN user u ON o.user_id = u.id
${ew.customSqlSegment} <!-- 动态条件 -->
</select>
<!-- 复杂关联查询 -->
<select id="selectOrderWithUser" resultType="cn.ufood.fny.mall.vo.OrderDetailVO">
SELECT
o.*,
u.name,
u.phone,
GROUP_CONCAT(oi.product_name) as product_names
FROM orders o
LEFT JOIN user u ON o.user_id = u.id
LEFT JOIN order_item oi ON o.id = oi.order_id
WHERE o.id IN
<foreach collection="orderIds" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
GROUP BY o.id
</select>
</mapper>
```
```java
// 使用示例
public IPage<OrderVO> queryOrderPage(OrderQueryRequest request) {
Page<OrderVO> page = new Page<>(request.getPageNum(), request.getPageSize());
// 构建动态条件
LambdaQueryWrapper<Order> wrapper = Wrappers.<Order>lambdaQuery()
.eq(request.getStatus() != null, Order::getStatus, request.getStatus())
.ge(request.getStartTime() != null, Order::getCreateTime, request.getStartTime());
// MyBatis-Plus自动处理分页
return orderMapper.selectOrderPage(page, wrapper);
}
```
## 1.3 批量操作优化
```java
@Service
public class OrderBatchService {
@Autowired
private OrderMapper orderMapper;
/**
* 批量插入(推荐方式1:saveBatch)
*
* 优点:
* 1. 自动分批(默认1000条)
* 2. 自动填充createTime等字段
* 3. 支持主键回填
*/
@Transactional(rollbackFor = Exception.class)
public void batchInsert1(List<Order> orders) {
// MyBatis-Plus自动分批
orderService.saveBatch(orders);
}
/**
* 批量插入(推荐方式2:insertBatchSomeColumn)
*
* 优点:
* 1. 性能更好(单条SQL插入多行)
* 2. 可以指定字段
*
* 需要配置:
*/
@Transactional(rollbackFor = Exception.class)
public void batchInsert2(List<Order> orders) {
// 使用扩展的批量插入
orderService.getBaseMapper().insertBatchSomeColumn(orders);
}
/**
* 批量更新(方式1:updateBatchById)
*/
@Transactional(rollbackFor = Exception.class)
public void batchUpdate1(List<Order> orders) {
orderService.updateBatchById(orders);
}
/**
* 批量更新(方式2:自定义SQL)
*
* 场景:更新指定字段,性能更好
*/
@Transactional(rollbackFor = Exception.class)
public void batchUpdateStatus(List<Long> orderIds, Integer status) {
orderMapper.update(null,
Wrappers.<Order>lambdaUpdate()
.set(Order::getStatus, status)
.in(Order::getId, orderIds)
);
}
/**
* 批量删除(逻辑删除)
*/
@Transactional(rollbackFor = Exception.class)
public void batchDelete(List<Long> ids) {
// 如果配置了逻辑删除,这里是更新is_delete字段
orderService.removeByIds(ids);
}
}
```
**批量插入性能对比:**
```
数据量:10000条
方式1:逐条插入
for (Order order : orders) {
orderMapper.insert(order);
}
耗时:~30秒
方式2:saveBatch(分批1000条)
orderService.saveBatch(orders);
耗时:~5秒
方式3:单SQL插入多行
INSERT INTO orders VALUES (...), (...), (...)
耗时:~1秒
```
## 1.4 自动填充(MetaObjectHandler)
```java
/**
* 自动填充配置
*
* 业务场景:
* - 创建时自动填充createTime、createBy
* - 更新时自动填充updateTime、updateBy
*/
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.debug("开始插入填充...");
// 自动填充创建时间
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
// 自动填充创建人(从ThreadLocal获取当前用户)
Long userId = UserContext.getCurrentUserId();
if (userId != null) {
this.strictInsertFill(metaObject, "createBy", Long.class, userId);
}
// 自动填充更新时间
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateBy", Long.class, userId);
}
@Override
public void updateFill(MetaObject metaObject) {
log.debug("开始更新填充...");
// 自动填充更新时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 自动填充更新人
Long userId = UserContext.getCurrentUserId();
if (userId != null) {
this.strictUpdateFill(metaObject, "updateBy", Long.class, userId);
}
}
}
// 实体类配置
@Data
@TableName("orders")
public class Order {
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
@TableField(fill = FieldFill.INSERT) // 插入时填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT)
private Long createBy;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新都填充
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
}
```
## 1.5 逻辑删除
```java
/**
* 全局逻辑删除配置
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
// application.yml
mybatis-plus:
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除字段
logic-delete-value: 1 # 删除值
logic-not-delete-value: 0 # 未删除值
// 实体类
@Data
public class Order {
@TableLogic // 逻辑删除标识
private Integer isDelete;
}
// 使用
orderService.removeById(1L); // 实际执行:UPDATE orders SET is_delete=1 WHERE id=1
orderService.list(); // 自动过滤:SELECT * FROM orders WHERE is_delete=0
```
## 1.6 多租户(SaaS场景)
```java
/**
* 多租户拦截器
*
* 业务场景:SaaS平台,每个租户只能看到自己的数据
*/
@Configuration
public class TenantConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 多租户插件
TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 从ThreadLocal获取租户ID
Long tenantId = TenantContext.getCurrentTenantId();
return new LongValue(tenantId);
}
@Override
public String getTenantIdColumn() {
return "tenant_id"; // 租户字段名
}
@Override
public boolean ignoreTable(String tableName) {
// 哪些表不需要租户隔离
return Arrays.asList("sys_user", "sys_config").contains(tableName);
}
});
interceptor.addInnerInterceptor(tenantInterceptor);
return interceptor;
}
}
// 效果:
// SELECT * FROM orders
// 自动拼接:SELECT * FROM orders WHERE tenant_id = 1001
// INSERT INTO orders (order_no, amount)
// 自动拼接:INSERT INTO orders (order_no, amount, tenant_id) VALUES (?, ?, 1001)
```
---
# 二、分库分表实战
## 2.1 为什么要分库分表?
**单表数据量到多少需要分表?**
| 数据量 | 查询性能 | 建议 |
|--------|---------|------|
| < 100万 | 正常 | 无需分表,加索引即可 |
| 100万 - 500万 | 开始变慢 | 考虑分表 |
| > 1000万 | 很慢 | 必须分表 |
**业务场景举例:订单表**
```
用户量:1000万
每个用户平均20个订单
订单总量:2亿
如果不分表:
- 单表2亿数据
- 即使有索引,查询也很慢
- 数据备份困难
- 影响整体性能
分表后:
- 按月分表:order_202401, order_202402...
- 每张表约1600万(假设均匀分布)
- 查询只需扫描当月表
- 历史数据可以归档
```
## 2.2 分库分表方案选择
| 方案 | 优点 | 缺点 | 适用场景 |
|------|------|------|---------|
| **ShardingSphere** | 功能全、社区活跃 | 学习成本高 | 复杂场景 |
| **MyCAT** | 独立中间件 | 额外维护成本 | 大型项目 |
| **手动分表** | 灵活、可控 | 开发成本高 | 简单场景 |
## 2.3 ShardingSphere分表实战
### 2.3.1 Maven依赖
```xml
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.3.2</version>
</dependency>
```
### 2.3.2 按月分表配置
```yaml
# application-sharding.yml
spring:
shardingsphere:
datasource:
names: ds0
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/order_db?useSSL=false
username: root
password: root
rules:
sharding:
tables:
# 订单表分片规则
orders:
actual-data-nodes: ds0.orders_$->{202401..202412} # 12张表
table-strategy:
standard:
sharding-column: create_time # 分片字段
sharding-algorithm-name: order-month-algorithm
key-generate-strategy:
column: id
key-generator-name: snowflake
# 分片算法
sharding-algorithms:
order-month-algorithm:
type: CLASS_BASED
props:
strategy: STANDARD
algorithmClassName: cn.ufood.fny.sharding.OrderMonthShardingAlgorithm
# 主键生成策略
key-generators:
snowflake:
type: SNOWFLAKE
props:
sql-show: true # 打印SQL
```
### 2.3.3 自定义分片算法
```java
/**
* 按月分表算法
*
* 分表规则:
* - 2024-01的数据 → orders_202401
* - 2024-02的数据 → orders_202402
*/
public class OrderMonthShardingAlgorithm implements StandardShardingAlgorithm<Date> {
@Override
public String doSharding(Collection<String> availableTables,
PreciseShardingValue<Date> shardingValue) {
Date createTime = shardingValue.getValue();
String suffix = DateUtil.format(createTime, "yyyyMM");
String tableName = "orders_" + suffix;
// 检查表是否存在
if (availableTables.contains(tableName)) {
return tableName;
}
// 表不存在,抛异常或创建表
throw new IllegalArgumentException("表不存在: " + tableName);
}
@Override
public Collection<String> doSharding(Collection<String> availableTables,
RangeShardingValue<Date> shardingValue) {
// 范围查询:查询2024-01到2024-03的数据
Range<Date> range = shardingValue.getValueRange();
Date start = range.lowerEndpoint();
Date end = range.upperEndpoint();
// 计算需要查询哪些表
Set<String> result = new HashSet<>();
LocalDate current = LocalDateTimeUtil.of(start).toLocalDate();
LocalDate endDate = LocalDateTimeUtil.of(end).toLocalDate();
while (!current.isAfter(endDate)) {
String suffix = current.format(DateTimeFormatter.ofPattern("yyyyMM"));
String tableName = "orders_" + suffix;
if (availableTables.contains(tableName)) {
result.add(tableName);
}
current = current.plusMonths(1);
}
return result;
}
}
```
### 2.3.4 使用示例
```java
@Service
public class OrderShardingService {
@Autowired
private OrderMapper orderMapper;
/**
* 插入数据(自动路由到对应月份的表)
*/
public void createOrder(Order order) {
order.setCreateTime(new Date());
// ShardingSphere自动计算:插入到orders_202401
orderMapper.insert(order);
}
/**
* 精确查询(单表)
*/
public Order getOrderByCreateTime(String orderNo, Date createTime) {
// ShardingSphere自动路由到orders_202401
return orderMapper.selectOne(
Wrappers.<Order>lambdaQuery()
.eq(Order::getOrderNo, orderNo)
.eq(Order::getCreateTime, createTime)
);
}
/**
* 范围查询(跨表)
*/
public List<Order> queryOrdersByTimeRange(Date startTime, Date endTime) {
// ShardingSphere自动查询多张表并合并结果
// 实际执行:
// SELECT * FROM orders_202401 WHERE create_time BETWEEN ? AND ?
// UNION ALL
// SELECT * FROM orders_202402 WHERE create_time BETWEEN ? AND ?
// UNION ALL
// SELECT * FROM orders_202403 WHERE create_time BETWEEN ? AND ?
return orderMapper.selectList(
Wrappers.<Order>lambdaQuery()
.between(Order::getCreateTime, startTime, endTime)
);
}
/**
* 分页查询(跨表)
*
* 注意:跨表分页性能较差,建议限制查询范围
*/
public Page<Order> pageQuery(Date startTime, Date endTime, int pageNum, int pageSize) {
Page<Order> page = new Page<>(pageNum, pageSize);
return orderMapper.selectPage(page,
Wrappers.<Order>lambdaQuery()
.between(Order::getCreateTime, startTime, endTime)
.orderByDesc(Order::getCreateTime)
);
}
}
```
### 2.3.5 自动建表
```java
/**
* 自动创建分表
*
* 场景:每月初自动创建下个月的表
*/
@Component
@Slf4j
public class AutoCreateTableTask {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 每月1号凌晨2点创建下个月的表
*/
@Scheduled(cron = "0 0 2 1 * ?")
public void createNextMonthTable() {
LocalDate nextMonth = LocalDate.now().plusMonths(1);
String suffix = nextMonth.format(DateTimeFormatter.ofPattern("yyyyMM"));
String tableName = "orders_" + suffix;
// 检查表是否存在
String checkSql = "SHOW TABLES LIKE '" + tableName + "'";
List<Map<String, Object>> result = jdbcTemplate.queryForList(checkSql);
if (result.isEmpty()) {
// 创建表(复制上个月表结构)
String createSql = "CREATE TABLE " + tableName + " LIKE orders_template";
jdbcTemplate.execute(createSql);
log.info("自动创建分表成功: {}", tableName);
}
}
}
```
## 2.4 按用户ID分表(Hash分表)
```yaml
spring:
shardingsphere:
rules:
sharding:
tables:
orders:
actual-data-nodes: ds0.orders_$->{0..9} # 10张表
table-strategy:
standard:
sharding-column: user_id # 按用户ID分片
sharding-algorithm-name: order-hash-algorithm
sharding-algorithms:
order-hash-algorithm:
type: HASH_MOD # 哈希取模
props:
sharding-count: 10 # 10张表
```
```java
// 使用
public void createOrder(Order order) {
order.setUserId(123456L);
// 自动计算:123456 % 10 = 6
// 插入到:orders_6
orderMapper.insert(order);
}
```
---
# 三、分表数据一致性保证
## 3.1 跨表查询问题
**问题**:订单分表后,如何按订单号查询?
```java
/**
* 方案1:订单号中包含路由信息
*/
public String generateOrderNo(Long userId, Date createTime) {
// 订单号格式:yyyyMMdd + userId后4位 + 随机数
String datePart = DateUtil.format(createTime, "yyyyMMdd");
String userPart = String.format("%04d", userId % 10000);
String randomPart = RandomUtil.randomNumbers(6);
return datePart + userPart + randomPart;
// 例如:202401010123456789
// 2024-01-01 + 用户0123 + 456789
}
// 查询时可以解析订单号,确定查询哪张表
public Order getOrderByNo(String orderNo) {
// 解析日期 → 确定月份表
String dateStr = orderNo.substring(0, 8);
String tableSuffix = orderNo.substring(0, 6); // 202401
// 查询指定表
return orderMapper.selectOne(
Wrappers.<Order>lambdaQuery()
.eq(Order::getOrderNo, orderNo)
.last("FROM orders_" + tableSuffix) // 指定表名
);
}
/**
* 方案2:建立路由表
*/
CREATE TABLE order_route (
order_no VARCHAR(32) PRIMARY KEY,
table_name VARCHAR(32),
create_time DATETIME,
INDEX idx_create_time (create_time)
);
// 插入订单时,同时插入路由表
@Transactional
public void createOrder(Order order) {
// 1. 插入订单
orderMapper.insert(order);
// 2. 插入路由信息
OrderRoute route = new OrderRoute();
route.setOrderNo(order.getOrderNo());
route.setTableName("orders_202401");
route.setCreateTime(order.getCreateTime());
orderRouteMapper.insert(route);
}
// 查询时先查路由表
public Order getOrderByNo(String orderNo) {
// 1. 查路由表
OrderRoute route = orderRouteMapper.selectById(orderNo);
// 2. 查具体表
return orderMapper.selectOne(
Wrappers.<Order>lambdaQuery()
.eq(Order::getOrderNo, orderNo)
.last("FROM " + route.getTableName())
);
}
```
## 3.2 分布式事务
```java
/**
* Seata分布式事务
*
* 场景:下单时需要操作多张分表
*/
@Service
public class OrderDistributedTransactionService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderItemMapper orderItemMapper;
/**
* 下单:订单主表 + 订单明细表
*
* 订单主表:按月分表
* 订单明细表:按订单ID Hash分表
*/
@GlobalTransactional // Seata全局事务
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order, List<OrderItem> items) {
// 1. 插入主订单(路由到orders_202401)
orderMapper.insert(order);
// 2. 插入订单明细(可能分散到多张表)
for (OrderItem item : items) {
item.setOrderId(order.getId());
orderItemMapper.insert(item);
}
// 如果任何一步失败,Seata保证所有表都回滚
}
}
```
## 3.3 数据迁移与归档
```java
/**
* 历史数据归档
*
* 场景:6个月前的订单归档到历史库
*/
@Component
@Slf4j
public class OrderArchiveTask {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private JdbcTemplate archiveJdbcTemplate; // 归档库
/**
* 每月归档6个月前的数据
*/
@Scheduled(cron = "0 0 3 1 * ?")
public void archiveOldOrders() {
LocalDate sixMonthsAgo = LocalDate.now().minusMonths(6);
String tableSuffix = sixMonthsAgo.format(DateTimeFormatter.ofPattern("yyyyMM"));
String sourceTable = "orders_" + tableSuffix;
String targetTable = "orders_archive_" + tableSuffix;
try {
// 1. 复制数据到归档库
String copySql = String.format(
"INSERT INTO archive_db.%s SELECT * FROM order_db.%s",
targetTable, sourceTable
);
archiveJdbcTemplate.execute(copySql);
// 2. 验证数据
Long sourceCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM " + sourceTable, Long.class);
Long targetCount = archiveJdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM " + targetTable, Long.class);
if (!sourceCount.equals(targetCount)) {
throw new RuntimeException("归档数据不一致");
}
// 3. 删除原表数据
jdbcTemplate.execute("DROP TABLE " + sourceTable);
log.info("订单归档成功: {} -> {}, 数量: {}",
sourceTable, targetTable, sourceCount);
} catch (Exception e) {
log.error("订单归档失败", e);
// 发送告警
}
}
}
```
---
# 四、数据库切换实战
## 4.1 MySQL切换PostgreSQL
**差异点梳理:**
| 差异项 | MySQL | PostgreSQL |
|--------|-------|-----------|
| **自增主键** | AUTO_INCREMENT | SERIAL / SEQUENCE |
| **字符串拼接** | CONCAT() | \|\| |
| **分页** | LIMIT offset, size | LIMIT size OFFSET offset |
| **日期函数** | NOW(), DATE_FORMAT() | NOW(), TO_CHAR() |
| **反引号** | \`table\` | "table" |
| **IF函数** | IF(condition, a, b) | CASE WHEN condition THEN a ELSE b END |
**改造步骤:**
```java
// 1. 抽象SQL方言接口
public interface SqlDialect {
String getPageSql(String sql, int offset, int size);
String getDateFormatFunction(String column, String format);
String getIfFunction(String condition, String trueValue, String falseValue);
}
// 2. MySQL实现
@Component("mysqlDialect")
public class MySqlDialect implements SqlDialect {
@Override
public String getPageSql(String sql, int offset, int size) {
return sql + " LIMIT " + offset + ", " + size;
}
@Override
public String getDateFormatFunction(String column, String format) {
return "DATE_FORMAT(" + column + ", '" + format + "')";
}
}
// 3. PostgreSQL实现
@Component("postgresDialect")
public class PostgresDialect implements SqlDialect {
@Override
public String getPageSql(String sql, int offset, int size) {
return sql + " LIMIT " + size + " OFFSET " + offset;
}
@Override
public String getDateFormatFunction(String column, String format) {
return "TO_CHAR(" + column + ", '" + format + "')";
}
}
```
**XML改造:**
```xml
<!-- MySQL版本 -->
<select id="selectPage" resultType="Order" databaseId="mysql">
SELECT * FROM orders
WHERE create_time > #{startTime}
LIMIT #{offset}, #{size}
</select>
<!-- PostgreSQL版本 -->
<select id="selectPage" resultType="Order" databaseId="postgres">
SELECT * FROM orders
WHERE create_time > #{startTime}
LIMIT #{size} OFFSET #{offset}
</select>
```
---
# 五、信创达梦数据库改造
## 5.1 信创改造背景
**什么是信创?**
- 信创 = 信息技术应用创新
- 国产化替代:操作系统、数据库、中间件、芯片等
- 常见信创数据库:达梦(DM)、人大金仓(KingBase)、神通(Oscar)
**为什么要改造?**
- 政策要求:政府、国企、金融等行业必须使用国产数据库
- 数据安全:核心数据不依赖国外技术
## 5.2 MySQL迁移到达梦数据库
### 5.2.1 达梦数据库特点
| 对比项 | MySQL | 达梦DM8 |
|--------|-------|---------|
| **兼容性** | - | 兼容Oracle、MySQL语法 |
| **驱动** | com.mysql.cj.jdbc.Driver | dm.jdbc.driver.DmDriver |
| **URL** | jdbc:mysql://host:3306/db | jdbc:dm://host:5236/db |
| **默认端口** | 3306 | 5236 |
| **字符串拼接** | CONCAT() | \|\| 或 CONCAT() |
| **分页** | LIMIT | LIMIT 或 ROWNUM |
### 5.2.2 项目改造步骤
**Step 1:添加达梦驱动**
```xml
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
<version>8.1.2.192</version>
</dependency>
```
**Step 2:配置文件改造**
```yaml
# application-dm.yml(达梦环境配置)
spring:
datasource:
driver-class-name: dm.jdbc.driver.DmDriver
url: jdbc:dm://192.168.1.100:5236/SYSDBA
username: SYSDBA
password: SYSDBA001
hikari:
maximum-pool-size: 20
minimum-idle: 5
mybatis-plus:
configuration:
# 达梦数据库标识
database-id: dm
global-config:
db-config:
# 达梦支持的主键策略
id-type: auto
# 表名大写(达梦默认大写)
table-prefix: ""
# 字段映射策略
capital-mode: true
```
**Step 3:MyBatis-Plus配置**
```java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件(指定达梦数据库)
PaginationInnerInterceptor paginationInterceptor =
new PaginationInnerInterceptor(DbType.DM);
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
/**
* 数据库类型识别
*/
@Bean
public DatabaseIdProvider databaseIdProvider() {
DatabaseIdProvider provider = new VendorDatabaseIdProvider();
Properties properties = new Properties();
properties.setProperty("MySQL", "mysql");
properties.setProperty("DM DBMS", "dm"); // 达梦标识
properties.setProperty("Oracle", "oracle");
provider.setProperties(properties);
return provider;
}
}
```
### 5.2.3 SQL语句改造
**问题1:表名和字段名大小写**
```sql
-- MySQL:不区分大小写
SELECT * FROM orders WHERE order_no = 'xxx';
-- 达梦:默认大写,需要统一
SELECT * FROM ORDERS WHERE ORDER_NO = 'xxx';
-- 或者使用引号保持原样
SELECT * FROM "orders" WHERE "order_no" = 'xxx';
```
**解决方案:**
```java
// 方式1:全局配置大小写(推荐)
mybatis-plus:
global-config:
db-config:
# 表名不转大写
table-underline: true
# 字段名不转大写
column-underline: true
// 方式2:注解指定
@TableName(value = "orders", resultMap = "BaseResultMap")
public class Order {
@TableField("order_no")
private String orderNo;
}
```
**问题2:分页语法差异**
```xml
<!-- MySQL版本 -->
<select id="selectPage" resultType="Order" databaseId="mysql">
SELECT * FROM orders
LIMIT #{offset}, #{size}
</select>
<!-- 达梦版本(兼容MySQL语法)-->
<select id="selectPage" resultType="Order" databaseId="dm">
SELECT * FROM orders
LIMIT #{offset}, #{size}
</select>
<!-- 达梦原生分页(使用ROWNUM,性能更好)-->
<select id="selectPageNative" resultType="Order" databaseId="dm">
SELECT * FROM (
SELECT A.*, ROWNUM RN FROM (
SELECT * FROM orders ORDER BY create_time DESC
) A WHERE ROWNUM <= #{endRow}
) WHERE RN > #{startRow}
</select>
```
**问题3:函数差异**
```xml
<!-- 通用写法(多数据库兼容)-->
<select id="selectByDateRange">
SELECT * FROM orders
WHERE 1=1
<if test="startTime != null">
<choose>
<when test="_databaseId == 'mysql'">
AND DATE_FORMAT(create_time, '%Y-%m-%d') >= #{startTime}
</when>
<when test="_databaseId == 'dm'">
AND TO_CHAR(create_time, 'YYYY-MM-DD') >= #{startTime}
</when>
</choose>
</if>
</select>
```
### 5.2.4 数据迁移工具
```java
/**
* 数据迁移工具
*
* 场景:MySQL数据迁移到达梦
*/
@Component
@Slf4j
public class DataMigrationService {
@Autowired
@Qualifier("mysqlJdbcTemplate")
private JdbcTemplate mysqlJdbcTemplate;
@Autowired
@Qualifier("dmJdbcTemplate")
private JdbcTemplate dmJdbcTemplate;
/**
* 迁移订单表数据
*/
public void migrateOrders() {
String selectSql = "SELECT * FROM orders";
String insertSql = "INSERT INTO ORDERS (ID, ORDER_NO, USER_ID, TOTAL_AMOUNT, STATUS, CREATE_TIME) " +
"VALUES (?, ?, ?, ?, ?, ?)";
// 分批迁移
int batchSize = 1000;
int offset = 0;
int totalCount = 0;
while (true) {
// 查询MySQL数据
List<Map<String, Object>> rows = mysqlJdbcTemplate.queryForList(
selectSql + " LIMIT " + offset + ", " + batchSize
);
if (rows.isEmpty()) {
break;
}
// 批量插入达梦
dmJdbcTemplate.batchUpdate(insertSql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Map<String, Object> row = rows.get(i);
ps.setLong(1, (Long) row.get("id"));
ps.setString(2, (String) row.get("order_no"));
ps.setLong(3, (Long) row.get("user_id"));
ps.setBigDecimal(4, (BigDecimal) row.get("total_amount"));
ps.setInt(5, (Integer) row.get("status"));
ps.setTimestamp(6, (Timestamp) row.get("create_time"));
}
@Override
public int getBatchSize() {
return rows.size();
}
});
totalCount += rows.size();
offset += batchSize;
log.info("已迁移 {} 条数据", totalCount);
}
log.info("迁移完成,共 {} 条数据", totalCount);
}
/**
* 数据校验
*/
public boolean validateData(String tableName) {
// 1. 校验数量
Long mysqlCount = mysqlJdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM " + tableName, Long.class);
Long dmCount = dmJdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM " + tableName.toUpperCase(), Long.class);
if (!mysqlCount.equals(dmCount)) {
log.error("数据量不一致: MySQL={}, DM={}", mysqlCount, dmCount);
return false;
}
// 2. 抽样校验数据一致性
List<Map<String, Object>> mysqlSample = mysqlJdbcTemplate.queryForList(
"SELECT * FROM " + tableName + " ORDER BY id LIMIT 100");
List<Map<String, Object>> dmSample = dmJdbcTemplate.queryForList(
"SELECT * FROM " + tableName.toUpperCase() + " ORDER BY ID FETCH FIRST 100 ROWS ONLY");
// 比对数据...
return true;
}
}
```
### 5.2.5 兼容性配置
```java
/**
* 多数据库兼容配置
*
* 支持一套代码,同时兼容MySQL和达梦
*/
@Configuration
public class MultiDatabaseConfig {
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Bean
public DbType getDbType() {
if (driverClassName.contains("mysql")) {
return DbType.MYSQL;
} else if (driverClassName.contains("dm")) {
return DbType.DM;
} else if (driverClassName.contains("oracle")) {
return DbType.ORACLE;
}
throw new RuntimeException("不支持的数据库类型");
}
@Bean
public SqlDialect sqlDialect() {
DbType dbType = getDbType();
switch (dbType) {
case MYSQL:
return new MySqlDialect();
case DM:
return new DmDialect();
default:
throw new RuntimeException("不支持的数据库类型");
}
}
}
/**
* 达梦SQL方言
*/
public class DmDialect implements SqlDialect {
@Override
public String getLimitSql(String sql, int offset, int size) {
// 达梦8支持MySQL的LIMIT语法
return sql + " LIMIT " + offset + ", " + size;
}
@Override
public String getDateFormatSql(String column, String format) {
// 达梦使用TO_CHAR
return "TO_CHAR(" + column + ", '" + convertFormat(format) + "')";
}
private String convertFormat(String mysqlFormat) {
// MySQL: %Y-%m-%d → 达梦: YYYY-MM-DD
return mysqlFormat
.replace("%Y", "YYYY")
.replace("%m", "MM")
.replace("%d", "DD")
.replace("%H", "HH24")
.replace("%i", "MI")
.replace("%s", "SS");
}
}
```
## 5.3 信创改造总结
**改造清单:**
```
✅ 驱动更换:MySQL → 达梦驱动
✅ URL修改:jdbc:mysql → jdbc:dm
✅ 表名大小写:统一处理
✅ SQL语法:适配达梦语法
✅ 分页逻辑:LIMIT → ROWNUM
✅ 函数替换:DATE_FORMAT → TO_CHAR
✅ 数据迁移:MySQL数据 → 达梦数据
✅ 测试验证:功能测试、性能测试
```
**注意事项:**
1. **提前测试**:在测试环境充分测试
2. **灰度发布**:先部分业务,再全量
3. **数据校验**:迁移后务必校验数据一致性
4. **性能对比**:对比MySQL和达梦的性能差异
5. **回滚方案**:准备快速回滚到MySQL的方案
---
# 六、面试常见问题
## Q1:MyBatis-Plus的分页查询原理?
**答**:
MyBatis-Plus通过拦截器实现分页:
1. **PaginationInnerInterceptor** 拦截SQL
2. 解析SQL,获取总记录数(COUNT查询)
3. 改写SQL,添加分页语句(LIMIT)
4. 执行分页查询
5. 封装结果到Page对象
```java
// 原始SQL
SELECT * FROM orders WHERE status = 1
// 自动改写为
// 1. 查总数
SELECT COUNT(*) FROM orders WHERE status = 1
// 2. 分页查询
SELECT * FROM orders WHERE status = 1 LIMIT 0, 10
```
---
## Q2:分库分表后如何保证数据一致性?
**答**:
1. **单表事务**:操作同一张表,本地事务即可
2. **跨表事务**:使用分布式事务(Seata)
3. **最终一致性**:
- 本地消息表
- MQ保证
- 定时补偿
4. **避免跨库JOIN**:
- 数据冗余
- 应用层聚合
- 宽表设计
---
## Q3:信创改造遇到的最大困难是什么?
**答**:
**最大困难:SQL兼容性**
我们项目有上千条SQL,改造时遇到:
1. **函数差异**:DATE_FORMAT、IF、CONCAT等
2. **分页差异**:LIMIT → ROWNUM
3. **大小写敏感**:达梦默认大写
**解决方案:**
1. 抽象SQL方言接口
2. XML中用databaseId区分
3. 编写自动化测试,对比MySQL和达梦结果
4. 建立SQL兼容性文档
---
## 📝 面试速记卡
```
【MyBatis-Plus高阶】
LambdaQueryWrapper - 类型安全,支持重构
自定义SQL + MP分页 - IPage<VO> + ${ew.customSqlSegment}
批量操作 - saveBatch(1000条一批)
自动填充 - MetaObjectHandler(createTime/updateTime)
逻辑删除 - @TableLogic
多租户 - TenantLineInnerInterceptor
【分库分表】
时机:单表 > 1000万
方案:ShardingSphere(推荐)、MyCAT
策略:按月分表、按用户Hash
一致性:Seata、本地消息表
【信创改造】
MySQL → 达梦DM8
驱动:dm.jdbc.driver.DmDriver
URL:jdbc:dm://host:5236/db
难点:SQL兼容性、大小写敏感
方案:databaseId、SQL方言、自动化测试
```
---
**文档版本**:v1.0
**创建时间**:2026-01-19
**适用场景**:MyBatis-Plus进阶、分库分表、信创改造面试