JDBC编码的缺点
- 结果集解析复杂,列名硬编码,sql变化导致解析代码变化,系统不易维护。
- Sql语句硬编码,难以维护。 数据库配置硬编码。
- 频繁连接、释放数据库资源,降低系统性能。
- preparedStatement向占位符号传参数存在硬编码,不易维护。
MyBatis的核心原理、思想
MyBatis的出现,使得我们不用在代码中进行SQL语句硬编码,代码更加容易维护,更好的管理数据库资源,提升性能。
手写一个简单的ORM框架,加深理解
用例分析
框架写出来是给开发人员用的,使用过程无非 导包、配置、按规则编写代码、运行。
功能分析
设计一个最小可用的仿MyBatis的ORM框架,那首先我们需要拿到数据源的配置信息、Mapper配置的信息,然后在调用Mapper方法的时候,解析方法参数,填充到Mapper方法对应的SQL中去,然后执行QL,拿到结果之后,按照配置解析并映射到对应的Java对象。
功能大概:
代码分析、设计
1.格式约定
首先,我们要约定数据源、Mapper配置信息的格式。
- 存储数据源信息、Mapper 配置信息的格式
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!-- 数据源信息 -->
<dataSource>
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/ruoyi?characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
<!-- Mapper XML文件配置信息 -->
<mappers>
<mapper resource="SysRoleMapper.xml"/>
</mappers>
</configuration>
- 具体的Mapper XML文件的格式
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.hero.dao.SysRoleMapper">
<!-- id:方法名称; resultType: 查询结果返回值-->
<select id="findAll" resultType="com.hero.dao.SysRole">
select * from sys_role;
</select>
</mapper>
2.数据对象
然后,我们需要一个存储数据源信息、Mapper信息、SQL信息的载体,简单来说 XML -> Java对象。
- Configuration类存储了数据源信息、Mapper XML解析之后的具体方法信息。其中,
sqlSourceMap
对象的key存储Mapper XML中方法的全路径,比如:com.hero.dao.SysRoleMapper.findAll
,而value存储SqlSource对象,此对象有 具体sql 以及 查询的结果类型。
- SqlSource类存储了Mapper XML 中方法对应对应的sql,以及方法结果类型。
从json格式的角度来看Configuration对象的数据的格式,这样更好理解一些:
{
"driver": "xxx",
"url": "xxx",
"sqlSourceMap": {
"com.hero.dao.SysRoleMapper.findAll": {
"sql": "select * from sys_role",
"reultType": ""
}
},
"password": "xxx",
"username": "xxxx"
}
3.数据源加载、装配Mapper、解析SQL
有了载体之后,我们要实现 加载数据源配置 、 **装配Mapper XML功能、解析方法对应的SQL **,在这里,由SqlSessionFactoryBuilder
类负载从xml文件中加载数据源信息 并 装配Mapper信息,统一写入Configuration
对象。
4.通用SQL方法接口与执行器
有了上述信息,我们就可以创建执行SQL语句的规范接口SqlSession
,此接口提供了通用的方法去处理业务的CRUD语句,默认实现类是DefaultSqlSessionImpl
,接口方法传入 Mapper XML方法 全路径,实现类将全路径传递给Executor
类,Executor
会获取到具体的SQL,进行参数填充,并发送SQL到数据库,然后拿到执行结果,进行结果映射,最后返回给实现类。
在这里,我们不直接创建SqlSession实现类,而是通过SqlSessionFactory
工厂类创建。
- SqlSession接口。除了通用CURD方法(select\update\delete\insert),getMapper 也是一个重要的方法,可以获取Mapper接口的代理实例,通过Mapper接口即可执行SQL,不用直接使用SqlSession接口,例如
List<SysRole> list = sysRoleMapper.findAll()
- Executor类负责发送SQL到数据库,处理SQL的参数填充(绑定)、结果集的映射。
- SqlSessionFactory通过
openeSession()
方法创建SqlSession对象
小结
通过SqlSessionFactoryBuilder
类,我们将XML信息写入到Configuration
对象,并构建一个SqlSessionFactory
会话工厂,通过工厂类可以获取SqlSession
接口具体实现类实例DefaultSqlSessionImpl
,SqlSession
接口提供了通用的CURD方法,而实现类依赖Executor
执行器,实现参数填充、SQL发送、结果映射功能。
同时,我们提到了SqlSession的getMapper方法,在后面代码中会看到。
代码实现
Configuration
public class Configuration {
private String driver;
private String url;
private String username;
private String password;
private Map<String, SqlSource> sqlSourceMap = new HashMap<>();
// getting/setting
}
SqlSource
public class SqlSource {
//存储了Sql语句
private String sql;
//存储封装JavaBean对象类的全限定名称
private String resultType;
// getting/setting
}
SqlSessionFactoryBuilder
public class SqlSessionFactoryBuilder {
/**
* 构建工厂对象
* 参数:SqlMapConfig.xml配置文件的输入流对象
*/
public SqlSessionFactory build(InputStream inputStream) throws
DocumentException {
Configuration configuration = new Configuration();
//解析配置文件
loadXmlConfig(configuration, inputStream);
return new SqlSessionFactory(configuration);
}
/**
* 解析框架使用者传入的配置文件
*/
private void loadXmlConfig(Configuration configuration, InputStream
inputStream) throws DocumentException {
//创建解析XML文件对象SAXReader
SAXReader saxReader = new SAXReader();
//读取SqlMapConfig.xml配置文件流资源,获取文档对象
Document document = saxReader.read(inputStream);
//获取SqlMapConfig.xml 配置文件内所有property标签元素
List<Element> selectNodes = document.selectNodes("//property");
//循环解析property标签内容,抽取配置信息
for (Element element : selectNodes) {
String name = element.attributeValue("name");
if ("driver".equals(name)) {//数据库驱动
configuration.setDriver(element.attributeValue("value"));
} else if ("url".equals(name)) {//数据库地址
configuration.setUrl(element.attributeValue("value"));
} else if ("username".equals(name)) {//用户名
configuration.setUsername(element.attributeValue("value"));
} else if ("password".equals(name)) {//密码
configuration.setPassword(element.attributeValue("value"));
}
}
//解析SqlMapConfig.xml 映射器配置信息
List<Element> list = document.selectNodes("//mapper");
for (Element element : list) {
//SQL映射配置文件路径
String resource = element.attributeValue("resource");
//解析SQL映射配置文件
loadSqlConfig(resource, configuration);
}
}
/**
* 解析SQL配置文件
*/
private void loadSqlConfig(String resource, Configuration configuration)
throws DocumentException {
//根据SQL映射配置文件路径,读取流资源。classpath路径下
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(resource);
//创建解析XML文件的对象SAXReader
SAXReader saxReader = new SAXReader();
//读取UserMapper.xml配置文件文档对象
Document document = saxReader.read(inputStream);
//获取文档对象根节点:<mapper namespace="test">
Element rootElement = document.getRootElement();
//取出根节点的命名空间
String namespace = rootElement.attributeValue("namespace");
//获取当前SQL映射文件所有查询语句标签
List<Element> selectNodes = document.selectNodes("//select");
//循环解析查询标签select,抽取SQL语句
for (Element element : selectNodes) {
//查询语句唯一标识
String id = element.attributeValue("id");
//当前查询语句返回结果集对象类型
String resultType = element.attributeValue("resultType");
//查询语句
String sql = element.getText();
//创建Mapper对象
SqlSource mapper = new SqlSource();
mapper.setSql(sql);
mapper.setResultType(resultType);
//在configuration中设置mapper类,key:(命名空间+.+SQL语句唯一标识符)
configuration.getSqlSourceMap().put(namespace + "." + id, mapper);
}
}
}
SqlSessionFactory
public class SqlSessionFactory {
private Configuration configuration;
public SqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
/**
* 创建SqlSession会话
*/
public SqlSession openSession(){
return new DefaultSqlSessionImpl(configuration);
}
public Configuration getConfiguration() {
return configuration;
}
}
SqlSession
public interface SqlSession {
<T> List<T> selectList(String statement) throws Exception;
<T> List<T> selectList(String statement, Object parameter) throws Exception;
int update(String statement) throws Exception;
int update(String statement, Object parameter) throws Exception;
int delete(String statement) throws Exception;
int delete(String statement, Object parameter) throws Exception;
int insert(String statement) throws Exception;
int insert(String statement, Object parameter) throws Exception;
<T> T getMapper(Class<T> type);
}
DefaultSqlSessionImpl
public class DefaultSqlSessionImpl implements SqlSession {
private Configuration configuration;
public DefaultSqlSessionImpl(Configuration configuration) {
this.configuration = configuration;
}
public <T> List<T> selectList(String statement) throws Exception {
Executor executor = new Executor(configuration);
return (List<T>) executor.executeQuery(statement);
}
@Override
public <T> T getMapper(Class<T> type) {
ClassLoader classLoader = type.getClassLoader();
// 创建一个代理对象,用户给定一个Mappper接口,我们负责生成一个Mapper代理实例返回去
// 这样在调用Mapper接口方法的时候,会进入代理类里面。
InvocationHandler handler = new MapperProxy(configuration);
return (T) Proxy.newProxyInstance(
classLoader,
new Class[]{type},
handler
);
}
// 其它略
}
Executor
public class Executor {
private Configuration configuration;
public Executor(Configuration configuration) {
this.configuration = configuration;
}
public List executeQuery(String statement) throws Exception {
/**
* 数据库连接信息硬编码
*/
String driver = configuration.getDriver();
String url = configuration.getUrl();
String username = configuration.getUsername();
String password = configuration.getPassword();
Map<String, SqlSource> map = configuration.getSqlSourceMap();
//SQL映射对象
SqlSource mapper = map.get(statement);
String sqlStr = mapper.getSql();
//获取查询SQL
String resultType = mapper.getResultType(); //获取返回值类的全限定名(包名 +类名)
//1.注册MySQL驱动
Class.forName(driver);
//2.获取连接Connection对象
/**
* 数据库连接的创建和释放频繁,造成系统资源的浪费,严重影响了系统性能
*/
Connection conn = DriverManager.getConnection(url, username, password);
//3.创建SQL语句对象Statement,填写SQL语句
PreparedStatement ps = conn.prepareStatement(sqlStr);
//4.执行查询SQL,返回结果集ResultSet
ResultSet rs = ps.executeQuery();
//5.解析结果集,获取查询用户list集合
//获取结果集元数据
ResultSetMetaData metaData = rs.getMetaData();
//获取总列数
int columnCount = metaData.getColumnCount();
//获取所有列的list集合
List<String> columnNames = new ArrayList<String>();
for (int i = 1; i <= columnCount; i++) {
columnNames.add(metaData.getColumnName(i));
}
List list = new ArrayList();
//循环解析结果集
while (rs.next()) {
//通过反射获取类的字节码对象,传入的参数是类的全限定名称
Class<?> clazz = Class.forName(resultType);
//反射创建对象
Object user = clazz.newInstance();
//反射获取当前类的所有方法
Method[] methods = clazz.getMethods();
//循环 遍历所有列名
for (String columnName : columnNames) {
for (Method method : methods) {
String methodName = method.getName();
//判断方法的名称,与set+列名相等,那么就把列名对应的值设置到当前对象的set方法中
if (("set" + columnName).equalsIgnoreCase(methodName)) {
//把列名column对应的值,设置到对象的set方法中,给属性赋值
method.invoke(user, rs.getObject(columnName));
}
}
}
//将用户存入集合中
list.add(user);
}
//关闭连接,释放资源
rs.close();
ps.close();
conn.close();
return list;
}
}
MapperProxy
public class MapperProxy implements InvocationHandler {
private final Configuration configuration;
public MapperProxy(Configuration configuration) {
this.configuration = configuration;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取方法所在类的包路径
String packagePath = method.getDeclaringClass().getName();
// 获取方法名
String methodName = method.getName();
// 将包路径和方法名通过"."拼接在一起
String fullPath = packagePath + "." + methodName;
SqlSource sqlSource = configuration.getSqlSourceMap().get(fullPath);
SqlSessionFactory factory = new SqlSessionFactory(this.configuration);
SqlSession sqlSession = factory.openSession();
switch (getSQLStatementType(sqlSource.getSql())) {
case SELECT:
// 简单来
return sqlSession.selectList(fullPath);
case DELETE: break;
case UPDATE: break;
case INSERT: break;
}
throw new RuntimeException();
}
public enum SQLStatementType {
SELECT,
DELETE,
UPDATE,
INSERT,
UNKNOWN
}
public static SQLStatementType getSQLStatementType(String sql) {
String normalizedSQL = sql.trim().toLowerCase();
if (normalizedSQL.startsWith("select")) {
return SELECT;
} else if (normalizedSQL.startsWith("delete")) {
return SQLStatementType.DELETE;
} else if (normalizedSQL.startsWith("update")) {
return SQLStatementType.UPDATE;
} else if (normalizedSQL.startsWith("insert")) {
return SQLStatementType.INSERT;
} else {
return SQLStatementType.UNKNOWN;
}
}
}
使用Demo
按照之前的用例图,我们先导入工程的依赖包、配置数据源等信息、编写Mapper、编写Mapper XML,就可以运行了。
- 导包
<!--自定义框架坐标jar包-->
<dependency>
<groupId>cn.lsj</groupId>
<artifactId>lsjORM</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>compile</scope>
</dependency>
- SqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<dataSource>
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/ruoyi?characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
<mappers>
<mapper resource="SysRoleMapper.xml"/>
</mappers>
</configuration>
- SysRoleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.hero.dao.SysRoleMapper">
<select id="findAll" resultType="com.hero.dao.SysRole">
select * from sys_role;
</select>
</mapper>
- 接口和实体
public interface SysRoleMapper {
List<SysRole> findAll();
}
public class SysRole {
private Long role_id;
private String role_name;
// getting/setting
}
- 测试类
@Test
public void test() throws Exception {
//1.创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//2.builder对象构建工厂对象
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = builder.build(inputStream);
// 拿到接口的默认实现类DefaultSqlSessionImpl
SqlSession sqlSession = sqlSessionFactory.openSession();
// 调用方式一:直接指定方法全路径
List<SysRole> objects = sqlSession.selectList("com.hero.dao.SysRoleMapper.findAll");
for (SysRole u : objects) {
System.out.println(u);
}
// 调用方式二:返回Mappper接口代理实例 MapperProxy,findAll方法会跑MapperProxy#invoke()方法
SysRoleMapper sysRoleMapper = sqlSession.getMapper(SysRoleMapper.class);
List<SysRole> users = sysRoleMapper.findAll();
for (SysRole u : users) {
System.out.println(u);
}
}
总结
通过手写一个简单的ORM框架,了解到 SQL映射配置、SQL执行、结果映射的基本原理,在上面的案例中,没有实现参数处理、缓存机制、插件机制,感兴趣的可以继续完善。