JDBC API 允许应用程序访问任何形式的表格数据,特别是存储在关系数据库中的数据
代码示例:
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 通过驱动管理类获取数据库链接
connection =
DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis? characterEncoding=utf-8", "root", "root");
// 定义sql语句?表示占位符
String sql = "select * from user where username = ?";
// 获取预处理statement
preparedStatement = connection.prepareStatement(sql);
// 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值 preparedStatement.setString(1, "tom");
// 向数据库发出sql执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
// 遍历查询结果集
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
// 封装User
user.setId(id);
user.setUsername(username);
}
System.out.println(user);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
剖开代码,逐个分析:
(1)加载驱动,获取链接:
硬编码:原生的JDBC在执行SQL操作时,常常需要编写SQL语句以及与数据库连接相关的配置信息,这些配置信息通常以硬编码的方式直接写在代码中。这种做法被认为是硬编码,因为配置信息与代码紧密耦合在一起,无法在运行时进行动态修改。
存在问题1:数据库配置信息存在硬编码问题
优化思路:使用配置文件!
存在问题2:频繁创建、释放数据库连接问题。
优化思路:使用数据连接池
(2)定义sql、设置参数、执行查询:
存在问题3:SQL语句、设置参数、获取结果集参数均存在硬编码问题 。
优化思路:使用配置文件!
(3)遍历查询结果集:
存在问题4:手动封装返回结果集,较为繁琐
优化思路:使用Java反射、内省!
针对JDBC各个环节中存在的不足,现在,我们整理出对应的优化思路,
存在问题 | 优化思路 |
---|---|
数据库配置信息存在硬编码问题 | 使用配置文件 |
频繁创建、释放数据库连接问题 | 使用数据连接池 |
SQL语句、设置参数、获取结果集参数均存在硬编码问题 | 使用配置文件 |
手动封装返回结果集,较为繁琐 | 使用Java反射、内省 |
JDBC是个人作战,凡事都是亲力亲为,低效而高险,自己加载驱动,自己建连接,自己手动创建和释放数据库资源,处理事务和异常等,是一种相对原始且底层的数据库访问方式。虽然JDBC提供了直接与数据库交互的API,但它需要开发者编写大量冗余的代码来处理数据库连接、结果集的封装等,容易出现重复性代码和错误,使得开发变得繁琐且容易出错。
而持久层框架好比是多工种协作,分工明确,执行高效,有专门负责解析注册驱动建立连接的,有专门管理数据连接池的,有专门执行sql语句的,有专门做预处理参数的,有专门装配结果集的 …
优化思路: 框架的作用,就是为了帮助我们减去繁重开发细节与冗余代码,使我们能更加专注于业务应用开发。
通过上图可以发现,拥有一套持久层框架是如此的舒适,我们仅仅需要干两件事:
框架,除了思考本身的工程设计,还需要考虑到实际项目端的使用场景,干系方涉及两端:
以上两步,我们通过一张架构图《 手写持久层框架基本思路 》来梳理清楚:
分工协作 | 角色定位 | 类名定义 |
---|---|---|
负责读取配置文件 | 资源辅助类 | Resources |
负责存储数据库连接信息 | 数据库资源类 | Configuration |
负责存储SQL映射定义、存储结果集映射定义 | SQL与结果集资源类 | MappedStatement |
负责解析配置文件,创建会话工厂SqlSessionFactory | 会话工厂构建者 | SqlSessionFactoryBuilder |
负责创建会话SqlSession | 会话工厂 | SqlSessionFactory |
指派执行器Executor | 会话 | SqlSession |
负责执行SQL (配合指定资源Mapped Statement) | 执行器 | Executor |
正常来说项目只对应一套数据库环境,一般对应一个SqlSessionFactory实例对象,我们使用单例模式只创建一个SqlSessionFactory实例。
如果需要配置多套数据库环境,那需要做一些拓展,例如Mybatis中通过environments等配置就可以支持多套测试/生产数据库环境进行切换。
(1)调用框架API,除了引入自定义持久层框架的jar包
(2)提供两部分配置信息:1.sqlMapConfig.xml : 数据库配置信息(地址/数据名/用户名/密码),以及mapper.xml的全路径
2.mapper.xml : SQL配置信息,存放SQL语句、参数类型、返回值类型相关信息
1、加载配置文件:根据配置文件的路径,加载配置文件成字节输入流,存储在内存中。
创建Resource类,提供加载流方法:InputStream resourceAsStream(String path)
2、 创建两个javaBean(容器对象):存放配置文件解析出来的内容
Configuration核心配置类: 解析存储sqlMapConfig.xml里面的内容
MappedStatement 配置映射类:存储mapper.xml解析出来的内容
3、解析配置文件(使用dom4j) ,并创建SqlSession会话对象
创建类:SqlSessionFactoryBuilder方法:build(Inputstream in)
>使用dom4j解析配置文件,将解析出来的内容封装到容器对象中
>创建SqlSessionFactory对象,生产sqlSession会话对象(工厂模式)
4、创建SqlSessionFactory接口以及实现类DefaultSqlSessionFactory
创建openSession()方法,生产sqlSession
5、创建SqlSession接口以及实现类DefaultSqlSession
定义对数据库的CRUD操作:
> selectList();
> selectOne();
> update();
> delete();
6、创建Executor接口以及实现类SimpleExecutor
创建query(Configuration configuration, MappedStatement mappedStatement, Object param)
实际执行的就是JDBCD代码。
基本过程我们已经清晰,我们再细化一下类图,更好的助于我们实际编码:
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8maven.compiler.encoding>
<java.version>8java.version>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
在使用端项目中创建配置配置文件
<configuration>
<dataSource>
<property name="driverClassName" value="com.mysql.jdbc.Driver">property>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC">property>
<property name="username" value="root">property>
<property name="password" value="root">property>
dataSource>
<mappers>
<mapper resource="mapper/UserMapper.xml">mapper>
<mapper resource="mapper/ProductMapper.xml">mapper>
mappers>
configuration>
(此时namespace还不是全类名的形式,如:“com.xc.mapper.UserMapper”)
<mapper namespace="User">
<select id="selectOne" resultType="com.xc.pojo.User" parameterType="com.xc.pojo.User">
select * from user where id= #{id} and username= #{username}
select>
<select id="selectList" resultType="com.xc.pojo.User">
select * from user
select>
mapper>
public class User {
//主键标识
private Integer id;
//用户名
private String username;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' + '}';
}
}
新建一个Maven工程在pom文件引入需要用到的依赖
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8maven.compiler.encoding>
<java.version>8java.version>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.6version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>dom4jgroupId>
<artifactId>dom4jartifactId>
<version>1.6.1version>
dependency>
<dependency>
<groupId>jaxengroupId>
<artifactId>jaxenartifactId>
<version>1.1.6version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.21version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
dependencies>
Resources
Configuration
/**
* @author xingchen
* @description 核心配置类: 解析存储sqlMapConfig.xml里面的内容
*/
public class Configuration {
// 数据源对象
private DataSource dataSource;
// key: statementId value :封装好的MappedStatement对象
private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public Map<String, MappedStatement> getMappedStatementMap() {
return mappedStatementMap;
}
public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) {
this.mappedStatementMap = mappedStatementMap;
}
}
MappedStatement
/**
* @author xingchen
* @description 配置映射类:存储mapper.xml解析出来的内容
* mapper.xml中
public class MappedStatement {
// 1.唯一标识 (statementId 由namespace.id组成)
private String statementId;
// 2.返回结果类型
private String resultType;
// 3.参数类型
private String parameterType;
// 4.要执行的sql语句
private String sql;
// 5.mapper代理:sqlCommandType 标签类型,如select
private String sqlCommandType;
public String getStatementId() {
return statementId;
}
public void setStatementId(String statementId) {
this.statementId = statementId;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public String getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public String getSqlCommandType() {
return sqlCommandType;
}
public void setSqlCommandType(String sqlCommandType) {
this.sqlCommandType = sqlCommandType;
}
}
SqlSessionFactoryBuilder
XMLMapperBuilder
/**
* @author xingchen
* @description
*/
public class XMLMapperBuilder {
private Configuration configuration;
public XMLMapperBuilder(Configuration configuration) {
this.configuration = configuration;
}
public void parse(InputStream inputStream) throws DocumentException {
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
List<Element> selectList = rootElement.selectNodes("//select");
/**
*
* select * from user where id= #{id} and username= #{userName}
*
*/
String namespace = rootElement.attributeValue("namespace");
for (Element element : selectList) { //id的值
//解析标签,封装mappedStatement
String id = element.attributeValue("id");
//返回结果class
String resultType = element.attributeValue("resultType");
//输入参数class
String parameterType = element.attributeValue("parameterType");
//sql语句
String sql = element.getTextTrim();
//statementId
String statementId = namespace + "." + id;
//封装 mappedStatement
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setStatementId(statementId);
mappedStatement.setResultType(resultType);
mappedStatement.setParameterType(parameterType);
mappedStatement.setSql(sql);
mappedStatement.setSqlCommandType("select");
//存到Configuration里面的map集合
configuration.getMappedStatementMap().put(statementId, mappedStatement);
}
}
}
sqlSessionFactory 接口及DefaultSqlSessionFactory 实现类
/**
* @author xingchen
* @description
*/
public interface SqlSessionFactory {
/**
* 生产sqlSession :封装着与数据库交互的方法
*/
SqlSession openSession();
}
/**
* @author xingchen
* @description
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
/**
* (1)创建执行器对象 (2)创建SqlSession对象
*
* @return
*/
@Override
public SqlSession openSession() {
// 执行器创建出来
Executor executor = new SimpleExecutor();
return new DefaultSqlSession(configuration, executor);
}
}
sqlSession 接口及 DefaultSqlSession 实现类(初步实现)
/**
* @author xingchen
* @description
*/
public interface SqlSession {
/**
* 查询所有的方法 select * from user where username like '%aaa%' and sex = ''
* 参数1:唯一标识
* 参数2:入参
*/
public <E> List<E> selectList(String statementId,Object parameter) throws Exception;
/**
* 查询单个的方法
*/
public <T> T selectOne(String statementId,Object parameter) throws Exception;
}
/**
* @author xingchen
* @description
*/
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration, Executor executor) {
this.configuration = configuration;
this.executor = executor;
}
@Override // user.selectList 1 tom user
public <E> List<E> selectList(String statementId, Object params) throws Exception {
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
// 将查询操作委派给底层的执行器
List<E> list = executor.query(configuration,mappedStatement,params);
return list;
}
@Override
public <T> T selectOne(String statementId, Object params) throws Exception {
List<Object> list = this.selectList(statementId, params);
if(list.size() == 1){
return (T) list.get(0);
}else if(list.size() > 1){
throw new RuntimeException("返回结果过多");
}else {
return null;
}
}
}
Executor
/**
* @author xingchen
* @description
*/
public interface Executor {
<E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception;
}
SimpleExecutor
/**
* @author xingchen
* @description
*/
public class SimpleExecutor implements Executor {
/**
* 真正执行底层的JDBC操作
*
* @param configuration
* @param mappedStatement
* @param param
* @param
* @return
*/
@Override
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception {
//1.获取数据库链接
Connection connection = configuration.getDataSource().getConnection();
//2.获取预编译对象 prepareStatement
//select * from user where id= #{id} and username= #{userName}
//select * from user where id= ? and username= ?
//获取出来的sql还需要处理 转换占位符: (1) #{}转换成? (2) #{}里面的值要保存下来
String sql = mappedStatement.getSql();
BoundSql boundSql = getBoundSql(sql);
String finalSql = boundSql.getFinalSql();
PreparedStatement preparedStatement = connection.prepareStatement(finalSql);
//3.设置参数
//问题1:param类型是不确定? map|user|String|product
String parameterType = mappedStatement.getParameterType();
if (parameterType != null) {
//user
Class<?> parameterTypeClass = Class.forName(parameterType);
//问题2:该把user对象里面哪个属性赋值给哪个占位符呢
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
// #{} id or username
String fileName = parameterMapping.getContent();
//通过字段名获取属性对象
Field declaredField = parameterTypeClass.getDeclaredField(fileName);
// 暴力访问
declaredField.setAccessible(true);
//获取到参数的值
Object fileValue = declaredField.get(param);
preparedStatement.setObject(i + 1, fileValue);
}
}
//4.执行sql
ResultSet resultSet = preparedStatement.executeQuery();
//5.封装返回结果集
String resultType = mappedStatement.getResultType();
Class<?> resultTypeClass = Class.forName(resultType);
ResultSetMetaData metaData = resultSet.getMetaData();
List<E> list = new ArrayList<>();
while (resultSet.next()) {
Object o = resultTypeClass.getConstructor().newInstance();
//比如说此处查询有id username字段
// 整个for循环的遍历针对表中的属性依次处理 id对象封装到user对象的id属性值,username封装到user对象的username属性值
for (int i = 1; i <= metaData.getColumnCount(); i++) {
String columnName = metaData.getColumnName(i);
Object columnValue = resultSet.getObject(columnName);
//根据映射关系进行封装
Field declaredField = resultTypeClass.getDeclaredField(columnName);
declaredField.setAccessible(true);
declaredField.set(o, columnValue);
}
list.add((E) o);
}
return list;
}
/**
* (1) #{}转换成? (2) #{}里面的值要保存下来
*
* @param sql
* @return
*/
private BoundSql getBoundSql(String sql) {
//标记处理器: 配合标记解析器完成占位符的解析
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
//标记解析器
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
//替换成了?的sql
String finalSql = genericTokenParser.parse(sql);
//#{}里面的值集合封装
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
return new BoundSql(finalSql, parameterMappings);
}
}
BoundSql
/**
* @author xingchen
* @description
*/
public class BoundSql {
//解析过后的sql语句
private String finalSql;
//解析出来的参数
private List<ParameterMapping> parameterMappings;
public BoundSql(String finalSql, List<ParameterMapping> parameterMappings) {
this.finalSql = finalSql;
this.parameterMappings = parameterMappings;
}
public String getFinalSql() {
return finalSql;
}
public void setFinalSql(String finalSql) {
this.finalSql = finalSql;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public void setParameterMappings(List<ParameterMapping> parameterMappings) {
this.parameterMappings = parameterMappings;
}
}
BoundSql
/**
* @author xingchen
* @description
*/
public class BoundSql {
//解析过后的sql语句
private String finalSql;
//解析出来的参数
private List<ParameterMapping> parameterMappings;
public BoundSql(String finalSql, List<ParameterMapping> parameterMappings) {
this.finalSql = finalSql;
this.parameterMappings = parameterMappings;
}
public String getFinalSql() {
return finalSql;
}
public void setFinalSql(String finalSql) {
this.finalSql = finalSql;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public void setParameterMappings(List<ParameterMapping> parameterMappings) {
this.parameterMappings = parameterMappings;
}
}
需要用到的工具类
GenericTokenParser
用于解析
${}
和#{}
这样的占位符。在 MyBatis 的 SQL 语句中,我们可以使用${}
来直接替换变量的值,而#{}
则会被解析为预编译的占位符。这个类通过传入的openToken
和closeToken
参数来标识占位符的开始标记和结束标记,然后通过传入的TokenHandler
参数来处理解析后的占位符的值。GenericTokenParser
的parse
方法会遍历 SQL 语句中的占位符,将占位符解析为实际的参数,并通过TokenHandler
处理后返回最终的 SQL 语句。
/**
* Copyright 2009-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.xc.utils;
/**
* @author Clinton Begin
*/
public class GenericTokenParser {
private final String openToken; //开始标记
private final String closeToken; //结束标记
private final TokenHandler handler; //标记处理器
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
/**
* 解析${}和#{}
* @param text
* @return
* 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。
* 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现
*/
public String parse(String text) {
// 验证参数问题,如果是null,就返回空字符串。
if (text == null || text.isEmpty()) {
return "";
}
// 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。
int start = text.indexOf(openToken, 0);
if (start == -1) {
return text;
}
// 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,
// text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
// 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理
if (start > 0 && src[start - 1] == '\\') {
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
//重置expression变量,避免空指针或者老数据干扰。
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {存在结束标记时
if (end > offset && src[end - 1] == '\\') {//如果结束标记前面有转义字符时
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {//不存在转义字符,即需要作为参数进行处理
expression.append(src, offset, end - offset);
offset = end + closeToken.length();
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//首先根据参数的key(即expression)进行参数处理,返回?作为占位符
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
ParameterMapping
用于存储 SQL 语句中的参数映射信息。在 MyBatis 解析占位符时,会将占位符的内容作为
ParameterMapping
的实例进行存储,以便后续处理。
package com.xc.utils;
public class ParameterMapping {
// id || username
private String content;
public ParameterMapping(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
ParameterMappingTokenHandler
实现了
TokenHandler
接口,主要用于处理解析后的占位符。在handleToken
方法中,它会将解析后的占位符内容构建成ParameterMapping
实例,并添加到parameterMappings
列表中。这样,parameterMappings
列表中就存储了 SQL 语句中的所有参数映射信息。
package com.xc.utils;
import java.util.ArrayList;
import java.util.List;
public class ParameterMappingTokenHandler implements TokenHandler {
private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
// context是参数名称 #{id} #{username}
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
ParameterMapping parameterMapping = new ParameterMapping(content);
return parameterMapping;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public void setParameterMappings(List<ParameterMapping> parameterMappings) {
this.parameterMappings = parameterMappings;
}
}
TokenHandler
这个接口定义了一个
handleToken
方法,用于处理解析后的占位符。具体的处理逻辑由实现TokenHandler
接口的类来实现。
/**
* Copyright 2009-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.xc.utils;
/**
* @author Clinton Begin
*/
public interface TokenHandler {
String handleToken(String content);
}
综合起来,这些工具类在 MyBatis 中的配合使用,可以将 SQL 语句中的占位符解析为真正的参数,并存储参数映射信息,方便后续的数据库操作。通过这种方式,MyBatis 可以实现动态 SQL 的功能,使得 SQL 语句更加灵活和易于维护。
通过上述的自定义框架,已经解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连 接,硬编码,手动封装返回结果集等问题,但是现在继续来分析刚刚完成的自定义框架代码,还有一些问题
问题如下:
dao的实现类中存在重复的代码,整个操作的过程模板重复(创建sqlsession,调用sqlsession方法,关闭 sqlsession)
dao的实现类中存在硬编码,调用sqlsession的方法时,参数statement的id硬编码,如下所示
@Test
public void test1() throws Exception {
//根据配置文件,加载成字节输入流
InputStream inputStream = Resources.resourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User user1 = new User();
user1.setUsername("tom");
user1.setId(1);
List<User> list1 = sqlSession.selectList("user.selectList");
List<User> list2 = sqlSession.selectList("user.selectOne", user1);
System.out.println(list1);
System.out.println(list2);
}
解决:使用代理模式来创建接口的代理对象
@Test
public void test2() throws Exception {
//根据配置文件,加载成字节输入流
InputStream inputStream = Resources.resourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User user1 = new User();
user1.setUsername("tom");
user1.setId(1);
// proxy 代理对象 jdk动态代理生成的!
//代理对象调用接口中的任意方法都会被拦截执行底层的invoke()
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> allUser = mapper.findAllUser();
User userObj = mapper.findUserByCondition(user1);
System.out.println(userObj);
System.out.println(allUser);
}
用户端UserMapper.java更改
public interface UserMapper {
/**
* 查询所有
*/
List<User> findAllUser();
/**
* 查询单个
*/
User findUserByCondition(User user);
}
UserMapper.xml
<mapper namespace="com.xc.mapper.UserMapper">
<select id="findAllUser" resultType="com.xc.pojo.User">
select * from user
select>
<select id="findUserByCondition" resultType="com.xc.pojo.User" parameterType="com.xc.pojo.User">
select * from user where id= #{id} and username= #{username}
select>
mapper>
在sqlSession中添加getMappper()方法
public interface SqlSession {
/**
* 查询所有的方法 select * from user where username like '%aaa%' and sex = ''
* 参数1:唯一标识
* 参数2:入参
*/
List selectList(String statementId) throws Exception;
List selectList(String statementId, Object param) throws Exception;
/**
* 根据条件查询
*/
T selectOne(String statementId, Object param) throws Exception;
/**
* mapper代理方式
*/
T getMapper(Class> mapperClass);
}
DefaultSqlSession实现类
/**
* @author xingchen
* @description
*/
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration, Executor executor) {
this.configuration = configuration;
this.executor = executor;
}
@Override
public <E> List<E> selectList(String statementId) throws Exception {
return selectList(statementId, null);
}
@Override
public <E> List<E> selectList(String statementId, Object param) throws Exception {
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
List<E> list = executor.query(configuration, mappedStatement, param);
return list;
}
@Override
public <T> T selectOne(String statementId, Object param) throws Exception {
List<Object> list = this.selectList(statementId, param);
if (list.size() == 1) {
return (T) list.get(0);
} else if (list.size() > 1) {
throw new RuntimeException("返回结果过多");
}
return null;
}
/**
* mapperClass接口,使用动态代理生成代理对象 jdk动态代理 cglib动态代理
*
* @param mapperClass
* @param
* @return
*/
@Override
public <T> T getMapper(Class<?> mapperClass) {
Object proxyInstance = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperClass},
new InvocationHandler() {
/**
* proxy:代理对象 method:当前被调用的方法对象 例:findAllUser args : 查询参数
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 查询数据库 增删改查 执行JDBC
//调用像selectOne或者selectList
//问题1:能不能获取到statementId
// 当前 statementId = 类名.方法名
//获取当前对象的类名 全路径,和UserMapper.xml中的namespace对应了
String className = method.getDeclaringClass().getName();
//获取正在调用的方法名
String methodName = method.getName();
String statementId = className + "." + methodName;
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
String sqlCommandType = mappedStatement.getSqlCommandType();
//问题2:怎么知道调用增删改查的哪个方法?
//解决:根据sqlCommandType判断
switch (sqlCommandType) {
case "select":
//问题3:现在是调用selectOne还是SelectList
//解决:根据返回值类型
Class<?> returnType = method.getReturnType();
boolean assignableFrom = Collection.class.isAssignableFrom(returnType);
if (assignableFrom) {
//查询所有
if (args != null) {
//判断是否有查询参数
return selectList(statementId, args[0]);
} else {
return selectList(statementId);
}
}
return selectOne(statementId, args[0]);
case "update":
//TODO 更新操作
break;
case "delete":
//TODO 更新操作
break;
case "insert":
//TODO 更新操作
break;
}
return null;
}
}
);
return (T) proxyInstance;
}
}
mybatis架构四层作用是什么呢?
组件关系如下图所示:
组件介绍:
SqlSession:是Mybatis对外暴露的核心API,提供了对数据库的DRUD操作接口。
Executor:执行器,由SqlSession调用,负责数据库操作以及Mybatis两级缓存的维护
StatementHandler:封装了JDBC Statement操作,负责对Statement的操作,例
PrepareStatement参数的设置以及结果集的处理。
ResultSetHandler:是StatementHandler内部一个组件,主要负责对ResultSet结果集的处理,封装成目标对象返回
TypeHandler:用于Java类型与JDBC类型之间的数据转换,ParameterHandler和ResultSetHandler会分别使用到它的类型转换功能
MappedStatement:是对Mapper配置文件或Mapper接口方法上通过注解申明SQL的封装
Configuration:Mybatis所有配置都统一由Configuration进行管理,内部由具体对象分别管理各自的小功能模块