MyBatis 框架使用

1,929 阅读12分钟

文中涉及到的代码地址:MyBatis示例代码

一、MyBatis框架简介

1. 什么是框架?

框架(Framework)是整个或者部分系统的可重用设计,从应用角度看框架可以被当为一组抽象构件及构件实例间交互的方法;在应用目的的角度来看框架是一种可以被应用开发者定制的应用骨架。总结来讲,框架是一种半成品应用,是一种组件,可以辅助你开发自己的系统。

框架解决的问题

框架要解决的最重要的一个问题是技术整合的问题,在J2EE的框架中,有着各种各样的技术,不同的软件企业需要从J2EE中选择不同的技术,这就使得软件企业最终的应用依赖于这些技术,技术自身的复杂性和技术的风险性将会直接对应用造成冲击。而应用是软件企业的核心,是竞争力的关键所在,因此应该将应用自身的设计和具体的实现技术解耦。这样,软件企业的研发将集中在应用的设计上,而不是具体的技术实现,技术实现是应用的底层支撑,它不应该直接对应用产生影响。

框架一般处在低层应用平台(如J2EE)和高层业务逻辑之间的中间层,框架实现了部分功能,并能很好的进行业务逻辑和功能的整合。

软件开发中的分层思想

为了方便对软件进行开发和管理,实现软件工程中的“高内聚,低耦合”,将开发中的问题划分开各个解决,使得应用易于控制,易于扩展,易于资源管理,常见分层思想-MVC设计思想。通过分层更好的实现了各个部分的职责,再细分化出不同的框架,分别解决各层关注的问题。

分层开发中的常见框架

① 解决数据的持久层问题的框架--MyBatis

② 解决web层问题的框架--springMVC

③ 解决技术整合问题得框架--spring

2. MyBatis框架简介

MyBatis是一个优秀的基于Java的持久层框架,内部封装了jdbc,开发者仅需关注sql语句本身,不需要花更多的精力去处理加载驱动、创建连接、创建statement等繁杂的过程。

MyBatis通过注解或xml配置的方式将需要执行的statement配置起来,并通过Java对象和statement中sql的动态参数进行映射生成最终需要执行的sql语句,最后由Mybatis框架执行sql语句并将结果映射为Java对象并返回。

Mybatis采用 ORM思想决了实体类和数据库映射的问题,对jdbc进行了封装,屏蔽了jdbc底层API的访问细节,让我们不用和jdbc API打交道,也能完成数据库的访问操作。

强烈推荐阅读:MyBatis官方中文文档

二、MyBatis实现基本CRUD操作

1. 基本配置流程:

① 创建Maven工程,并添加坐标导入jar包

② 书写与数据库表映射的实体类

③ 书写持久层接口

④ 书写持久层接口的映射文件或在持久层接口书写注解

⑤ 书写配置文件

2. 持久层映射配置文件的使用

2.1 映射配置文件中的标签

2.1.1 常见的标签

<resultMap>:用于配置返回的数据集,建立查询结果列名和实体类属性名之间的对应的关系。

id属性:指定名称

type属性:指定实体类名

  • 内部标签 <id>:用于配置与数据库表中的关键字对应的字段。
  • 内部标签 <result>:用于配置与数据库中的非关键字段对应的字段。
    • property属性:用于指定实体类的属性
    • colum属性:用于指定数据库列的属性

<sql> :用于书写通用的SQL语句,可以搭配 <include> 标签对通用SQL语句进行抽取。

2.1.2 与查询相关的标签

<insert> :用于书写数据库插入相关的操作

<delete> :用于书写数据库删除相关的操作

<update> :用于书写数据库更新相关的操作

<select> :用于书写数据库查询相关的操作

标签的属性

  • id属性:要和持久层接口的方法名相同,否则找不到映射的方法。
  • resultType属性:指定结果集的类型
  • resultMap属性:指定结果集
  • parameterType属性:指定传入参数的类型。如果传入的是自定义的类需要写类的全限定类名
  • parameterMap属性:指定传入参数

此处列出的标签并不全,更多关于映射标签的介绍建议参考MyBatis官方的文档:MyBatis XML映射器

2.2 在标签内书写SQL语句

在对应的标签内部书写SQL语句,支持SQL语句中的联合查询、聚合查询等复杂查询。

sql语句中的 #{} 字符:表示占位符,相当于jdbc中的 ? ,用于执行SQL语句时替换实际数据,具体的数据

由大括号里的内容决定。

#{} 中内容的写法:如果为基本数据类型可以随意书写,如果是对象类型的则需要使用ognl表达。

ognl表达式:是Apache提供的一种表达式语言,全称是:object Graphic Navigation Language(对象导航语言),它按照一定的语法格式来获取数据,语法格式为:#{对象.对象} 的方式

#{}${} 的区别:

#{} 表示一个占位符号:通过 #{} 可以实现向prepareStatement向占位符中设置值,自动进行Java类型和jdbc类型转换,#{} 可以防止SQL注入。#{} 可以接收简单类型值或pojo属性值。如果parameterType传输单个简单类型值,#{} 括号中可以是value或者其他名称。

${} 表示拼接字符串:通过 ${} 可以将parameterType传入的内容拼接在SQL中且不进行jdbc类型的转换,${} 可以接收简单类型值或pojo属性值,如果parameterType传输简单类型值,${} 括号中只能是value。

映射文件使用注意事项:

  • 持久层接口和持久层接口的映射必须在相同的包结构下
  • 持久层映射配置中mapper标签的namespace属性取值必须是持久层接口的全限定类名

3. MyBatis配置文件的使用

配置信息内容和顺序

-properties(属性):配置数据库连接文件
	--property:单个配置数据库连接

-settings(全局配置参数):配置mybatis相关
	--setting(子配置参数)

-typeAliases(类型别名):配置实体类别名
	--typeAliase
	--package(实体类包名)

-typeHandlers(类型处理器)

-objectFactory(对象工厂)

-plugging(插件)

-environments(环境集合属性对象)
	--environment(环境子属性对象)
		--transactionManager(事务管理)
		--dataSource(数据源):引用properties配置中的内容

-mappers(映射器):指定映射文件位置
	--mapper(指定映射类)
		--resource(相对路径资源)
		--class(多用于配置注解SQL):需要mapper接口名称和mapper映射文件名称相同,且在一个路径中。
	--package(指定映射所在的包):需要mapper接口名称和mapper映射文件名称相同,且在一个路径中。

更多关于MyBatis配置文件的的内容建议查看MyBatis官方文档:MyBatis XML配置

4. 示例代码:

前期准备:执行SQL语句生成user表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名称',
  `birthday` datetime(0) NULL DEFAULT NULL COMMENT '生日',
  `sex` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '性别',
  `address` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 72 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (41, '老王', '2018-02-27 17:47:08', '男', '北京');
INSERT INTO `user` VALUES (42, '小二王', '2018-03-02 15:09:37', '女', '北京朝阳区');
INSERT INTO `user` VALUES (43, '小二王', '2018-03-04 11:34:34', '女', '北京朝阳区');
INSERT INTO `user` VALUES (45, 'Tom', '2018-03-04 12:04:06', '男', '北京朝阳区');
INSERT INTO `user` VALUES (46, '老王', '2018-03-07 17:37:26', '男', '北京');
INSERT INTO `user` VALUES (48, 'Bruce', '2020-09-14 14:54:47', '男', '山东威海');
INSERT INTO `user` VALUES (56, 'Tom', '2020-09-14 14:18:08', '男', '山东烟台');
INSERT INTO `user` VALUES (57, 'Jerry', '2020-09-14 14:15:51', '女', '山东烟台');
INSERT INTO `user` VALUES (60, 'Big M', '2020-09-17 11:28:08', '男', '啥不懂');
INSERT INTO `user` VALUES (67, '王五', '2020-09-15 08:41:57', '男', '浙江温州');
INSERT INTO `user` VALUES (68, 'update', '2020-09-17 16:13:24', '男', '北京市海淀区');
INSERT INTO `user` VALUES (69, 'test', '2020-09-17 15:59:56', '女', '北京市海淀区');
INSERT INTO `user` VALUES (70, 'test', '2020-09-17 16:07:22', '女', '北京市海淀区');
INSERT INTO `user` VALUES (71, 'test', '2020-09-17 16:07:35', '女', '北京市海淀区');

SET FOREIGN_KEY_CHECKS = 1;

① 创建Maven工程,并添加坐标

<!-- 配置编译插件 -->
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.7.0</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>
<!-- 设置打成jar包 -->
<packaging>jar</packaging>
<!-- 配置三方依赖库 -->
<dependencies>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.5</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.49</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13</version>
        <scope>test</scope>
    </dependency>
</dependencies>

② 书写与数据对应的实体类User

public class User {
    private Integer id;

    private String username;

    private Date birthday;

    private String sex;

    private String address;

    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;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", birthday=" + birthday +
                ", sex='" + sex + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

③ 书写持久层接口IUserDao

public interface IUserDao {

    /**
     * 获取所有的账户
     * @return
     */
    List<User> getUsers();

    /**
     * 保存账户
     * @param user
     */
    void saveUser(User user);

    /**
     * 更新账户
     * @param user
     */
    void updateUserById(User user);

    /**
     * 通过id查询用户
     * @param userId
     * @return
     */
    User getUserById(Integer userId);

    /**
     * 通过id删除用户
     * @param userId
     */
    void deleteUserById(Integer userId);

    /**
     * 通过名称查询用户
     * @param name
     * @return
     */
    List<User> getUserByName(String name);

    /**
     * 查询用户总数
     * @return
     */
    int getUserTotal();
}

④ 书写持久层接口的映射文件IUserDao.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 namespace="cn.bruce.dao.IUserDao">

    <!-- 建立查询结果列名和实体类属性名之间的对应关系 -->
    <resultMap id="userMap" type="user">
        <!-- 关键字对应 -->
        <id property="userId" column="id"/>
        <!-- 非关键字对应 -->
        <result property="userName" column="username"/>
        <result property="userSex" column="sex"/>
        <result property="userBirthday" column="birthday"/>
        <result property="userAddress" column="address"/>
    </resultMap>

    <!-- 配置查询相关的语句 -->

    <!-- 查询所有用户 -->
    <select id="getUsers" resultMap="userMap">
        select * from user;
    </select>

    <!-- 保存用户 -->
    <insert id="saveUser" parameterType="user">
        <selectKey keyProperty="userId" keyColumn="id" resultType="int" order="AFTER">
            select last_insert_id();
        </selectKey>
        insert into user(username, sex, birthday, address) value (#{userName}, #{userSex}, #{userBirthday},
        #{userAddress})
    </insert>

    <!-- 更新操作 -->
    <update id="updateUserById" parameterType="user">
        update user set username = #{userName}, sex = #{userSex}, birthday = #{userBirthday}, address =
        #{userAddress} where id = #{userId}
    </update>

    <!-- 根据id查询用户 -->
    <select id="getUserById" parameterType="int" resultMap="userMap">
        select * from user where id = #{userId}
    </select>

    <!-- 根据id删除用户 -->
    <delete id="deleteUserById" parameterType="java.lang.Integer">
        delete from user where id = #{userId}
    </delete>

    <!-- 根据名称查询 -->
    <select id="getUserByName" parameterType="string" resultMap="userMap">
        select * from user where username like #{userName}
    </select>

    <!-- 获取用户记录总条目数 -->
    <select id="getUserTotal" resultType="int">
        select count(id) from user
    </select>
</mapper>

⑤ 书写jdbc连接配置文件jdbcConfig.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/base_crud?characterEncoding=utf-8&useSSL=false
jdbc.username=root
jdbc.password=*****

⑥ 书写MyBatis配置文件mybatis-config.xml

<?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>
    <!-- 三种配置方式效果相同,resource属性通过查找相对路径得到jdbc连接,url属性通过绝对路径获取jdbc连接,也可以通过配置property标签的方式指定jdbc连接 -->
    <!-- 第一种方式,通过resource属性配置相对路径获取jdbc连接 -->
    <properties resource="jdbcConfig.properties">
        <!-- 第二种方式,通过url属性配置绝对路径获取jdbc连接 -->
            <!--url="file:///E:/workspace/workspace_idea03/demo-mybatis/day02_mybatis01_CRUD/src/main/resources/jdbcConfig.properties">-->
        <!-- 第三种方式,配置property标签的方式配置jdbc连接 -->
        <!--<property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/base_crud"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>-->
    </properties>

    <!-- typeAliases用于配置别名,只能配置domain中类的别名 -->
    <typeAliases>
        <!-- typeAlias用于配置别名。type属性配置实体类全限定类名。alias属性指定别名,指定别名后则区分大小写 -->
        <!--<typeAlias type="cn.bruce.domain.User" alias="user"></typeAlias>-->

        <!-- 用于指定配置别名的包,当指定后,该包下的实体类都会注册别名,并且类名就是别名,并且不区分大小写 -->
        <package name="cn.bruce.domain"/>
    </typeAliases>

    <!-- 配置环境,配置jdbc连接和事务的控制 -->
    <environments default="mysql">
        <environment id="mysql">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 配置映射文件 -->
    <mappers>
        <mapper resource="dao/IUserDao.xml"></mapper>
        <!-- package标签用于指定dao接口所在的包,当指定以后就不需要再写mapper以及resource或者class属性了,但是要求映射文件和dao接口处于同一个包 -->
        <!--<package name="cn.bruce.dao"/>-->
    </mappers>
</configuration>

⑦ 配置日志log4j

# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE            debug   info   warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE

# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE

# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%t] %-5p %30.30c %x - %m\n

# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=E:test.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%t] %-5p %30.30c %x - %m\n

⑧ 书写测试类进行测试

public class IUserDaoTest {

    private InputStream in;
    private SqlSession sqlSession;
    private IUserDao userDao;

    /**
     * 在测试执行方法执行前执行
     * @throws IOException
     */
    @Before
    public void init() throws IOException {
        // 1.读取配置文件,生成字节输入流
        in = Resources.getResourceAsStream("mybatis-config.xml");
        // 2.获取SqlSessionFactory
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        // 3.获取SqlSession对象
        sqlSession = factory.openSession();
        // 4.获取dao对象
        userDao = sqlSession.getMapper(IUserDao.class);
    }

    /**
     * 用于在测试方法执行后执行
     * @throws IOException
     */
    @After
    public void destroy() throws IOException {
        // 提交事务
        sqlSession.commit();
        // 释放连接
        sqlSession.close();
        in.close();
    }

    /**
     * 测试查询所有用户
     */
    @Test
    public void testGetUsers() {
        List<User> users = userDao.getUsers();
        for (User user : users) {
            System.out.println(user);
        }
    }

    /**
     * 测试根据id获取用户
     */
    @Test
    public void testGetUserById() {
        User user = userDao.getUserById(41);
        System.out.println(user);

    }

    /**
     * 测试更新用户操作
     */
    @Test
    public void testUpdateUser() {
        User user = new User();
        user.setUserId(48);
        user.setUserName("Bruce");
        user.setUserSex("男");
        user.setUserBirthday(new Date());
        user.setUserAddress("山东济南");
        userDao.updateUserById(user);
    }

    /**
     * 测试保存账户
     */
    @Test
    public void testSaveUser() {
        User user = new User();
        user.setUserName("Bruce");
        user.setUserSex("男");
        user.setUserBirthday(new Date());
        user.setUserAddress("山东济南");
        userDao.saveUser(user);
        System.out.println(new Date());
    }


    /**
     * 测试根据id删除用户
     */
    @Test
    public void testDeleteUserById() {
        userDao.deleteUserById(59);
    }

    /**
     * 测试根据名称查询用户
     */
    @Test
    public void testGetUserByName() {
        List<User> users = userDao.getUserByName("Bruce");
        for (User user : users) {
            System.out.println(user);
        }
    }

    /**
     * 获取用户总条目数
     */
    @Test
    public void testGetUserTotal() {
        int userTotal = userDao.getUserTotal();
        System.out.println("用户总数为:" + userTotal);
    }

}

本文后续的内容基本都是围绕这些代码进行展开的,建议动手试一下

5. 关于log4j

5.1 log4j简介

Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

5.2 log4j的使用

① 配置Maven坐标,引入log4j

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

② 书写配置文件

# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE            debug   info   warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE

# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE

# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%t] %-5p %30.30c %x - %m\n

# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=E:test.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%t] %-5p %30.30c %x - %m\n

更多关于log4j的使用内容,后续会更新的,敬请期待。这里放一个我觉得还不错的博客log4j配置

三、MyBatis连接池和事务操作

1. 数据库连接池简介

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。这项技术能明显提高对数据库操作的性能。

2. MyBatis中数据库连接池

2.1 MyBatis连接池分类

  • UNPOOLED:不使用连接池的数据源

  • POOLED:使用连接池的数据源

  • JNDI:使用JNDI实现的数据源

结构如下所示:

image-20200924163926564

继承关系图:

image-20200925110938537

MyBatis内部分别定义实现了 java.sql.DataSource 接口的 UnpooledDataSource 类和 PooledDataSource 类来表示 UNPOOLEDPOOLED 类型的数据源

UnpooledDataSource 类和 PooledDataSource 都实现了 java.sql.DataSource 接口,并且 PooledDataSource 持有一个 UnpooledDataSource 的引用,当 PooledDataSource 需要创建 java.sql.Connection 实例对象时,还是通过 UnPooledDataSource 来创建,PooledDataSource 只提供一种缓存连接池机制。

一般常用的是 POOLED 数据源,使用数据源的目的是为了更好的管理数据库连接,通过数据库连接池管理连接。

2.2 MyBatis中数据源的配置

MyBatis在初始化时,会根据 <DataSource>type 属性来创建对应的数据源 DataSource,即:

type = "POOLED" :MyBatis会创建 PooledDataSource 实例。采用传统的 javax.sql.DataSource 规范中的连接池,mybatis中有针对规范的实现。

type = "UNPOOLED":MyBatis会创建 UnpooledDataSource 实例。采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。

type = "JNDI":MyBatis会从JNDI服务上查找 DataSource 实例,然后返回使用。采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。注意:如果不是web或者maven的war工程,是不能使用的。

2.3 MyBatis中DataSource的存取

MyBatis是通过工厂模式来创建数据源 DataSource 对象的,Mybatis定义了抽象工厂接口:org.apache.ibatis.datasource.DataSourceFactory ,通过 getDataSource() 方法返回数据源 DataSource

DataSourceFactory 源码:

package org.apache.ibatis.datasource;

import java.util.Properties;
import javax.sql.DataSource;

public interface DataSourceFactory {
    void setProperties(Properties var1);

    DataSource getDataSource();
}

MyBatis创建了 DataSource 实例后,会将其放到 Configuration 对象内的 Environment 对象中,供以后调用。

2.4 MyBatis中连接获取过程

当我们需要创建 SqlSession 对象并需要执行SQL语句时,此时MyBatis才会调用 dataSource 对象来创建 java.sql.Connection 对象。也就是说,java.sql.Connection 对象的创建一直延迟到执行SQL语句的时候。

PooledDataSource 工作原理

image-20200925114402766

3. MyBatis的事务控制

3.1 数据库事务

数据库事务是指作为单个逻辑工作单元执行的一系列操作,这些操作要么全做要么全不做,是一个不可分割的工作单位。

数据库事务的特性

  1. 原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。

  2. 一致性(Consistency):几个并行执行的事务,其执行结果必须与按某一顺序 串行执行的结果相一致。

  3. 隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。

  4. 持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。

事务的ACID特性是由关系数据库系统(DBMS)来实现的,DBMS采用日志来保证事务的原子性、一致性和持久性。日志记录了事务对数据库所作的更新,如果某个事务在执行过程中发生错误,就可以根据日志撤销事务对数据库已做的更新,使得数据库回滚到执行事务前的初始状态。

对于事务的隔离性,DBMS是采用锁机制来实现的。当多个事务同时更新数据库中相同的数据时,只允许持有锁的事务能更新该数据,其他事务必须等待,直到前一个事务释放了锁,其他事务才有机会更新该数据。

数据库读取时会出现的问题

Dirty reads:读脏数据。

也就是说,比如事务A的未提交(还依然缓存)的数据被事务B读走,如果事务A失败回滚,会导致事务B所读取的的数据是错误的。

non-repeatable reads:数据不可重复读。

比如事务A中两处读取数据price的值。在第一读的时候,price是100,然后事务B就把price的值改成 200;事务A再读一次,结果就发现,price竟然就变成200了,造成事务A数据混乱。

phantom reads:幻象读数据。

这个和non-repeatable reads相似,也是同一个事务中多次读不一致的问题。但是 non-repeatable reads 的不一致是因为他所要取的数据值被改变了(比如price)而 phantom reads 所要读的数据的不一致却是他的条件数据集发生变化了。

比如:执行 Select account.id where account.name="Bruce*",第一次读去了6个符合条件的id;第二次读取的时候,由于事务B把一个帐号的名字由"dd"改成"Bruce1",结果取出来了7个数据。

不可重复读的重点是修改:同样的条件,两次读发现值不一样;

幻读的重点在于新增或者删除:同样的条件,两次读发现得到的记录数不一样

数据库的隔离级别

ISOLATION_READ_UNCOMMITTED 就是一个事务可以读取另一个未提交事务的数据。会出现脏读、不可重复读、幻读(隔离级别最低,但并发性高)

ISOLATION_READ_COMMITTED 就是一个事务要等另一个事务提交后才能读取数据,解决脏读问题。会出现不可重复读、幻读问题(锁定正在读取的行,适用于大多数系统,Oracle默认级别)

ISOLATION_REPEATABLE_READ 就是在开始读取数据(事务开启)时,不再允许修改操作,解决不可重复读问题。会出现幻读问题(锁定所读的所有行,MYSQL默认级别)

ISOLATION_SERALZABLE 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。(锁整表)

事务隔离级别由上到下依次提升,隔离级别越高,越能保证数据的完整性和一致性。但对数据库性能的消耗依次增加,并发执行效率依次下降。

大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle

少数数据库默认隔离级别为:Repeatable Read 比如:MySQL InnoDB

3.2 MyBatis的事务控制

Mybatis框架是对JDBC的封装,所以MyBatis框架的事务控制方式,也是通过JDBC的 setAutoCommit() 方法设置事务提交方式的。

MyBatis的事务提交方式,本质上就是通过调用JDBC的 setAutoCommit() 方法来实现事务控制。通过sqlsession对象的commit方法和rollback方法实现事务的提交和回滚。

关于MyBatis中连接池和事务的控制的更多内容,建议查看Mybatis官方文档:Mybatis Java API

四、MyBatis动态SQL

1. 动态SQL简介

通过MyBatis提供的标签进行动态的SQL语句拼接,解决以往使用JDBC和其他类似框架,根据不同的条件使用字符串拼接SQL语句时的复杂和不方便。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号等问题。通过MyBatis动态SQL可以方便的解决这些问题,让代码变得更加简洁易读。

2. 动态SQL标签介绍

<if>:标签内条件符合则拼接SQL语句,否则不拼接。<if> 标签中的test属性写的是对象的属性名,如果是包装类对象要使用OGNL表达式的写法

<where>:当标签内元素符合条件的情况下MyBatis会插入 WHERE 子句。而且,若子句的开头为 ANDOR,where 元素也会将它们去除。

<foreach>:允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符。

注:此处列出的动态SQL标签只是常用的,更加具体的介绍请参考MyBatis官方文档:MyBatis 动态SQL

3. 动态SQL使用

使用 <sql> 标签提取SQL语句

<sql id="getUsers">
    select * from user
    order by birthday desc
</sql>

<!-- 查询所有,sql标签需要配合include标签使用 -->
<select id="getUsers" resultMap="userMap">
    <include refid="getUsers"></include>
</select>

使用 <where> 标签和 <if> 标签实现SQL语句条件拼接

<select id="getUserByCondition" parameterType="cn.bruce.domain.User" resultMap="userMap">
    select * from user
    <where>
        <if test="userName != null">
            and username like '%${userName}%'
        </if>
        <if test="userSex != null">
            and sex = #{userSex}
        </if>
        <if test="userAddress != null">
            and address like '%${userAddress}%'
        </if>
    </where>
    order by birthday desc
</select>

使用 <where><foreache> 标签实现查询结果的遍历

<!-- 根据queryVo中的id集合实现查询用户列表 -->
<select id="getUserByIds" parameterType="queryVo" resultMap="userMap">
    <include refid="getUsers"></include>
    <where>
        <if test="ids != null and ids.size() > 0">
            <foreach collection="ids" open="and id in (" close=")" item="userId" separator=",">
                #{userId}
            </foreach>
        </if>
    </where>
</select>

五、MyBatis多表操作

前面的操作我们针对的是单表操作,返回的结果集只是一个数据库表结构的内容,但是实际的开发业务中难免会遇到一对多和多对多的情况,这些情况就需要我们对MyBatis框架进行进一步的学习和理解,也就引出了我们这一节的内容--多表操作。

1. 多表一对多操作

一对多的举例:用户和账户之间的关系,一个用户拥有多个账户,一个账户只对应一个用户

方式一:定义专门的po类作为输出类型,其中定义了SQL查询结果集所有字段。

方式二:使用resultMap定义专门的resultMap映射查询结果,同时在实体类加入需要关联实体类的实例对象,并书写 get() set() 方法。

代码示例:

此处采用方式二实现,次数的代码和上面实现基本CRUD代码基本相似,仅展示不同的地方。

User实体类

// 此处省略get()、set()方法和toString()方法
public class Account {
    private Integer id;
    private Integer uid;
    private Double money;
    
    //从表实体应该包含一个主表实体的对象引用
    private User user;
}

Account实体类

// 此处省略get()、set()方法和toString()方法
public class User implements Serializable {
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;

    //一对多关系映射:主表实体应该包含从表实体的集合引用
    private List<Account> accounts;
}

持久层接口映射文件

IUserDao.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 namespace="cn.bruce.dao.IUserDao">
    <resultMap id="userAccountMap" type="user">
        <!-- 配置user实体类和列的对应 -->
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="birthday" column="birthday"/>
        <result property="sex" column="sex"/>
        <result property="address" column="address"/>

        <!-- 配置accounts实体类和列的对应 -->
        <collection property="accounts" ofType="account">
            <id property="id" column="id"/>
            <result property="uid" column="uid"/>
            <result property="money" column="money"/>
        </collection>
    </resultMap>

    <!-- 查询全部用户的全部账户 -->
    <select id="getUsers" resultMap="userAccountMap">
        select * from user u left outer join account a on u.id = a.uid
    </select>

    <!-- 根据id查询用户账户 -->
    <select id="getUserById" parameterType="int" resultMap="userAccountMap">
        select * from user where id = #{userId}
    </select>
</mapper>

IAccountDao.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 namespace="cn.bruce.dao.IAccountDao">

    <!-- 自定义封装user和account的resultMap -->
    <resultMap id="accountUserMap" type="account">
        <id property="id" column="id"/>
        <result property="uid" column="uid"/>
        <result property="money" column="money"/>
        <!-- 配置一对一关系映射:封装user的内容 -->
        <association property="user" column="uid" javaType="user">
            <id property="id" column="id"/>
            <result property="username" column="username"/>
            <result property="birthday" column="birthday"/>
            <result property="sex" column="sex"/>
            <result property="address" column="address"/>
        </association>
    </resultMap>

    <!-- 查询全部用户 -->
    <select id="getAccounts" resultMap="accountUserMap">
        select u.*, a.id as aid, a.uid, a.money
        from account a, user u
        where u.id = a.uid
    </select>

    <!-- 查询账户中包含的用户名和地址信息 -->
    <select id="getAccountWithMore" resultType="AccountUser">
        select a.*, u.username, u.address
        from account a, user u
        where a.uid = u.id
    </select>
</mapper>

2. 多表多对多操作

多对多关系举例:用户和角色之间的关系,一个用户对应多个角色,一个角色也对应着多个用户。数据库中表现多对多的关系需要借助中间表,因此在查询的时候也需要进行关联查询。

解决方案和一对多相似,在用户表的角度看每个用户和角色之间的关系时一对多的关系,在角色的角度看每个角色和用户之间的关系也是一对多,因此可以在用户和角色对应的实体类中分别加入角色和用户的数组用来存储查询返回的结果集。

示例代码:

User实体类

public class User {
    private Integer userId;
    private String userName;
    private Date userBirthday;
    private String userSex;
    private String userAddress;
    /**
     * 多对多关系映射配置,同一个用户对应多个角色
     * @return
     */
    private List<Role> roles;
}    

Role实体类

// 此处省略get()、set()和toString()方法
public class Role {
    private Integer roleId;
    private String roleName;
    private String roleDesc;

    /**
     * 多对多关系映射配置,同一角色对应多个用户
     */
    private List<User> users;
}

持久层接口映射文件

IUser.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 namespace="cn.bruce.dao.IUserDao">
    <!-- 定义resultMap -->
    <resultMap id="userMap" type="cn.bruce.domain.User">
        <id property="userId" column="id"/>
        <result property="userName" column="username"/>
        <result property="userBirthday" column="birthday"/>
        <result property="userSex" column="sex"/>
        <result property="userAddress" column="address"/>
        <!-- 配置角色集合映射 -->
        <collection property="roles" ofType="role">
            <id property="roleId" column="id"/>
            <result property="roleName" column="role_name"/>
            <result property="roleDesc" column="role_desc"/>
        </collection>
    </resultMap>
    
    <!-- 查询所有用户 -->
    <select id="getUsers" resultMap="userMap">
        select u.*, r.id as rid, r.role_name, r.role_desc from user u
        left join user_role ur on u.id = ur.uid
        left join role r on ur.rid = r.id
    </select>

    <!-- 根据id查询用户 -->
    <select id="getUserById" resultType="cn.bruce.domain.User">
        select * from user where id = #{userId}
    </select>
</mapper>

IRole.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 namespace="cn.bruce.dao.IRoleDao">
    <resultMap id="roleMap" type="Role">
        <id property="roleId" column="id"/>
        <result property="roleName" column="role_name"/>
        <result property="roleDesc" column="role_desc"/>
        <collection property="users" ofType="user">
            <id property="userId" column="id"/>
            <result property="userName" column="username"/>
            <result property="userBirthday" column="birthday"/>
            <result property="userSex" column="sex"/>
            <result property="userAddress" column="address"/>
        </collection>
    </resultMap>

    <!-- 查询所有角色 -->
    <select id="getRoles" resultMap="roleMap">
        select u.*, r.id as rid, r.role_name, r.role_desc from role r
        left join user_role ur on r.id = ur.rid
        left join user u on ur.uid = u.id
    </select>
</mapper>

使用SQL语句查询时需要用到数据库表的连接,这里采用的时左外连接。本章涉及到较多与数据库操作相关的内容,关于数据库操作的内容会写博客进行详细说明的,敬请期待。(●'◡'●)

六、JNDI相关

1. JNDI简介

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。两者之间的关键差别是目录服务中对象不但可以有名称还可以有属性(例如,用户有email地址),而命名服务中对象没有属性 。

集群JNDI实现了高可靠性JNDI,通过服务器的集群,保证了JNDI的负载平衡和错误恢复。在全局共享的方式下,集群中的一个应用服务器保证本地JNDI树的独立性,并拥有全局的JNDI树。每个应用服务器在把部署的服务对象绑定到自己本地的JNDI树的同时,还绑定到一个共享的全局JNDI树,实现全局JNDI和自身JNDI的联系。

JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。现在JNDI已经成为J2EE的标准之一,所有的J2EE容器都必须提供一个JNDI的服务。

JNDI可访问的现有的目录及服务有:

DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。

2. JNDI使用

建立Maven工程引入相关的jar包

<!-- 此处指展示不同的Maven坐标,其余的可参考上面的完整代码 -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
</dependency>

<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.2</version>
</dependency>

在webapp文件下创建META-INF目录,在目录下书写context.xml配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<Context>
    <Resource>
        name="jdbc/jndi_mybatis"
        type="javax.sql.DataSource"
        auth="Container"
        maxActive="20"
        maxWait="10000"
        maxIdle="5"
        userName="root"
        password="123456"
        driverClassName="com.mysql.jdbc.Driver"
        url="jdbc:mysql://localhost:3306/base_crud?characterEncoding=utf-8"
    </Resource>
</Context>

修改mybatis配置文件

<?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>
    <typeAliases>
        <package name="cn.bruce.domain"/>
    </typeAliases>

    <!-- 配置mysql环境 -->
    <environments default="mysql">
        <environment id="mysql">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="JNDI">
                <property name="data_source" value="java:comp/env/jdbc/jndi_mybatis"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 指定映射文件 -->
    <mappers>
        <mapper resource="dao/IUserDao.xml"/>
    </mappers>
</configuration>

更多关于JNDI的介绍和使用,感兴趣的同学可以查看这篇博文:JNDI

七、MyBatis缓存相关

1. MyBatis延迟加载

1.1 简介

延迟加载就是在用到数据的时候才进行加载,不需要的时候不加载数据,也称为懒加载。

  • 优点:可以节省资源,每次查询的时候先查询单表,需要数据时才去查询关联表,可以提高数据库的性能。单表查询比关联查询消耗的数据库性能要小得多。
  • 缺点:关联查询的时机被延后了,当需要查询大量数据时,查询结果的返回时间也会增加,可能造成用户的长时间等待,影响用户的体验。

举例:在进行用户和账户之间一对多关系查询时,可以将账户查询操作延后到需要账户信息时。

1.2 使用方式

① 在MyBatis配置文件中配置延迟加载

② 在一对多的关系中,在 <collection> 标签中配置延迟加载策略,在一对一的关系中,配置 <association> 标签来实现延迟加载。

<collection> 标签:用于加载关联的集合对象

  • select 属性:用于指定查询关联表的SQL语句,填写此SQL映射id
  • column 属性:配置查询的主键。用于指定select属性的SQL语句的参数来源。

<association> 标签:用于加载关联的单表

  • select 属性:用于指定查询关联表的SQL语句,填写此SQL映射id
  • column 属性:配置查询的主键。用于指定select属性的SQL语句的参数来源。

代码示例:

修改MyBatis配置文件

<?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>
    <!-- 配置配置文件 -->
    <properties resource="jdbcConfig.properties"></properties>

    <!-- 配置参数 -->
    <settings>
        <!-- 开启懒加载的配置参数 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>

    <!-- 为实体类配置别名 -->
    <typeAliases>
        <package name="cn.bruce.domain"/>
    </typeAliases>

    <!-- 配置环境 -->
    <environments default="mysql">
        <environment id="mysql">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 配置映射文件位置 -->
    <mappers>
        <mapper resource="dao/IUserDao.xml"/>
        <mapper resource="dao/IAccountDao.xml"/>
    </mappers>

</configuration>

配置持久层映射文件

IUser.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 namespace="cn.bruce.dao.IUserDao">

    <!-- 配置restMap -->
    <resultMap id="userMap" type="user">
        <id property="userId" column="id"/>
        <result property="userName" column="username"/>
        <result property="userBirthday" column="birthday"/>
        <result property="userSex" column="sex"/>
        <result property="userAddress" column="address"/>
        <!-- 配置user中的account集合映射 -->
        <collection property="accounts" ofType="account" select="cn.bruce.dao.IAccountDao.getAccountByUid" column="id"/>
    </resultMap>

    <!-- 查询所有 -->
    <select id="getUsers" resultMap="userMap">
        select * from user
    </select>

    <!-- 根据id获取用户名 -->
    <select id="getUserById" parameterType="int" resultType="user">
        select * from user where id = #{userId}
    </select>

</mapper>

IAccount.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 namespace="cn.bruce.dao.IAccountDao">

    <!-- 建立resultMap -->
    <resultMap id="accountMap" type="account">
        <id property="id" column="id"/>
        <result property="uid" column="uid"/>
        <result property="money" column="money"/>
        <!-- 一对一的关系映射:配置封装user的内容
        select属性指定的内容:查询用户的唯一标识:
        column属性指定的内容:用户根据id查询时,所需要的参数的值
        -->
        <association property="user" column="uid" javaType="user" select="cn.bruce.dao.IUserDao.getUserById"/>
    </resultMap>
    
    <!-- 查询所有账户 -->
    <select id="getAccounts" resultMap="accountMap">
        select * from account
    </select>

    <!-- 根据id查询账户 -->
    <select id="getAccountByUid" parameterType="int" resultMap="accountMap">
        select * from account where uid = #{userId};
    </select>
</mapper>

此处仅展示重要代码,完整代码请查看:Mybatis懒加载

2. Mybatis缓存

2.1 简介

MyBatis框架和其他持久层框架一样也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。MyBatis中缓存分为一级缓存和二级缓存。

缓存结构图:

image-20200927165345769

2.1.1 MyBatis中的一级缓存

MyBatis的一级缓存是SqlSession级别的缓存,只要SQL Session没有执行 flush() 或者 close() 方法,一级缓存就存在。

当我们多次发起查询时,第一次查询会发起SQL查询,然后Mybatis会把查询的结果缓存起来,以后在进行查询时不会再发起SQL查询而是直接从缓存中查询数据。当调用SqlSession的修改、删除、添加、commit()close() 等方法时,会清空一级缓存。

调用执行图:

image-20200927170847222

  • 第一次发起査询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库査 询用户信息。
  • 得到用户信息,将用户信息存储到一级缓存中。
  • 如果 sqlSession去执行 commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样 做的目的为了让缓存中存储的是最新的信息,避免脏读。
  • 第二次发起査询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存 中获取用户信息。

2.1.2 MyBatis中的二级缓存

MyBatis中的二级缓存是Mapper级别的缓存,多个SqlSession去操作同一个Mapper映射的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。

缓存结构图:

image-20200927171506613

开启二级缓存后:

  • SqlSession1 去查询用户信息,查询到的用户信息数据会存储到二级缓存中;
  • 如果SqlSession3 去执行相同的mapper映射下的sql,执行commit提交,将会清空该mapper映射下的二级缓存缓存区的数据;
  • SqlSession2 去查询与SqlSession1 相同的用户信息,首先会到缓存中找是否存在数据,如果存在则直接从缓存中取出数据,如果不存在则发起SQL查询;

使用二级缓存注意事项:在使用二级缓存时,所缓存的类一定要实现 java.io.Serializable 接口,这样就可以使用序列化来存储对象。

关于缓存部分的代码比较多,此处就不再展示,详细代码请查看:MyBatis缓存

八、MyBatis注解驱动

1. MyBatis常用注解

@Insert :实现新增操作,相当于 <insert> 标签

@Update :实现更新操作,相当于 <update> 标签

@Delete :实现删除操作,相当于 <delete> 标签

@Select :实现查询操作,相当于 <select> 标签

@Result :实现结果集的封装,相当于 <result><id> 标签

  • id属性:是否为主键字段
  • column属性:与数据库对应的列名
  • property属性:需要配置的属性名
  • one属性:需要使用 @<One> 注解 (@Result (one = @One) ())
  • many属性:需要使用 @Many 注解 (@Result (many = @Many) ())

@Results :可以配合 @Result 一起使用,封装多个结果集。相当于 <resultMap> 标签

@ResultMap :实现引用 @Results 定义的封装

@One :实现一对一结果集封装,相当于 <Assocation> 标签,在注解中用于指定子查询返回单一对象。

  • select属性:指定用于多表查询的sqlmapper
  • fetchType属性:会覆盖全局的配置参数 lazyLoadingEnabled
  • 使用格式: @Result(column = "", property = "", one = @One(select = ""))

@Many :实现一对多结果集封装,相当于 <Collection> 标签,在注解中用于指定子查询返回对象集合。

  • 聚焦元素用来处理一对多的关系。需要指定映射的Java实体类的属性,属性的javaType(一般为ArrayList)但注解中可以不定义。
  • 使用格式:@Result(property = "", column = "", many = @Many(select = ""))

@SelectProvider :实现动态SQL映射

@CacheNameSpace :实现注解二级缓存的应用

2. 注解实现基本CRUD

使用步骤:

  • 书写和数据库对应的实体类
  • 书写数据持久层接口
  • 书写配置文件

代码示例:

持久层接口IUserDao

public interface IUserDao {

    /**
     * 增加新的用户
     * @param user
     */
    @Insert("insert into user (username, birthday, sex, address) values (#{username}, #{birthday}, #{sex}, #{address})")
    void saveUser(User user);

    /**
     * 根据id删除用户
     * @param userId
     */
    @Delete("delete from user where id = #{userId}")
    void deleteUserById(Integer userId);

    /**
     * 修改用户信息
     * @param user
     */
    @Update("update user set username = #{username}, birthday = #{birthday}, sex = #{sex}, address = #{address} where" +
            " id = #{id}")
    void updateUser(User user);

    /**
     * 获取全部用户
     * @return
     */
    @Select("select * from user")
    List<User> getUsers();

    /**
     * 根据id获取用户
     * @param userId
     * @return
     */
    @Select("select * from user where id = #{userId}")
    User getUserById(Integer userId);

    /**
     * 根据名称模糊查询
     * @param userName
     * @return
     */
    @Select("select * from user where username like '%${userName}%'")
    List<User> getUserByName(String userName);

    /**
     * 查询用户总数
     * @return
     */
    @Select("select count(*) from user")
    int getTotalUser();
}

此处仅展示注解相关代码,具体详细代码请查看:Mybatis注解CRUD

3. 注解进阶操作

使用注解实现一对多的关联查询和缓存懒加载

书写步骤:

  • 书写和数据库对应的实体类
  • 书写持久层接口
  • 书写配置文件

代码示例:

持久层接口IUserDao,配置一对多关系

public interface IUserDao {

    /**
     * 查询所有用户
     * @return
     */
    @Select("select * from user")
    @Results(id = "userMap", value={
        @Result(id = true, property = "userId",column = "id"),
        @Result(property = "userName", column = "username"),
        @Result(property = "userBirthday", column = "birthday"),
        @Result(property = "userSex", column = "sex"),
        @Result(property = "userAddress", column = "address"),
        @Result(property = "accounts", column = "id",
                many = @Many(select = "cn.bruce.dao.IAccountDao.getAccountByUid", fetchType = FetchType.LAZY))
    })
    List<User> getUsers();

    /**
     * 根据id获取用户
     * @param userId
     * @return
     */
    @Select("select * from user where id = #{userId}")
    @ResultMap("userMap")
    User getUserById(Integer userId);

    /**
     * 根据名称进行模糊查询
     * @param userName
     * @return
     */
    @Select("select * from user where username like '%${userName}%'")
    @ResultMap("userMap")
    List<User> getUserByName(String userName);

}

持久层接口IAccountDao,配置一对一关系

public interface IAccountDao {

    /**
     * 获取全部账户
     * @return
     */
    @Select("select * from account")
    @Results(id = "accountMap", value = {
        @Result(id = true, property = "id", column = "id"),
        @Result(property = "uid", column = "uid"),
        @Result(property = "money", column = "money"),
        @Result(property = "user", column = "uid",
                one = @One(select = "cn.bruce.dao.IUserDao.getUserById", fetchType = FetchType.EAGER))
    })
    List<Account> getAccounts();

    /**
     * 根据uid获取账户信息
     * @param userId
     * @return
     */
    @Select("select * from account where uid = #{userId}")
    List<Account> getAccountByUid(Integer userId);
}

此处仅展示持久层的注解配置,具体的代码请查看:Mybatis注解配置一对多

九、手动实现Mybatis框架基本结构

经过上面的介绍我们对MyBatis框架有了基本的认识和了解,我们也可以尝试实现MyBatis框架的部分功能,比如查询相关的功能。我们手动实现MyBatis框架不是为了取代已经设计好的框架,我们毕竟了解的还不够深入,对于其中的诸多设计细节理解也不够深入。我们手动实现的目的在于,了解框架解析执行的原理和涉及到的设计模式,加强对MyBatis框架的理解,帮助我们更好的使用框架。

1. MyBatis框架执行的基本流程:

自定义框架流程

2. 设计的基本思路

  • 设计sqlSession相关的类

    • 设计创建工厂的类
    • 设计代理工厂类
    • 设计真正创建Dao对象的类
  • 设计配置Bean

    • 设计数据库连接相关的Bean
    • 设计执行SQL语句和结果类型的Bean
  • 设计文件IO读取的类--用于进行XML配置

  • 设计自定义注解类--用于进行注解配置

  • 设计工具类

    • 设计解析xml的工具类获取SQL语句
    • 设计解析注解的工具类获取SQL语句

具体的实现代码比较多,此处就不再展示,详细代码请查看:手动实现MyBatis框架基本结构

3. 应用到的设计模式

3.1 工厂模式

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

工厂模式调用UML图

image-20200929092347145

实现代码:

// 图形接口,没有指明具体的类型
public interface Shape {
   void draw();
}

// 矩形类,实现图形接口
public class Rectangle implements Shape {
 
   @Override
   public void draw() {
      System.out.println("Inside Rectangle::draw() method.");
   }
}

// 正方形类,实现图形接口
public class Square implements Shape {
 
   @Override
   public void draw() {
      System.out.println("Inside Square::draw() method.");
   }
}

// 圆形类,实现图形接口
public class Circle implements Shape {
 
   @Override
   public void draw() {
      System.out.println("Inside Circle::draw() method.");
   }
}

// 图形工厂,用于创建图形对象
public class ShapeFactory {
    
   //使用 getShape 方法获取形状类型的对象
   public Shape getShape(String shapeType){
      if(shapeType == null){
         return null;
      }        
      if(shapeType.equalsIgnoreCase("CIRCLE")){
         return new Circle();
      } else if(shapeType.equalsIgnoreCase("RECTANGLE")){
         return new Rectangle();
      } else if(shapeType.equalsIgnoreCase("SQUARE")){
         return new Square();
      }
      return null;
   }
}

// 测试类,测试工厂模式
public class FactoryPatternDemo {
 
   public static void main(String[] args) {
      ShapeFactory shapeFactory = new ShapeFactory();
 
      //获取 Circle 的对象,并调用它的 draw 方法
      Shape shape1 = shapeFactory.getShape("CIRCLE");
 
      //调用 Circle 的 draw 方法
      shape1.draw();
 
      //获取 Rectangle 的对象,并调用它的 draw 方法
      Shape shape2 = shapeFactory.getShape("RECTANGLE");
 
      //调用 Rectangle 的 draw 方法
      shape2.draw();
 
      //获取 Square 的对象,并调用它的 draw 方法
      Shape shape3 = shapeFactory.getShape("SQUARE");
 
      //调用 Square 的 draw 方法
      shape3.draw();
   }
}

3.2 代理模式

使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

3.1.1 静态代理

代码示例:

耐克公司与合作工厂共同生产衣服,耐克公司不需要关注衣服具体是怎样生产的,只需要提供设计图即可,合作工厂会帮助耐克完成生产,与耐克合作的工厂就是代理类。

interface ClothFactory{
    void produceCloth();
}

//被代理类
class NikeClothFactory implements ClothFactory{

    @Override
    public void produceCloth() {
        System.out.println("Nike 生产衣服");
    }
}

//代理类
class ProxyClothFactory implements ClothFactory{

    private ClothFactory factory;//用被代理类对象进行实例化

    public ProxyClothFactory(ClothFactory factory) {
        this.factory = factory;
    }

    @Override
    public void produceCloth() {
        System.out.println("代理工厂做一些准备工作");

        factory.produceCloth();

        System.out.println("代理工厂做一些后续的收尾工作");

    }
}

//测试
public class StaticProxyTest {
    public static void main(String[] args) {

        //创建被代理类的对象
        ClothFactory nike = new NikeClothFactory();

        //创建代理类的对象
        ProxyClothFactory proxyClothFactory = new ProxyClothFactory(nike);

        proxyClothFactory.produceCloth();
    }
}

静态代理的缺点:

① 代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展。

② 每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。

3.1.2 动态代理

动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。

相比于静态代理的优点:

抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法。

动态代理的实现:

  1. 创建一个实现接口 InvocationHandler 的类,它必须实现invoke方法,以完成代理的具体操作。
  2. 创建被代理类以及接口
  3. 通过Proxy的静态方法 newProxyInstance(ClassLoader loader, Class<?>...interface, InvocationHandler h) 创建一个接口代理
  4. 通过代理类的实例调用被代理类的方法

代码实现:

interface Human {
    String getBelief();

    void eat(String food);
}

//被代理类
class SuperMan implements Human {

    @Override
    public String getBelief() {
        return "I believe I can fly!";
    }

    @Override
    public void eat(String food) {
        System.out.println("I like eat " + food);
    }
}

/*
要想实现动态代理,需要解决的问题?
问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象。
问题二:当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法a。
 */

//创建继承了InvocationHandler接口的类
class MyInvocationHanlder implements InvocationHandler {
    private Object obj;//需要使用被代理类的对象进行赋值

    public void bind(Object obj) {
        this.obj = obj;
    }
    //当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
    //将被代理类要执行的方法a的功能就声明在invoke()中
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        //method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
        //obj:被代理类的对象
        Object returnValue = method.invoke(obj, args);

        //上述方法的返回值就作为当前类中的invoke()的返回值。
        return returnValue;
    }
}

class ProxyFactory {
    //调用此方法,返回一个代理类的对象。解决问题一
    public static Object getProxyInstance(Object obj) {
        MyInvocationHanlder hanlder = new MyInvocationHanlder();
        hanlder.bind(obj);
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),hanlder);

    }
}

//测试动态代理
public class ProxyTest {
    public static void main(String[] args) {
        SuperMan superMan = new SuperMan();
        //proxyInstance:代理类的对象
        Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
        //当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
        String belief = proxyInstance.getBelief();
        System.out.println(belief);
        proxyInstance.eat("火锅");
    }
}

关于设计模式部分,还有很多要写的,后续会再写博客详细讲解的,敬请期待。(。^▽^)