SSM整合01:MyBatis

405 阅读10分钟

MyBatis

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

一、框架概述

框架一般处在低层应用平台(如 J2EE)和高层业务逻辑之间的中间层。

1. 三层架构

  • 表现层:用于显示控制
  • 业务层:处理业务逻辑需求
  • 持久层:与数据库交互

2. 持久层技术解决方案

3. MyBatis框架

MyBatis框架采用ORM思想解决了实体和数据库映射的问题,内部封装了JDBC加载驱动、创建连接、创建 statement等繁杂的过程,屏蔽了JDBC-API底层访问细节,使开发者只需要关注sql语句本身。

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

ORM:Object Relational Mappging 对象关系映射

我们把 Mybatis 的功能架构分为三层:

  1. API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
  2. 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
  3. 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

二、简单案例分析

1. 环境搭建

1.1 前期准备:创建数据库

CREATE DATABASE javaee; -- 创建数据库
USE javaee; 			-- 使用数据库
-- 创建表user
DROP TABLE IF EXISTS `user`
CREATE TABLE `user` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(32) NOT NULL COMMENT '用户名称',
    `birthday` DATETIME DEFAULT NULL COMMENT '生日',
    `sex` CHAR(1) DEFAULT NULL COMMENT '性别',
    `address` VARCHAR(256) DEFAULT NULL COMMENT '地址',
    PRIMARY KEY  (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;	
-- 添加数据
INSERT  INTO `user`(`id`,`username`,`birthday`,`sex`,`address`) VALUES (41,'老王','2018-02-27 17:47:08','男','北京'),(42,'小二王','2018-03-02 15:09:37','女','北京金燕龙'),(43,'小二王','2018-03-04 11:34:34','女','北京金燕龙'),(45,'传智播客','2018-03-04 12:04:06','男','北京金燕龙'),(46,'老王','2018-03-07 17:37:26','男','北京'),(48,'小马宝莉','2018-03-08 11:44:00','女','北京修正');

1.2 IDEA新建maven工程

  • 新建工程时注意及信息填写

    • 不勾选Create from archetype
    • GroupId:com.hihanying
    • ArtifactId:javaee01_mybatis01_xml
  • 项目建完后pom.xml文件配置:在<version></version>后加入以下内容

    <packaging>jar</packaging>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.9</source>
                    <target>1.9</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    

    使用 MyBatis, 除了将坐标写入pom.xml也可将 mybatis-x.x.x.jar 文件置于 classpath 中。

2. 构建项目框架

2.1 项目目录结构

2.2 编写User实体类:

  • 路径:javaee01_mybatis01\src\main\java\com\hihanying\domain\User.java

  • 字段:

    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    
  • Alt+Insert 插入 getter、setter、tostring

2.3 编写持久层接口文件

  • 路径:javaee01_mybatis01\src\main\java\com\hihanying\dao\IUserDao.java

  • 名称:可以是UserDaoUserMapper,个人习惯

  • 代码:

    public interface IUserDao {
        List<User> findAll();
    }
    

2.4 编写持久层接口映射文件

  • 路径:javaee01_mybatis01\src\main\resources\com\hihanying\dao\IUserDao.xml

    在IDEA中创建的目录dictionary和包package是不一样的

    • 包在创建时输入com.itheima.dao,三级结构
    • 目录在创建时输入com.itheima.dao,一级目录
  • 内容:

    <?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="com.hihanying.dao.IUserDao">
        <select id="findAll" resultType="com.hihanying.domain.User">
            select * from user
        </select>
    </mapper>
    
  • 注意:

    • MyBatis 的映射配置文件位置必须和dao接口的包结构相同
    • 文件名称必须以持久层接口的名称命名,扩展名是.xml
    • 映射配置文件的mapper标签namespace属性的取值必须是dao接口的全限定类名
    • 映射配置文件的操作配置(select标签)id属性的取值必须是dao接口的方法名

2.5 编写 SqlMapConfig.xml 配置文件

  • 路径:javaee01_mybatis01\src\main\resources\SqlMapConfig.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>
        <!-- 配置环境 -->
        <environments default="mysql">
            <!-- 配置mysql的环境 -->
            <environment id="mysql">
                <!-- 配置事务类型 -->
                <transactionManager type="JDBC"></transactionManager>
                <!-- 配置数据源/连接池 -->
                <dataSource type="POOLED">
                    <!-- 配置连接数据库的4个基本类型 -->
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://localhost:3306/javaee"/>
                    <property name="username" value="root"/>
                    <property name="password" value="root"/>
                </dataSource>
            </environment>
        </environments>
    
        <mappers>
            <mapper resource="com/hihanying/dao/IUserDao.xml"/>
        </mappers>
    </configuration>
    

3. 编写测试类

3.1 代码示例

路径:javaee01_mybatis01\src\test\java\com\hihanying\test\MybatisTest.java

public class MybatisTest {
    public static void main(String[] args) throws IOException {
        // 1.读取配置文件 SqlMapConfig.xml
        InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 2.通过配置文件创建 SqlSessionFactory 工厂
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(is);
        // 3.通过 SqlSessionFactory 工厂创建 SqlSession 对象
        SqlSession sqlSession = factory.openSession();
        // 4.通过 SqlSession 对象创建 IUserDao 接口的代理对象
        IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        // 5.使用代理对象 userDao 执行方法
        List<User> users = userDao.findAll();
        for (User user : users) {
            System.out.println(user);
        }
        // 6.释放资源
        sqlSession.close();
        is.close();
    }
}

3.2 代码分析

读取配置文件

  • 使用类加载器
    • 使用Resources.getResourceAsStream("SqlMapConfig.xml")应该导入org.apache.ibatis.io.Resources,MyBatis 中的 Resources 工具类为从类路径中加载资源提供了易于使用的方法。
    • 这等价于:MybatisTest.class.getClassLoader().getResourceAsStream("SqlMapConfig.xml")

SqlSessionFactory

  • SqlSessionFactory 是Mybatis的关键对象, 是创建SqlSession的工厂,每一个 MyBatis 的应用程序都以一个 SqlSessionFactory 对象的实例为核心

    • 操作图
    • 时序图

SqlSessionFactory 的创建使用了Builder模式

  • Builder模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

  • 根据不同的输入参数来构建 SqlSessionFactory 这个工厂对象

  • 优点:把创建对象的细节隐藏,使用者直接调用方法即可拿到对象

SqlSession 对象的创建,即 SqlSessionFactory 本身使用了静态/简单工厂模式

  • 在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

  • SqlSessionFactory 是个接口,它有两个实现类 DefaultSqlSessionFactory.java, SqlSessionManager.java ,Mybatis使用的是DefaultSqlSessionFactory.java 来作为其默认实现

  • SqlSessionFactory 的 openSession 方法重载了很多,分别支持autoCommit、Executor、Transaction等参数的输入,来构建核心的 SqlSession 对象。

  • 优势:解耦

创建 IUserDao 接口实现类使用了代理模式

  • 代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。

  • 执行过程

    其中第6步,MapperProxy对象实现了InvocationHandler接口,并且实现了该接口的invoke方法。

  • 优势:只需要编写Mapper.java接口类,当真正执行一个Mapper接口的时候,就会转发给MapperProxy.invoke方法,而该方法则会调用后续的 sqlSession.cud > executor.execute > prepareStatement 等一系列方法,完成SQL的执行和返回。即在不修改源码的基础上对已有的方法进行了增强

4. 基于注解的方式

MyBatis 基于注解方式有两处需要注意的地方

  1. 不需要 IUserDao.xml 文件,只需在 IUserDao 接口的方法上使用@Select注解,并且指定SQL语句

    public interface IUserDao {
        @Select("select * from user")
        List<User> findAll();
    }
    
  2. 需要在 SqlMapConfig.xml 文件中的 mapper 配置时,使用class属性指定 IUserDao 接口的全限定类名。

    <mappers>
        <mapper class="com.hihanying.dao.IUserDao"/>
    </mappers>
    

三、自定义MyBatis框架

目的:深入了解 MyBatis 实现过程

过程:将 SqlMapConfig.xml 配置文件中的 mybatis 坐标删除,而测试代码中使用到的 mybatis 包中的类通过自定义实现

1. 项目结构

自定义MyBatis框架将为以下测试代码服务:

// 1.读取配置文件 SqlMapConfig.xml
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2.通过配置文件创建 SqlSessionFactory 工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
// 3.通过 SqlSessionFactory 工厂创建 SqlSession 对象
SqlSession sqlSession = factory.openSession();
// 4.通过 SqlSession 对象创建 IUserDao 接口的代理对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 5.使用代理对象 userDao 执行方法
List<User> users = userDao.findAll();

上述代码中涉及到的类和接口有:

public class Resources
public class SqlSessionFactoryBuilder
public interface SqlSessionFactory
public interface SqlSession

项目结构如下,包括上述类与接口及其源码中涉及的类:

2. 实现 Resources 类

Resources 类的作用就是通过其 getResourceAsStream 方法,读取配置文件 SqlMapConfig.xml,返回一个 InputStream 对象

public class Resources {
    public static InputStream getResourceAsStream(String filePath) {
        return Resources.class.getClassLoader().getResourceAsStream(filePath);
    }
}

3. 实现 SqlSessionFactoryBuilder 类

SqlSessionFactoryBuilder 类用于创建 SqlSessionFactory 对象,使用 build 方法读取配置文件的 InputStream 对象,返回一个 SqlSessionFactory 对象。

public class SqlSessionFactoryBuilder {
    public SqlSessionFactory build(InputStream config) {
        Configuration cfg = XMLConfigBuilder.loadConfiguration(config);
        return new DefaultSqlSessionFactory(cfg);
    }
}

读取配置文件涉及到 xml 文件的解析,在这里我们使用工具类 XMLConfigBuilder 的 loadConfiguration 方法,它返回一个 Configuration 对象,里面存取了配置文件中的以下信息

public class Configuration {
    private String driver;
    private String url;
    private String username;
    private String password;
    private Map<String, Mapper> mappers = new HashMap<String, Mapper>();
    
    //注意:此处采用追加赋值的方式,而非直接赋值,防止mappers中由多个内容造成覆盖
    public void setMappers(Map<String, Mapper> mappers) {
        this.mappers.putAll(mappers);
    }
}

其中,前4个 String 类型的对象表示配置连接数据库的4个基本对象,而 mappers 对象中的 key 是dao 的全限定类名和方法名,Mapper是自定义对象,包括 queryString 和 resultType ,分别表示要 执行的SQL语句和要封装的实体类全限定类名

public class Mapper {
    private String queryString;
    private String resultType;
}

当获取 Configuration 对象(即配置文件中所有有用的信息)之后,应该返回一个 SqlSessionFactory 对象,这就需要创建 SqlSessionFactory 的实现类 DefaultSqlSessionFactory

4. 实现 SqlSessionFactory 接口

SqlSessionFactory 接口中应该有一个 openSession 方法用于创建一个新的 SqlSession 对象操作数据库

public interface SqlSessionFactory {
    SqlSession openSession();
}

实现:一方面要接收创建 SqlSessionFactory 时传递的 Configuration 对象,另一方面通过openSession 方法,将 Configuration 对象传递给新创建的 SqlSession 对象。

public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private Configuration cfg;
    public DefaultSqlSessionFactory(Configuration cfg) {
        this.cfg = cfg;
    }
    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(cfg);
    }
}

5. 实现 SqlSession 接口

SqlSession 接口是与数据库交互的核心类

  • getMapper 方法用于创建 dao 的代理对象,其参数是 dao 接口的字节码,返回的是该 dao 接口的代理对象。
  • close 方法用于释放资源
public interface SqlSession {
    <T> T getMapper(Class<T> daoInterfaceClass);
    void close();
}

实现:

public class DefaultSqlSession implements SqlSession {
    private Configuration cfg;
    private Connection connection;

    public DefaultSqlSession(Configuration cfg) {
        this.cfg = cfg;
        connection = DataSourceUtil.getConnection(cfg);
    }
    @Override
    public <T> T getMapper(Class<T> daoInterfaceClass) {
        return (T) Proxy.newProxyInstance(
                daoInterfaceClass.getClassLoader(),
                new Class[]{daoInterfaceClass},
                new MapperProxy(cfg.getMappers(),connection));
    }
    @Override
    public void close() {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

DefaultSqlSession 的构造方法接收 Configuration 对象并转化为 Connection,这里使用了工具类 DataSourceUtil 中的 getConnection 方法

public class DataSourceUtil {
    public static Connection getConnection(Configuration cfg) {
        try {
            Class.forName(cfg.getDriver());
            return DriverManager.getConnection(cfg.getUrl(),
                    cfg.getUsername(), cfg.getPassword());
        } catch (SQLException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

DefaultSqlSession 类重写了 getMapper 方法,用于创建代理对象,其中参数 MapperProxy 定义如下,selectList是工具类Executor中的方法,用于执行sql语句并封装结果集:

public class MapperProxy implements InvocationHandler {
    private Map<String, Mapper> mappers;
    private Connection conn;
    public MapperProxy(Map<String, Mapper> mappers, Connection conn) {
        this.mappers = mappers;
        this.conn = conn;
    }
    // 用于对方法进行增强,即调用selectList方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        String className = method.getDeclaringClass().getName();
        String key = className + '.' + methodName;
        Mapper mapper = mappers.get(key);
        if (mapper ==null) {
            throw new IllegalArgumentException("传入的参数有误");
        }
        return new Executor().selectList(mapper,conn);
    }
}

四、使用Mybatis完成CRUD

  1. 注意

    1. 测试程序的@Before和@After的使用
    2. 使用大括号读值
    insert into user (username,sex,birthday,address) values (#{username},#{sex},#{birthday},#{address})
    
    1. 数据库中的表中内容应该和代码中对用的类中的字段一致
  2. 基本流程:以向数据库中添加一条记录为例

    1. 在 com\hihanying\dao\IUserDao.xml 中添加

      <select id="saveUser" resultType="com.hihanying.domain.User">
          insert into user (username,sex,birthday,address) values (#{username},#{sex},#{birthday},#{address})
      </select>
      
    2. 在com\hihanying\dao\IUserDao.java中添加对应的方法

      void saveUser(User user);
      
    3. 在测试方法中测试

    @Test
    public void testSaveUser() throws IOException {
        User user = new User();
        user.setSex("男");
        user.setBirthday(new Date());
        user.setUsername("应寒");
        user.setAddress("山东省潍坊市");
    
        mapper.saveUser(user);
        sqlSession.commit();
    }
    
    1. 在SQL可视化工具中查看效果