一、引言

1.JDBC

在没有ORM框架的时候,对数据库的操作都是直接通过jdbc来完成的,假如有一张t_user表,那操作流程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 注册 JDBC 驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 打开连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatisdb?characterEncoding=utf-8&serverTimezone=UTC","root","123456");

// 执行查询
stmt = conn.createStatement();
String sql = "SELECT id,user_name,real_name,password,age,d_id from t_user where id = 1";
ResultSet rs = stmt.executeQuery(sql);

// 获取结果集
while (rs.next()) {
Integer id = rs.getint("id");
String userName = rs.getString("user_name");
String realName = rs.getString("real_name");
String password = rs.getString("password");
Integer did = rs.getInt("d_id");
user.setId(id);
user.setRealName(realName);
user.setUserName(userName);
user.setPassword(password);
user.setDid(did);

System.out.println(user);
}

具体的操作步骤是,首先在pom文件中引入MySQL驱动依赖,注意版本

  • Class.forName注册驱动
  • 获取一个Connection对象
  • 创建一个Statement对象
  • execute()方法执行SQL语句,获取ResultSet结果集
  • 通过ResultSet结果集给POJO的属性赋值
  • 最后关闭相关的资源

但是通过分析上面的代码可以发现,注册驱动,创建连接,关闭资源这些步骤都是重复的

而获取结果集这一步其实做的工作是很相似的,也可以使用配置完成

执行sql则也没什么不一样的地方,所以上面的整个步骤过程,其实完全可以搞一个模版来完成,只需要修改sql和结果集映射就行了,其他地方统一处理

2.优化代码

首先是创建连接,关闭连接这一部分代码,可以抽取出来进行封装成一个工具类,此处为类DBUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class DBUtils {

private static final String JDBC_URL ;
private static final String JDBC_NAME ;
private static final String JDBC_PASSWORD ;

private static Connection conn;

static {
Properties properties = new Properties();
InputStream in = DBUtils.class.getClassLoader().getResourceAsStream("db.properties");
try {
properties.load(in);
} catch (IOException e) {
e.printStackTrace();
}
// 读取配置文件
JDBC_URL = properties.getProperty("JDBC_URL");
JDBC_NAME = properties.getProperty("JDBC_NAME");
JDBC_PASSWORD = properties.getProperty("JDBC_PASSWORD");
}

/**
* 获取连接通道
* @return
* @throws Exception
*/
public static Connection getConnection() throws Exception {
if(conn == null){
try{
conn = DriverManager.getConnection(JDBC_URL,JDBC_NAME,JDBC_PASSWORD);
}catch (Exception e){
e.printStackTrace();
throw new Exception();
}
}
return conn;
}
// 关闭连接
public static void close(Connection conn ){
close(conn,null);
}

public static void close(Connection conn, Statement sts ){
close(conn,sts,null);
}

public static void close(Connection conn, Statement sts , ResultSet rs){
if(rs != null){
try {
rs.close();
}catch (Exception e){
e.printStackTrace();
}
}

if(sts != null){
try {
sts.close();
}catch (Exception e){
e.printStackTrace();
}
}

if(conn != null){
try {
conn.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}

然后是结果集映射,此处也可以进行封装,这里从原数据和反射这两方面入手

所谓元数据,就是对应数据库表的列数据,可以把这里的字段自动映射成驼峰,反射创建结果集数据

最终封装结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 根据字段名称设置 对象的属性
* @param o 存储结果集的对象
* @param columnName o的字段名称
*/
private static void setFieldValueForColumn(Object o, String columnName,Object columnValue) {
Class<?> clazz = o.getClass();
try {
// 根据字段获取属性
Field field = clazz.getDeclaredField(columnName);
// 私有属性放开权限
field.setAccessible(true);
field.set(o,columnValue);
field.setAccessible(false);
}catch (Exception e){
// 说明不存在 那就将 _ 转换为 驼峰命名法 user_name --> userName
if(columnName.contains("_")){
Pattern linePattern = Pattern.compile("_(\\w)");
columnName = columnName.toLowerCase();
Matcher matcher = linePattern.matcher(columnName);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
}
matcher.appendTail(sb);
// 再次调用复制操作
setFieldValueForColumn(o,sb.toString(),columnValue);
}
}
}

此时已经完成了通用部分的处理,那么剩下的自然就是DML操作的实现了,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 执行数据库的DML操作
* @return
*/
public static Integer update(String sql,Object ... parameter) throws Exception{
conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
if(parameter != null && parameter.length > 0){
for (int i = 0; i < parameter.length; i++) {
ps.setObject(i+1,parameter[i]);
}
}
int i = ps.executeUpdate();
close(conn,ps);
return i;
}

/**
* 查询方法的简易封装
* @param sql
* @param clazz
* @param parameter
* @param <T>
* @return
* @throws Exception
*/
public static <T> List<T> query(String sql, Class clazz, Object ... parameter) throws Exception{
conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
if(parameter != null && parameter.length > 0){
for (int i = 0; i < parameter.length; i++) {
ps.setObject(i+1,parameter[i]);
}
}
ResultSet rs = ps.executeQuery();
// 获取对应的表结构的元数据
ResultSetMetaData metaData = ps.getMetaData();
List<T> list = new ArrayList<>();
while(rs.next()){
// 根据 字段名称获取对应的值 然后将数据要封装到对应的对象中
int columnCount = metaData.getColumnCount();
Object o = clazz.newInstance();
for (int i = 1; i < columnCount+1; i++) {
// 根据每列的名称获取对应的值
String columnName = metaData.getColumnName(i);
Object columnValue = rs.getObject(columnName);
setFieldValueForColumn(o,columnName,columnValue);
}
list.add((T) o);
}
return list;
}

最后写一下测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class JdbcTest {

public static void main(String[] args) {
new JdbcTest().queryUser();
//new JdbcTest().addUser();
}

/**
*
* 通过JDBC查询用户信息
*/
public void queryUser(){
try {
String sql = "SELECT id,user_name,real_name,password,age,d_id from t_user where id = ?";
List<User> list = DBUtils.query(sql, User.class,2);
System.out.println(list);
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 通过JDBC实现添加用户信息的操作
*/
public void addUser(){
String sql = "INSERT INTO T_USER(user_name,real_name,password,age,d_id)values(?,?,?,?,?)";
try {
DBUtils.update(sql,"wangwu","王五","111",22,1001);
} catch (Exception e) {
e.printStackTrace();
}
}
}

可以看到,此时用户可以很方便的使用查询与更新操作,而免去了之前的冗余操作

事实上,有很多公司在这个基础上确实实现了一些数据库操作工具的封装,如Apache DBUtils、SpringJDBC(jdbcTemplate)

但是上面这些工具还是有些简陋的,实际上在开发中,用的较多的是专业的ORM持久层框架,Hibernate和MyBatis这些

二、ORM框架

1.ORM介绍

什么叫ORM框架呢?

全称是ORM(Object Relational Mapping),即对象关系映射

所谓对象,即程序中的对象,关系就是它和数据库里面数据的关系

因此,ORM框架解决的就是程序对象和关系型数据库的相互映射问题

2.Hibernate

Hibernate是一个全映射的ORM框架,所谓全映射,就是它可以自动的帮我们处理对象到数据库和数据库到对象的映射关系

相对应地,半ORM框架MyBatis就要求用户自己编写数据库到结果集的映射关系

Hibernate的出现大大简化了我们的数据库操作,同时也能够更好的应对更加复杂的业务场景

Hibernate具有如下的特点

  • 根据数据库方言自定生成SQL,移植性好
  • 自动管理连接资源
  • 实现了对象和关系型数据的完全映射,操作对象就像操作数据库记录一样
  • 提供了缓存机制

Hibernate在处理复杂业务的时候同样也存在一些问题

  • 比如API中的get(),update()和save()方法,操作的实际上是所有的字段,没有办法指定部分字段,不够灵活
  • 自定生成SQL的方式,如果要基于SQL去做一些优化的话,也是非常困难的
  • 不支持动态SQL,比如分表中的表名,条件,参数变化等,无法根据条件自动生成SQL

因此,更加灵活的框架MyBatis就出现了

3.MyBatis

MyBatis是一个半映射的ORM的持久层框架,为什么是半映射的ORM呢?

因为像Hibernate这种全映射的ORM框架,因为封装程度太高,导致sql语句不够灵活,并且还需要学习他的HQL语言,配置也更为复杂,相对而言,MyBatis的sql和代码是分离开的,对于用户来说,只要会写sql就能用mybatis,基本省却了很大的学习成本

半映射和全映射

  • 半映射:半映射即指实体类和数据库表之间的映射是部分的,即只映射了实体类中需要持久化到数据库的属性,而忽略了其他的属性。在半映射方式下,开发人员需要手动编写SQL语句,执行数据库操作
  • 全映射:全映射指实体类和数据库表之间的映射是完全的,即实体类中的所有属性都需要持久化到数据库中。在全映射方式下,ORM框架会自动将实体类映射为数据库表,并自动执行相应的数据库操作,开发人员不需要手动编写SQL语句

三、MyBatis详解

在MyBatis当中,最核心的配置主要就两个,一个是全局配置文件,一个是映射文件

那么他们都有什么作用呢?下面分别介绍

1.全局配置文件

MyBatis的全局配置文件在全局层面规定了MyBatis的行为和属性信息,配置文件的顶层结构如下

  • configuration(配置)
  • properties(属性)
  • settings(设置)
  • typeAliases(类型别名)
  • typeHandlers(类型处理器)
  • objectFactory(对象工厂)
  • plugins(插件)
  • environment(环境配置)
    • environment(环境变量)
      • transactionManager(事务管理器)
      • dataSource(数据源)
  • databaseIdProvider(数据库厂商标识)
  • mappers(映射器)

1.1 configuration

它是整个配置文件的根标签,实际上也对应着MyBatis里面最重要的配置类Configuration

1.2 properties

第一个一级标签是properities,用来配置参数信息,例如最常见的数据库连接信息

一般来说,这些参数都是放在properties文件中的,让你好使用$符号把参数引入,这样就可以统一在参数文件中修改参数

1.3 settings

这个参数在MyBatis当中极为重要,规定了MyBatis运行时的行为,包含大量子标签,常用如下

  • cacheEnabled:是否启用缓存,默认为true
  • lazyLoadingEnabled:是否启用延迟加载,默认为false
  • multipleResultSetsEnabled:是否允许返回多个结果集(游标),默认为true
  • useColumnLabel:是否使用列标签代替列名,如果设置为true,则会优先使用列标签。默认为true
  • useGeneratedKeys:是否使用自动生成的主键,如果设置为true,则会使用数据库自动生成的主键作为实体类的主键。默认为false
  • defaultExecutorType:默认的执行器类型,可选值为SIMPLEREUSEBATCH。默认为SIMPLE
  • defaultStatementTimeout:默认的SQL语句执行超时时间(秒),默认为null,表示没有超时时间限制。
  • logImpl:指定MyBatis的日志实现类。
  • autoMappingBehavior:自动映射行为,可选值为PARTIALFULLNONE,默认为PARTIAL
  • mapUnderscoreToCamelCase:是否将下划线分隔的列名转换为驼峰式命名的属性名,如果设置为true,则会将user_name转换为userName
  • defaultScriptingLanguage:默认的动态SQL语言,可选值为XMLRAW#{}${},默认为XML

除了上述属性外,settings标签还可以包含其他的属性,具体可以参考MyBatis官方文档

下面看一个项目中实际用到的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 使全局的映射器启用或禁用缓存。 -->
<setting name="cacheEnabled" value="true"/>
<!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- 是否允许单条sql 返回多个数据集 (取决于驱动的兼容性) default:true -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
<setting name="useColumnLabel" value="true"/>
<!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。 default:false -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不隐射 PARTIAL:部分 FULL:全部 -->
<setting name="autoMappingBehavior" value="PARTIAL"/>
<!-- 这是默认的执行类型 (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新) -->
<setting name="defaultExecutorType" value="SIMPLE"/>
<!-- 使用驼峰命名法转换字段。 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 设置本地缓存范围 session:就会有数据的共享 statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->
<setting name="localCacheScope" value="SESSION"/>
<!-- 设置但JDBC类型为空时,某些驱动程序 要指定值,default:OTHER,插入空值时不需要指定类型 -->
<setting name="jdbcTypeForNull" value="NULL"/>
<!--输出日志-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
</configuration>

1.4 typeAliases

typeAliases是类型的别名,根linux中的alias一样,主要用来简化类名全路径的拼写

如下使用

1
2
3
<typeAliases>
<typeAlias alias="user" type="com.zjyan.domain.User"/>
</typeAliases>

事实上,在MyBatis当中有很多系统预先定义好的类型别名,在TypeAliasRegistry中,这也是为什么可以直接用string代替java.lang.String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public TypeAliasRegistry() {
this.registerAlias("string", String.class);
this.registerAlias("byte", Byte.class);
this.registerAlias("long", Long.class);
this.registerAlias("short", Short.class);
this.registerAlias("int", Integer.class);
this.registerAlias("integer", Integer.class);
this.registerAlias("double", Double.class);
this.registerAlias("float", Float.class);
this.registerAlias("boolean", Boolean.class);
this.registerAlias("byte[]", Byte[].class);
this.registerAlias("long[]", Long[].class);
this.registerAlias("short[]", Short[].class);
this.registerAlias("int[]", Integer[].class);
this.registerAlias("integer[]", Integer[].class);
this.registerAlias("double[]", Double[].class);
this.registerAlias("float[]", Float[].class);
this.registerAlias("boolean[]", Boolean[].class);
this.registerAlias("_byte", Byte.TYPE);
this.registerAlias("_long", Long.TYPE);
this.registerAlias("_short", Short.TYPE);
this.registerAlias("_int", Integer.TYPE);
this.registerAlias("_integer", Integer.TYPE);
this.registerAlias("_double", Double.TYPE);
this.registerAlias("_float", Float.TYPE);
this.registerAlias("_boolean", Boolean.TYPE);
this.registerAlias("_byte[]", byte[].class);
this.registerAlias("_long[]", long[].class);
this.registerAlias("_short[]", short[].class);
this.registerAlias("_int[]", int[].class);
this.registerAlias("_integer[]", int[].class);
this.registerAlias("_double[]", double[].class);
this.registerAlias("_float[]", float[].class);
this.registerAlias("_boolean[]", boolean[].class);
this.registerAlias("date", Date.class);
this.registerAlias("decimal", BigDecimal.class);
this.registerAlias("bigdecimal", BigDecimal.class);
this.registerAlias("biginteger", BigInteger.class);
this.registerAlias("object", Object.class);
this.registerAlias("date[]", Date[].class);
this.registerAlias("decimal[]", BigDecimal[].class);
this.registerAlias("bigdecimal[]", BigDecimal[].class);
this.registerAlias("biginteger[]", BigInteger[].class);
this.registerAlias("object[]", Object[].class);
this.registerAlias("map", Map.class);
this.registerAlias("hashmap", HashMap.class);
this.registerAlias("list", List.class);
this.registerAlias("arraylist", ArrayList.class);
this.registerAlias("collection", Collection.class);
this.registerAlias("iterator", Iterator.class);
this.registerAlias("ResultSet", ResultSet.class);
}

1.5 TypeHandler

这个关键字主要用于数据转换,什么是数据转换,又为什么需要数据转换呢?

比如写的接收查询结果的java类和数据库字段存在类型不一致,字段名称不一样的情况,要怎么办呢?

比如参数类型的返回是varchar,那么是如何转换成String类型的呢?

首先,常见的varchar转String这种类型转换由MyBatis内置的TypeHandler处理就够了,MyBatis会自动的将这些需要转换的类型进行处理

那么开发者什么时候用得到这些功能呢?如何使用呢?

一般来说,开发者可以自定义一个TypeHandler来处理不同的数据转换任务,比如写一个查询结果是字符且值为“zhangsan”的话,就添加666的处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

/**
* 自定义的类型处理器
* 处理的字段如果是 String类型的话就 且 内容是 zhangsan 拼接个信息
*/
/*@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(String.class)*/
public class MyTypeHandler extends BaseTypeHandler<String> {
/**
* 插入数据的时候回调的方法
* @param ps
* @param i
* @param parameter
* @param jdbcType
* @throws SQLException
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
//System.out.println("---------------setNonNullParameter1:"+parameter);
ps.setString(i, parameter);
}

@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String name = rs.getString(columnName);
if("zhangsan".equals(name)){
return name+"666";
}
return name;
}

@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String name = rs.getString(columnIndex);
if("zhangsan".equals(name)){
return name+"666";
}
return name;
}

@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String name = cs.getString(columnIndex);
if("zhangsan".equals(name)){
return name+"666";
}
return name;
}
}

当然,如果想使上面的handler生效,还需要typeHandlers配置

1
2
3
<typeHandlers>
<typeHandler handler="com.zjyan.type.MyTypeHandler"></typeHandler>
</typeHandlers>

当然,typeHandler不止上面这种简单的用法,试想一下,假如查询的字段中,有好几个字段都需要按照枚举id字段取对应的name值,那使用typeHandler是不是就可以避免再单独对字段进行转换的工作了?

1.6 objectFactory

当查询到数据库的结果集,将结果集转换为实体类的时候,是不是需要创建一个新的对象接收这些结果?那当然就需要创建对象了!

但是我们不知道需要创建什么类型的对象,对象有哪些属性都不清楚,所以需要使用反射来创建

而ObjectFactory就是MyBatis提供出来,专门创建对象实例的工厂接口!

它定义了四个方法,并且在MyBatis当中有多个实现类

1
2
3
4
5
6
7
8
public interface ObjectFactory{
default void setProperties(Properties properties){
// NOP
}
<T> T creat(Class<T> type);
<T> T creat(Class<T> type,List<Class<?>> constructorArgTypes,List<Object> constructorArgs);
<T> BOOLEAN ISCOLLECTION(CLASS<T> TYPE);
}

1.7 plugins

预留的插件接口

1.8 environments

在MyBatis中,environments标签用于配置数据源以及事务管理器。environments标签可以包含多个environment子标签,每个environment子标签用于定义一个数据源及其对应的事务管理器。一个environment标签包含三个子标签:

  1. transactionManager:用于配置事务管理器,可以使用JDBC、MANAGED等事务管理器
  2. dataSource:用于配置数据源,可以使用JNDI、UNPOOLED、POOLED等数据源
  3. id:用于指定环境的唯一标识符

使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>

1.9 mappers

标签配置的是映射器,也就是Mapper.xml的路径

这里配置的目的是让MyBatis启动的时候去扫描这些映射器,创建映射关系

可以使用相对路径,也能绝对路径配置,其实主要就两种配置方式,分别如下:

mapper标签的resource属性中指定Mapper XML文件的路径

1
2
3
<mappers>
<mapper resource="com/example/MyMapper.xml"/>
</mappers>

mapper标签的class属性中指定Mapper接口的全限定类名

1
2
3
<mappers>
<mapper class="com.example.MyMapper"/>
</mappers>

2.映射文件

上面1.9就是用来指定要扫描的映射文件的,那么映射文件什么格式,怎么用呢?

MyBatis的映射文件主要用于定义SQL语句、参数映射以及结果映射等信息。MyBatis的映射文件是基于XML格式的,每个映射文件都是以一个顶级元素开始,下面是MyBatis映射文件的顶级元素及其用途

  • mapper元素:这是映射文件的根元素,用于指定Mapper接口的全限定类名或者Mapper XML文件的位置。在mapper元素中可以包含多个子元素,包括selectinsertupdatedeleteresultMap
  • select元素:用于定义查询SQL语句以及参数映射和结果射等信息。
  • insert元素:用于定义插入SQL语句以及参数映射等信息
  • update元素:用于定义更新SQL语句以及参数映射等信息
  • delete元素:用于定义删除SQL语句以及参数映射等信息
  • sql元素:用于定义可重用的SQL片段,这些SQL片段可以在其他SQL语句中通过include元素进行引用
  • parameterMap元素:用于定义输入参数的映射关系,已逐渐废弃
  • resultMap元素:用于定义输出结果的映射关系,最常用也是最强大的功能
  • cache元素:用于配置缓存策略,是否开启二级缓存

映射文件中的这些顶级元素可以按照需要进行组合,以定义不同的SQL语句、参数映射以及结果映射等信息。在使用MyBatis时,通常需要根据具体的需求选择相应的元素进行配置,从而实现数据的持久化操作。

3.动态sql

动态SQL是MyBatis中一个非常强大的特性,可以根据不同的条件动态生成SQL语句,可以大大提高SQL语句的重用性和可维护性。MyBatis提供了多个标签来实现动态SQL,下面介绍几个常用的标签及其用法

1)if标签:用于判断某个条件是否成立,如果成立,则执行if标签内的SQL语句。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
bashCopy code
<select id="findUsers" resultType="User">
SELECT * FROM user
WHERE 1 = 1
<if test="name != null">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</select>

2)choose标签:类似于Java中的switch语句,根据某个条件选择不同的SQL语句执行。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vbnetCopy code
<select id="findUsers" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="orderBy == 'name'">
ORDER BY name
</when>
<when test="orderBy == 'age'">
ORDER BY age
</when>
<otherwise>
ORDER BY id
</otherwise>
</choose>
</where>
</select>

3)foreach标签:用于循环遍历集合或数组,生成对应的SQL语句。

示例代码:

1
2
3
4
5
6
7
sqlCopy code
<delete id="deleteUsers">
DELETE FROM user WHERE id IN
<foreach item="id" collection="ids" separator="," open="(" close=")">
#{id}
</foreach>
</delete>

4)set标签:用于更新操作,生成SET关键字后面的动态内容。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
bashCopy code
<update id="updateUser" parameterType="User">
UPDATE user
<set>
<if test="name != null">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
</set>
WHERE id = #{id}
</update>

以上标签是MyBatis中常用的动态SQL标签,通过这些标签的灵活组合,可以动态生成复杂的SQL语句,满足各种不同的查询、更新、删除等操作的需求

4.延迟加载

MyBatis的延迟加载(Lazy Loading)是指在查询数据时,只有在需要使用某个属性时才去加载该属性的值,而不是在查询时将所有属性都加载出来。这样可以提高查询效率和减少内存消耗。

MyBatis提供了两种延迟加载方式:

1)延迟加载属性(Lazy Property Loading):在查询时,只查询部分属性,需要使用某个属性时,再去查询该属性的值。这种方式需要在映射文件中配置属性的延迟加载方式。

示例代码:

1
2
3
4
5
6
pythonCopy code
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<association property="dept" column="dept_id" select="findDeptById" lazyLoadingEnabled="true"/>
</resultMap>

2)延迟加载集合属性(Lazy Collection Loading):在查询时,只查询主实体,需要使用某个关联实体时,再去查询该关联实体的值。这种方式需要在映射文件中配置集合属性的延迟加载方式。

示例代码:

1
2
3
4
5
6
pythonCopy code
<resultMap id="deptMap" type="Department">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="users" column="id" select="findUsersByDeptId" lazyLoadingEnabled="true"/>
</resultMap>

需要注意的是,MyBatis的延迟加载需要在SQLSession中开启延迟加载功能,即在配置文件中设置defaultLazyLoadingEnabled属性为true,或者在创建SQLSession时通过参数指定。

同时,延迟加载也会带来一些问题,例如多线程环境下可能会出现问题,需要开发人员根据具体情况进行选择和配置

四、源码

1.SqlSessionFactory

image-20230423212040325

每次操作数据库,都需要创建一个会话SqlSession,而它就由一个全局的SqlSessionFactory来创建

那SqlSessionFactory由谁来创建呢?都有哪些参数和作用呢?

SqlSessionFactory由SqlSessionFactoryBuilder().builder()来创建,很明显的建造者模式

在这个builder内,使用xmlConfigBuilder完成了全局配置文件的加载与解析

同时,在这个xmlConfigBuilder内创建了Configuration对象,它包含type别名,全局配置文件等配置信息

最终,返回的SqlSessionFactory主要包含两部分内容,一个是Configuration配置信息,一个是DefaultSqlSessionFactory

2.XMLConfigBuilder

XMLConfigBuilder是MyBatis中的一个重要类,它负责解析MyBatis配置文件,生成MyBatis的全局配置对象Configuration。在MyBatis启动时,XMLConfigBuilder会读取配置文件,并将其解析为Java对象。在解析过程中,它会对配置文件中的各种元素进行验证和解析,并将解析结果封装成Configuration对象的属性。

以下是XMLConfigBuilder的主要功能和解析过程:

  1. 首先,XMLConfigBuilder会创建Configuration对象,并通过XPathParser类读取配置文件。XPathParser类是一个轻量级的XML解析器,它可以方便地处理XML配置文件。
  2. 然后,XMLConfigBuilder会对配置文件中的各个元素进行解析。它会根据配置文件中的标签和属性,创建相应的Java对象,并将它们设置为Configuration对象的属性。例如,XMLConfigBuilder会解析<properties>元素,创建Properties对象,并将其设置为Configuration对象的属性。
  3. 在解析过程中,XMLConfigBuilder还会对配置文件中的各种元素进行验证和检查,以确保它们的合法性。例如,XMLConfigBuilder会验证<mapper>元素的resourceurl属性是否存在,是否正确等。
  4. 在解析完成后,XMLConfigBuilder会将Configuration对象返回给SqlSessionFactoryBuilder类,并将其用于创建SqlSessionFactory对象。此时,Configuration对象已经包含了所有的配置信息,包括数据库连接信息、类型别名、映射器配置等。

总之,XMLConfigBuilder是MyBatis中的一个重要类,它负责解析MyBatis配置文件,并生成MyBatis的全局配置对象Configuration。在解析过程中,它会对配置文件中的各种元素进行验证和解析,并将解析结果封装成Configuration对象的属性。通过XMLConfigBuilder,MyBatis可以方便地读取和解析配置文件,并将其转换成Java对象

3.核心流程

在MyBatis当中,从配置文件加载,到最后执行sql语句,整体的流程如下所示

  • 获取SqlSessionFactory(工厂)对象
  • 获取SqlSession(会话)对象
  • 获取接口的代理对象(MapperProxy)
  • SQL语句执行(代理对象的invoke方法最后执行jdbc的方法)