1、Mybatis概述
1.1 Mybatis概念
MyBatis 本是 apache 的一个开源项目 iBatis, 2010 年这个项目由 apache software foundation 迁移到了 google code,并且改名为 MyBatis 。2013 年 11 月迁移到 Github。iBATIS 一词来源于“internet”和“abatis”的组合,是一个基于 Java 的持久层框架。iBATIS 提供的持 久层框架包括 SQL Maps 和 Data Access Objects(DAO)。 Mybatis 基于java的持久层框架,它的内部封装了JDBC,让开发人员只需要关注SQL语句本身,不需要花费精力在驱动的加载、连接的创 建、Statement的创建等复杂的过程。 Mybatis通过XML或注解的方式将要执行的各种的statement配置起来,并通过java对象和statement中的sql的动态参数进行映射生成最终执 行的SQL语句,最后由mybatis框架执行SQL,并将结果直接映射为java对象。 采用了ORM思想解决了实体类和数据库表映射的问题。对JDBC进行了封装,屏蔽了JDBCAPI底层的访问细节,避免我们与jdbc的api打交 道,就能完成对数据的持久化操作。
O--Object java对象
R- Relation 关系,就是数据库中的一张表
M-mapping 映射
1.2 JDBC编程
通过反射实现对任意表的查询
public class JdbcUtil<E> {
public Connection getConnection() throws ClassNotFoundException, SQLException {
String url = "jdbc:mysql://localhost:3306/testdb?serverTimezone=Asia/Shanghai";
String username = "root";
String password = "123456";
// 加载驱动程序
Class.forName("com.mysql.cj.jdbc.Driver");
// 建立数据库连接
Connection connection = DriverManager.getConnection(url, username, password);
return connection;
}
@SneakyThrows
public List<Object> query(String sql,Class<E> clazz){
Connection connection = new JdbcUtil().getConnection();
PreparedStatement pstmt = connection.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery();
ResultSetMetaData md = rs.getMetaData();
int columns = md.getColumnCount();
// 获取默认构造函数
Constructor<?> constructor = clazz.getDeclaredConstructor();
List<Object> list = new ArrayList<>();
while (rs.next()) {
// 创建对象
String setMethod = "set";
Object object = constructor.newInstance();
for (int i = 1; i <= columns; i++) {
// 获取列名
String columnName = md.getColumnName(i);
// 获取属性的 setter 方法名
String setterMethodName = "set" + getMethodName(columnName);
// 获取属性的类型
Class<?> parameterType = object.getClass().getDeclaredField(columnName).getType();
// 获取属性的 setter 方法
Method setterMethod = object.getClass().getMethod(setterMethodName, parameterType);
// 获取结果集中的字段值
Object value = rs.getObject(i);
// 调用 setter 方法设置属性值
setterMethod.invoke(object, value);
}
list.add(object);
}
//关闭资源和连接
rs.close();
pstmt.close();
connection.close();
return list;
}
public String getMethodName(String str){
if (str == null || str.isEmpty()) {
return str;
}
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
}
}
1.3 Mybatis解决的问题
1、数据库连接的创建、释放连接的频繁操作造成资源的浪费从而影响系统的性能。
2、SQL语句编写在代码中,硬编码造成代码不容易维护,实际应用中SQL语句变化的可能性比较大,一旦变动就需要改变java类。
3、使用preparedStatement的时候传递参数使用占位符,也存在硬编码,因为SQL语句变化,必须修改源码。
4、对结果集的解析中也存在硬编码。
2、Mybatis对象分析
2.1 Resources
Resources 类,顾名思义就是资源,用于读取资源文件。其有很多方法通过加载并解析资源文件,返回不同类型的 IO 流对象。
2.2 SqlSessionFactoryBuilder
SqlSessionFactory 的 创 建 , 需 要 使 用 SqlSessionFactoryBuilder 对 象 的 build() 方 法 。 事实上使用SqlSessionFactoryBuilder的原因 是将SqlSessionFactory这个复杂对象的创建交由Builder来执行,也就是使用了建造者设计模式。
建造者模式: 又称生成器模式,是一种对象的创建模式。 可以将一个产品的内部表象与产品的生成过程分割开来, 从而可以使一个建造过程生成具有 不同的内部表象的产品(将一个复杂对象的构建与它的表示分离, 使得同样的构建过程可以创建不同的表示). 这样用户只需指定需要建造的类型就可 以得到具体产品,而不需要了解具体的建造过程和细节. 在建造者模式中,角色分指导者(Director)与建造者(Builder): 用户联系指导者, 指导者指挥建造者, 最后得到产品. 建造者模式可以强制实行 一种分步骤进行的建造过程
2.3 SqlSessionFactory
SqlSessionFactory 接口对象是一个重量级对象(系统开销大的对象),是线程安全的,所以一个应用只需要一个该对象即可。创建 SqlSession 需要使用 SqlSessionFactory 接口的的 openSession()方法。
默认的 openSession()方法没有参数,它会创建有如下特性的 SqlSession: 1、会开启一个事务(也就是不自动提交)。 2、将从由当前环境配置的 DataSource 实例中获取 Connection 对象。事务隔离级别将会使用驱动或数据源的默认设置。 3、预处理语句不会被复用,也不会批量处理更新。 openSession(true):创建一个有自动提交功能的 SqlSession openSession(false):创建一个非自动提交功能的 SqlSession,需手动提交 openSession():同 openSession(false)
2.4 SqlSession
SqlSession 接口对象用于执行持久化操作。一个 SqlSession 对应着一次数据库会话,一次会话以SqlSession 对象的创建开始,以 SqlSession 对象的关闭结束。 SqlSession 接口对象是线程不安全的,所以每次数据库会话结束前,需要马上调用其 close()方法,将其关闭。再次需要会话,再次创建。 SqlSession 在方法内部创建,使用完毕后关闭。 SqlSession 类中有超过 20 个方法,我们常用的几乎都是执行语法相关的方法。 这些方法被用来执行定义在 SQL 映射的 XML 文件中的 SELECT、INSERT、UPDATE 和 DELETE 语句。它们都会自行解释,每一句都使用语 句的 ID 属性和参数对象,参数可以是原生类型(自动装箱或包装类)、JavaBean、POJO 或 Map。
<T> T selectOne(String statement, Object parameter)
<E> List<E> selectList(String statement, Object parameter)
<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey)
int insert(String statement, Object parameter)
int update(String statement, Object parameter)
int delete(String statement, Object parameter)
<!--
selectOne 和 selectList 的不同仅仅是 selectOne 必须返回一个对象或 null 值。如果返回值多于一个,那么就会抛出异常。
selectMap 稍微特殊一点,因为它会将返回的对象的其中一个属性作为 key 值,将对象作为 value 值,从而将多结果集转为 Map 类型值。因为
并不是所有语句都需要参数,所以这些方法都重载成不需要参数的形式。
-->
3.5 Mybatis架构
1、Mybatis.xml文件是mybatis框架的全局配置文件,配置了mybatis框架运行的环境等信息Mapper1.xml.....是SQL的映射文件,文件中配置了所有的操作数据库的sql语句,这些文件需要在全局配置文件中加载。
2、通过mybatis环境等配置信息构建SqlSessionFactroy ,相当于是产生连接池
3、由会话工厂创建SqlSession即会话(连接),操作数据库需要通过SqlSession进行的。
4、Mybatis底层自定义了Executor执行器的接口操作数据库,Executor接口有两个实现,一个基本的执行器,一个是缓存的执行器。
5、Mapped statement 也是mybatis框架一个底层的封装对象,他包装了mybatis配置信息以及sql映射信息。Mapper.xml文件中的一个SQL语句对应一个Mapped statement对象,sql的id就是Mapped statement的id。
6、Mapped statement对SQL执行输入参数的定义,输入参数包括HashMap、基本类型、pojo,Executor通过Mapped statemen在执行SQL语句前将输入java对象映射到sql语句中,执行完毕SQL之后,输出映射就是JDBC编码中的对preparedStatement 执行结果的定义。
3.手写一个类似的mybatis中的一小段(查询)
在 MyBatis 中,动态代理技术用于自动生成 Mapper 接口的实现类,使开发者只需编写接口定义而无需实现具体的方法体。在这里,我们将通过注解的方式来模拟 MyBatis 读取 XML 配置文件的过程,并进行一些优化。
3.1首先,我们定义一个 @Select
注解,用于标记 Mapper 接口中的查询方法,并指定对应的 SQL 语句:
package com.fbatis.anno;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Select {
String value();
}
3.2接下来,我们创建一个 MapperProxy类,实现动态代理的逻辑:
package com.fbatis.proxy;
import com.fbatis.anno.Select;
import com.fbatis.session.Session;
import java.awt.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.List;
//数据库连接-Connection ===session
public class MapperProxy implements InvocationHandler {
private final String openToken="#{";
private final String closeToken="}";
Session session;
public MapperProxy(Session session){
this.session=session;
}
//执行sql 返回相应数据给我们
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/**
* 1、拿到sql
*/
if(method.isAnnotationPresent(Select.class)) {
//当前方法的返回类型---listOne=MemberEntity.class
Class<?> returnType = method.getReturnType();
Select select = method.getAnnotation(Select.class);
String sql = select.value();
//2、解析sql
sql = parse(sql);
//已经明确了需要执行 pstmt.executeQuery();
//需要知道返回多少数据
if(Collection.class.isAssignableFrom(returnType)){
//需要返回list 一般会有泛型 所以第一步获取泛型的类型
ParameterizedType parameterizedType = (ParameterizedType) method.getGenericReturnType();
Class clazz = (Class) parameterizedType.getActualTypeArguments()[0];
//需要把泛型类型传给执行sql那里---rs转化--entity--add--List
List list = session.executeQuery(sql, clazz,args);
return list;
}else if(Integer.class.isAssignableFrom(returnType)){
Integer count = (Integer) session.executeQuery(sql, returnType,args).get(0);
return count;
}else{
List entity1 = session.executeQuery(sql, returnType, args);
if(entity1.size()==0) {
return null;
}
Object entity = entity1.get(0);
return entity;
}
}
return null;
}
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
do {
if (start > 0 && src[start - 1] == '\\') {
// 这个开放标记被转义了。去掉反斜杠并继续
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// 找到了开放标记。让我们搜索闭合标记
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);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
builder.append("?");
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
} while (start > -1);
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
在上述代码中,我们通过 method.getAnnotation(Select.class)
来获取方法上的 @Select
注解,并从注解中获取 SQL 语句。然后,我们执行 SQL 查询,并处理结果集。
3.3接下来,我们创建一个 类,用于创建连接池:
package com.fbatis.session;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 1、创建session
* 2、连接池
*/
public class SessionFactory {
DataSource dataSource;
public SessionFactory(){
//创建连接池
DriverManagerDataSource source = new DriverManagerDataSource();
source.setDriverClassName("com.mysql.cj.jdbc.Driver");
source.setUsername("root");
source.setPassword("123456");
source.setUrl("jdbc:mysql://localhost:3306/testdb?serverTimezone=Asia/Shanghai");
dataSource=source;
}
public Session createSession(){
Connection connection = null;
try {
connection = dataSource.getConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
return new Session(connection);
}
}
3.4接下来,我们创建一个 MyBatisSqlSession
类,用于创建动态代理对象和创建连接:
package com.fbatis.session;
import com.fbatis.proxy.MapperProxy;
import com.fbatis.test.MemperDao;
import org.apache.commons.beanutils.BeanUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 1、getDao(Class<T> clazz)--返回对象
* 2、数据库连接
*/
public class Session<T> {
Connection connection;
Class<T> tClass;
public Session(Connection connection) {
this.connection = connection;
}
//返回一个 dao对应的对象
//JDK动态代理
public <T> T getMapper(Class<T> clazz) {
ClassLoader classLoader = Session.class.getClassLoader();
MapperProxy proxyInvocation = new MapperProxy(this);
Object proxy = Proxy.newProxyInstance(classLoader, new Class[]{clazz}, proxyInvocation);
return (T) proxy;
}
//所有都返回list
public List<T> executeQuery(String sql, Class<T> tClass, Object[] params) throws InstantiationException, IllegalAccessException, InvocationTargetException {
this.tClass = tClass;
//存储将来返回出去的所有
List<T> list = new ArrayList<>();
//存储每一行从数据库当中查询出来的列明和对应的值
Map<String, Object> map = new HashMap<String, Object>();
//每一列都会对应一个实体类对象
T t = null;
try {
PreparedStatement pstmt = connection.prepareStatement(sql);
if (params != null) {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
}
ResultSet rs = pstmt.executeQuery();
ResultSetMetaData md = rs.getMetaData();
int columns = md.getColumnCount();
while (rs.next()) {
if (Integer.class.isAssignableFrom(tClass)) {
Integer anInt = rs.getInt(1);
list.add((T) anInt);
} else {
for (int i = 1; i <= columns; ++i) {
map.put(md.getColumnName(i), rs.getObject(i));
}//把一行数据已经封装成为了一个map
//先要实例化对象
t = tClass.newInstance();
//map转实体类对象
BeanUtils.copyProperties(t, map);
list.add(t);
}
}
return list;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
3.5接下来开始测试,创建实体对象:
package com.fbatis.entity;
import java.util.List;
public class MemberEntity {
private Integer id;
private String account;
private String password;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
创建接口,注解实现查询的sql语句:
package com.fbatis.test;
import com.fbatis.anno.Select;
import com.fbatis.entity.MemberEntity;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public interface MemperDao {
@Select("select * from account ")
public List<MemberEntity> list();
@Select("select * from account where id= #{id} and password= #{password} ")
public MemberEntity listOne(Integer id, String password);
@Select("select count(*) from account ")
public Integer listCount(Integer id);
}
测试类:
package com.fbatis.test;
import com.fbatis.entity.MemberEntity;
import com.fbatis.session.Session;
import com.fbatis.session.SessionFactory;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j(topic = "e")
public class Test {
public static void main(String[] args) {
//log.debug("xxx");
//1 创建sqlSessionFactory
//m:配置文件
SessionFactory factory = new SessionFactory();
Session session = factory.createSession();
MemperDao mapper = (MemperDao) session.getMapper(MemperDao.class);
MemberEntity list = mapper.listOne(2,"300");
log.debug("id:{}",list.getId());
log.debug("password:{}",list.getPassword());
log.debug("account:{}",list.getAccount());
}
}
需要注意的是,这只是一个简单的示例,实际的MyBatis框架要比这个复杂得多,还涉及到事务管理、参数映射、结果映射等更多功能和细节。
通过使用动态代理和注解方式,可以实现简洁、灵活的Mapper接口编程模型,提高开发效率。同时,根据实际需求进行优化,如使用连接池管理数据库连接、缓存已解析的SQL语句等,以提升MyBatis框架的性能和可扩展性。