前言
经过升级方案的总结分析,我们可以发现,升级方案虽然相较于初步方案有了巨大提升,但是有优化空间:需要修改常量来实现注入不同的mapper实现类,需要添加好多空mapper接口。本文就彻底解决这些问题,并形成最终方案。
一、思路分析
1、service层升级思路分析
- 升级方案要求service层注入dao层接口实现时,必须要指定注入bean的名称
- 为什么要指定注入bean的名称呢,因为@Autowire注入机制决定,要么一个实现接口,要么bean名称和变量名称保持一致
- 咱们先看bean名称和变量名保持一致的方式
- 不改动service层代码的情况下,dao层接口变量名称已经和父接口保持了一致,如果想让子接口和service层中的dao层接口变量名称保持一致,就得改父dao层接口的类名。但是多个子dao层接口都用父dao层接口的名称这不冲突了吗,所以也行不通(实际上是行得通的,理解了后面的方案,可以自己试下,后面的方案更好,我就不介绍这种发方式了)。
- 然后咱们在回过头看第一种情况:一个实现接口
- 一个实现接口猛地一看不可行:父接口和多种数据库的子接口怎么可能只有一个实现???
- 首先:多个子接口除了名称其他内容完全一致,能不能只要一个呢?答案是可以,因为dao层接口和mapper.xml映射关系是mapper.xml维护的,那就可以做到多个mapper.xml映射到一个dao层接口上,问题是dao层接口调用实现时调用哪个mapper.xml呢???
- 肯定是调用对应数据库的实现,那其他数据库的实现呢?其他数据库实现有地方用到吗?没有,没有那项目启动时候不加载就行了!!!每种数据库的mapper.xml放到同一个文件夹下,启动时候只加载需要的文件夹中的mapper.xml!!!
- 那确实解决了每次项目运行时有多个子实现的问题,那还有父接口和子接口两个实现类呢?
- 父接口有地方用吗???不是service层用了吗!按刚才的设想,service层用的是子接口,自然就没有地方用父接口了!!!
- 那是什么意思???我的意思是只在Spring容器中注册子接口,不注册父接口!!!所有数据库只要一个子接口,再把子接口和父接口放在不同的目录下,在Spring容器中只注入子接口,子接口继承了父接口,所以可以直接调用父接口的实现,保证不直接使用父接口。兄弟,你这野路子给谁学的???不管是野路子还是正路子,只要能解决问题就是好路子。
2、dao层升级思路分析
- 按照上面的分析,dao层只保留一个dao层接口并继承父接口
- 每种数据库对应一个mapper.xml,所有mapper.xml都映射到同一个子mapper接口,mapper.xml只重写与父mapper.xml语法不一致的SQL
二、项目改造
1、项目改造
- 如果自己有项目,可以在自己项目上测试,如果没有或者不想在自己项目测试,可以使用同一套代码适配多种数据库之升级方案的示例代码。如果使用示例项目,先根据README.md正确运行示例项目。
- 修改service层代码:回复原状,不在创建多个dao层接口对象或者指定注入bean名称
package com.yu.service.impl;
import com.yu.dao.UserMapper;
import com.yu.entity.User;
import com.yu.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public List<User> list(User user) {
return userMapper.list(user);
}
}
- 修改dao层代码:只保留一个子接口继承父接口,并与父接口在不同包
package com.yu.dao.ext;
import com.yu.dao.UserMapper;
public interface UserExtMapper extends UserMapper {
}
- 修改Mapper.xml:所有子mapper.xml都与dao层的子接口映射,子mapper.xml中实现与父mapper.xml语法不一致的SQL,每种数据库的子mapper.xml放在同一个目录下
mysql/UserMysqlMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--映射的子mapper接口-->
<mapper namespace="com.yu.dao.UserExtMapper">
<!--继承方式获取表与entity映射关系,方便本mapper.xml使用-->
<resultMap type="User" id="BaseResultMap" extends="com.yu.dao.UserMapper.BaseResultMap"/>
<sql id="Base_Column_List">
<!--使用include方式包含父mapper.xml中的sql,方便本mapper.xml使用-->
<include refid="com.yu.dao.UserMapper.Base_Column_List"/>
</sql>
</mapper>
pg/UserPgMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--映射的子mapper接口-->
<mapper namespace="com.yu.dao.ext.UserExtMapper">
<!--继承方式获取表与entity映射关系,方便本mapper.xml使用-->
<resultMap type="User" id="BaseResultMap" extends="com.yu.dao.UserMapper.BaseResultMap"/>
<sql id="Base_Column_List">
<!--也可以使用include方式包含父mapper.xml中的sql,方便本mapper.xml使用,因为SQL不通用,所以这里不能直接包含-->
<!--<include refid="com.yu.dao.UserMapper.Base_Column_List"/>-->
select
id,
name
<!--TODO 特别注意,系统也有user表,所以这里需要加上模式-->
from multi_database.user
</sql>
<select id="list" parameterType="User" resultMap="BaseResultMap">
<include refid="Base_Column_List"/>
<where>
<if test="name != null and name != ''">
and name like concat('%', #{name}, '%')
</if>
</where>
</select>
</mapper>
- 只扫描子dao层接口
package com.yu;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
// TODO 只扫描子dao层接口
@MapperScan(basePackages = "com.yu.**.dao.ext")
public class MultiDatabaseApplication {
public static void main(String[] args) {
SpringApplication.run(MultiDatabaseApplication.class, args);
}
}
- 只加载父mapper.xml和当前数据库的子mapper.xml;也可以通过手动注册sqlSessionFactory对象,这里只配置父mapper.xml,注册时在根据驱动类型把对应的子mapper.xml加进去
database:
# mysql: classpath*:mapper/mysql/*Mapper.xml ;postgres: classpath*:mapper/pg/*Mapper.xml
son-mapper: classpath*:mapper/pg/*Mapper.xml
# MyBatis配置
mybatis:
# 配置mapper的扫描,父mapper.xml和当前子mapper.xml TODO 需要加载父mapper.xml,使用**时不要把所有的子mapper.xml加载进来
mapperLocations: classpath*:mapper/*Mapper.xml,${database.son-mapper}
- 测试MySQL方式
- 修改为PostgreSQL数据库
# 数据库配置 TODO 根据实际情况修改
database:
ip: xxx
# mysql: 3306 ;postgres: 5432
port: 5432
database: demo
# postgres需要
schema: multi_database
username: xxx
password: xxx
# TODO 数据库对应的mapper.xml路径
# mysql: classpath*:mapper/mysql/*Mapper.xml ;postgres: classpath*:mapper/pg/*Mapper.xml
son-mapper: classpath*:mapper/pg/*Mapper.xml
# mysql: com.mysql.cj.jdbc.Driver ;postgres: org.postgresql.Driver
driver-class-name: org.postgresql.Driver
# mysql: jdbc:mysql://${database.ip}:${database.port}/${database.database}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
# postgres: jdbc:postgresql://${database.ip}:${database.port}/${database.database}?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&¤tSchema=${database.schema},sys_catalog
url: jdbc:postgresql://${database.ip}:${database.port}/${database.database}?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&¤tSchema=${database.schema},sys_catalog
# 数据源配置
spring:
datasource:
# 数据源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: ${database.driver-class-name}
url: ${database.url}
username: ${database.username}
password: ${database.password}
# MyBatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.yu.**.entity
# 配置mapper的扫描,父mapper.xml和当前子mapper.xml TODO 需要加载父mapper.xml,使用**时不要把所有的子mapper.xml加载进来
mapperLocations: classpath*:mapper/*Mapper.xml,${database.son-mapper}
# 加载全局的配置文件
configLocation: classpath:mybatis/mybatis-config.xml
- 测试PostgreSQL方式
三、方案总结
总结下本方案添加一种新数据库时需要哪些操作:
- 添加新数据库驱动包,无可厚非,没问题
- 添加新数据库配置,无可厚非,没问题
- 修改数据库对应的mapper.xml路径,无可厚非,没问题(也可以通过手动注册sqlSessionFactory的方式,通过判断驱动类型自动添加)
- service层无需改动
- dao层无需改动,添加新模块时需要同时添加父接口和子接口
- 添加mapper.xml,并实现与父mapper.xml中不一致的增删改查语句,不管与父mapper.xml SQL是否完全一致,都推荐创建子mapper.xml;各个子mapper.xml之间相互独立。
总结下本方案切换新数据库时需要哪些操作:
- 修改数据库配置
- 修改数据库对应的mapper.xml路径(通过手动注册sqlSessionFactory的方式时,不需要这一步)
- 没了!!!本套方案切换数据库时,只改动数据库配置即可
总结下本方案添加新功能时需要哪些操作:
- 添加父mapper.xml
- 添加所有数据库对应的mapper.xml(实际上可以先不用添加,直接在每种数据库环境下测试,当父mapper.xml不适用时再添加子mapper.xml,而子mapper.xml中只添加父mapper.xml中不适用的SQL)
- 每种数据库环境环境下测试新功能
总结下本方案功能添加新操作时需要哪些操作:
- 在父mapper.xml中添加一种默认实现
- 每种数据库环境环境下测试功能的新操作
- 当父mapper.xml不适用时再在子mapper.xml添加SQL
代码改动量很少,每种数据库的实现也没有耦合在一个mapper文件中,很理想,后续会整理数据库数据迁移和项目适配的经验。
四、注意
- 示例中的 TODO 是需要注意的地方,如果运行项目报错可以着重看下 TODO 部分
- dao层接口中父接口和子接口在不同包下,配置dao层包扫描时只配置子接口路径(@MapperScan(basePackages = “com.yu.**.dao.ext”))
- 配置mapper.xml的扫描时,如果使用多层路径通配符**,会导致所有的mapper.xml全部加载,dao层调用实现时报错
- 如果不使用多层路径通配符**,就要保证多模块项目中的mapper.xml路径深度一致,业务层路径使用单层路径通配符*代替
- 同一套代码适配多种数据库之最终方案的示例代码