mybatis第一天

该文档是:mybatis框架学习...

博客连接:https://www.loveuluo.cn

日期:2021-01-01

课程

image-20201231100759866

1. mybatis概述

image-20201231100837756

2. mybatis快速入门

2.1 官网下载mybatis框架

image-20201231101743562

image-20201231101752525

image-20201231101800621

2.2 搭建Mybatis开发环境

2.2.1 创建maven工程

创建mybatis01的工程,工程信息如下:

image-20201231101926182

2.2.2 添加Mybatis3.4.5的坐标

在pom.xml文件中添加Mybatis3.4.5的坐标,如下:

<!--导入依赖-->
<dependencies>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.5</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.10</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.6</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.12</version>
    </dependency>
</dependencies>

2.2.3 导入数据库以便演示使用

image-20201231102241688

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','女','北京修正');

DROP TABLE IF EXISTS `account`;

CREATE TABLE `account` (
  `ID` int(11) NOT NULL COMMENT '编号',
  `UID` int(11) default NULL COMMENT '用户编号',
  `MONEY` double default NULL COMMENT '金额',
  PRIMARY KEY  (`ID`),
  KEY `FK_Reference_8` (`UID`),
  CONSTRAINT `FK_Reference_8` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert  into `account`(`ID`,`UID`,`MONEY`) values (1,41,1000),(2,45,1000),(3,41,2000);

DROP TABLE IF EXISTS `role`;

CREATE TABLE `role` (
  `ID` int(11) NOT NULL COMMENT '编号',
  `ROLE_NAME` varchar(30) default NULL COMMENT '角色名称',
  `ROLE_DESC` varchar(60) default NULL COMMENT '角色描述',
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert  into `role`(`ID`,`ROLE_NAME`,`ROLE_DESC`) values (1,'院长','管理整个学院'),(2,'总裁','管理整个公司'),(3,'校长','管理整个学校');

DROP TABLE IF EXISTS `user_role`;

CREATE TABLE `user_role` (
  `UID` int(11) NOT NULL COMMENT '用户编号',
  `RID` int(11) NOT NULL COMMENT '角色编号',
  PRIMARY KEY  (`UID`,`RID`),
  KEY `FK_Reference_10` (`RID`),
  CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `role` (`ID`),
  CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert  into `user_role`(`UID`,`RID`) values (41,1),(45,1),(41,2);

2.2.4 编写User实体类

这里的属性先和数据库保持一致。

public class User implements Serializable {
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    //get set toString。
}

为什么要进实现Serializable接口:

image-20201231103359821

2.2.5 编写持久层接口UserDao

//UserDao接口就是我们的持久层接口
public interface UserDao {
    /**
     * 查询所有用户
     * @return
     */
    List<User> findAll();
}

2.2.6 编写SqlMapConfig.xml配置文件

image-20201231104532741

这是mybatis的配置文件,里面放着数据库连接信息以及自定义sql文件的引用

<?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">
<!--mybatis主配置文件-->
<configuration>
    <!-- 配置mybatis的环境 -->
    <environments default="mysql">
        <!-- 配置mysql的环境 -->
        <environment id="mysql">
            <!-- 配置事务的类型 -->
            <transactionManager type="JDBC"></transactionManager>
            <!-- 配置连接数据库的信息:用的是数据源(连接池) -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"></property>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis"></property>
                <property name="username" value="root"></property>
                <property name="password" value="123456"></property>
            </dataSource>
        </environment>
    </environments>

    <!-- 指定映射配置的位置,映射文件指的是每个dao独立的配置文件 -->
    <mappers>
        <mapper resource="com/Luo/dao/UserDao.xml"/>
    </mappers>
</configuration>

2.2.7 编写持久层接口的映射文件UserDao.xml

创建位置:必须和持久层接口在相同的包中。

名称:必须以持久层接口名称命名文件名,扩展名是.xml

image-20201231104828540

<mapper namespace="com.Luo.dao.UserDao"><!--对应接口所在位置-->
    <!--配置查询所有-->
    <!--findAll这是方法名,对应到UserDao接口中的方法-->
    <!--resultType是为了指定这个方法执行后返回的结果集封装到User实体类中,并且会自动添加到List当中-->
    <select id="findAll" resultType="com.Luo.domain.User">
        SELECT * FROM user
    </select>
</mapper>

注意事项:

这样配置以后就无需再写dao的实现类。

创建UserDao.xml 和 UserDao.java时名称是为了和我们之前的知识保持一致。在Mybatis中它把持久层的操作接口名称和映射文件也叫做:Mappe所以:UserDao 和 UserMapper是一样的。

2.2.8 找到log4j配置文件并复制进resources

image-20201231105859408

2.2.9 编写测试类

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

image-20201231111914466

2.2.10 小结

image-20201231122739109

明确:
我们在实际开发中,都是越简便越好,所以都是采用不写dao实现类的方式。
不管使用XML还是注解配置。
但是Mybatis它是支持写dao实现类的(这里就不演示写dao实现类了)。

2.3 补充(基于注解的mybatis使用)

注意:在使用基于注解的Mybatis配置时,请移除xml的映射配置(UserDao.xml)。

image-20201231123050359

在持久层接口中添加注解

//UserDao接口就是我们的持久层接口
public interface UserDao {
    /**
     * 查询所有用户
     * @return
     */
    @Select("select * from user") //给方法上加上注解
    List<User> findAll();
}

修改SqlMapConfig.xml

<!-- 告知mybatis映射配置的位置 -->
<mappers>
    <!--改为class并且指定到UserDao接口-->
    <mapper class="com.Luo.dao.UserDao"/>
</mappers>

最终视图:

image-20201231124615902

3. 自定义mybatis框架分析

快速入门案例分析

image-20201231142933352

3.1 自定义Mybatis框架的分析

本章我们将使用前面所学的基础知识来构建一个属于自己的持久层框架,将会涉及到的一些知识点:工厂模式(Factory工厂模式)、构造者模式(Builder模式)、代理模式,反射,自定义注解,注解的反射,xml解析,数据库元数据,元数据的反射等。

分析流程:

image-20210101185738173

3.2 前期准备

(1)使用mybatis快速入门中的xml配置案例进行修改

(2)把pom.xml中的mybatis依赖删除
image-20210101190057491

此时测试类中所有和mybatis有关的都应该会爆红
image-20210101190449539

3.3 基于XML的自定义mybatis框架

按照前期准备中那些爆红的东西顺序进行编写他们:

第一步

(1)编写Resources类:
image-20210101190745415

Resources类:

/**
 * 使用类加载器读取配置文件的类
 */
public class Resources {
    /**
     * 根据传入的配置文件名,通过类加载器找到对应filePath的配置文件,获取一个字节输入流
     * @param filePath
     * @return
     */
    public static InputStream getResourceAsStream(String filePath) {
        //1拿到当前类的字节码,2获取这个字节码的类加载器,3根据类加载器读取配置。
        return Resources.class.getClassLoader().getResourceAsStream(filePath);
    }
}

编写完后那么这个就不爆红了。
image-20210101191807376

第二步

(2)编写SqlSessionFactoryBuilder类和SqlSessionFactory接口和SqlSession接口:
image-20210101193153992

SqlSessionFactoryBuilder类:

暂时先返回null。

/**
 * 用于创建一个SqlSessionFactory对象
 */
public class SqlSessionFactoryBuilder {

    /**
     * 根据参数的字节输入流来构建一个SqlSessionFactory工厂
     * @param config
     * @return
     */
    public SqlSessionFactory build(InputStream config) {
        return null;
    }
}

SqlSessionFactory接口:

public interface SqlSessionFactory {
    /**
     * 用于打开一个新的SqlSession对象
     * @return
     */
    SqlSession openSession();
}

SqlSession接口

/**
 * 自定义mybatis中的和数据库交互的核心类
 * 它里边可以创建dao接口的代理对象
 */
public interface SqlSession {

    /**
     * 根据参数创建一个代理对象
     * @param daoInterfaceClass dao的接口
     * @param <T>
     * @return
     */
    <T> T getMapper(Class<T> daoInterfaceClass);

    /**
     * 释放资源
     */
    void close();

}

编写完后那么这个就都不爆红了。
image-20210101193243974

第三步

(3)导入dom4j和jaxen的依赖,导入解析XML的工具类(这里我们重点关注的是mybatis,所以xml解析直接使用工具类):

导入解析XML工具类:

image-20210101200951739

image-20210101194914867

把爆红的部分进行处理,发现其实就是缺少Configuration类(mybatis的配置)和Mapper类(和分析中的Mapper一样):

image-20210101200938242

Configuration类:

/**
 * 自定义mybatis的配置类
 */
public class Configuration {
    //分析中的连接信息
    private String driver;
    private String url;
    private String username;
    private String password;
    //分析中的映射信息(直接new出来防止put的时候空指针异常)
    private Map<String,Mapper> mappers=new HashMap<>();

    public Map<String, Mapper> getMappers() {
        return mappers;
    }
    //这里setMappers的方法特殊拿出来,因为和系统生成的不一样,需要手动修改下。
    public void setMappers(Map<String, Mapper> mappers) {
        //为了防止覆盖,而是累加
        this.mappers.putAll(mappers);//此处使用追加的方式
    }
    //get set方法
}

Mapper类:

/**
 * 用于封装执行的SQL语句和结果类型的全限定类名
 */
public class Mapper {
    private String queryString;//SQl语句
    private String resultType;//实体类的全限定类名
    //get set方法
}

第四步

(4)填充SqlSessionFactoryBuilder中还未写完的build方法并实现未实现的接口

image-20210101202444387

SqlSessionFactoryBuilder类:

这里的SqlSessionFactoryBuilder其实就使用了建造者模式,相当于是一个指挥者,这里的build方法其实就是,先用XMLConfigBuilder得到了cfg(配置文件对象),然后把cfg配置文件给指挥者,指挥者造出一个带有配置文件的SqlSessionFactor工厂!

/**
 * 用于创建一个SqlSessionFactory对象
 */
public class SqlSessionFactoryBuilder {

    /**
     * 根据参数的字节输入流来构建一个SqlSessionFactory工厂
     * @param config
     * @return
     */
    public SqlSessionFactory build(InputStream config) {
        Configuration cfg = XMLConfigBuilder.loadConfiguration(config);
        SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(cfg);
        return sqlSessionFactory;
    }
}

SqlSessionFactory接口的实现类:

这里其实是使用了工厂方法设计模式,这就是那个被造出来的带有配置文件的SqlSessionFactor工厂,那么这个工厂可以做什么呢?客户可以直接通过调用openSession()方法获得一个操作数据库对象(里边带有cfg配置文件),无需在意里边的类似于类名的修改(DefaultSqlSession类似于如果这个名称改变了或者换成类似VIPSqlSession)。无需在意,反正客户只需要调用openSession()方法就能得到工厂生产的对象!

/**
 * SqlSessionFactory接口的实现类
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration cfg;

    public DefaultSqlSessionFactory(Configuration cfg) {
        this.cfg = cfg;
    }

    /**
     * 用于创建一个新的操作数据库对象
     * @return
     */
    @Override
    public SqlSession openSession() {
        SqlSession sqlSession = new DefaultSqlSession(cfg);
        return sqlSession;
    }
}

SqlSession接口的实现类:

具体功能还未编写。

/**
 * SqlSession接口的实现类
 */
public class DefaultSqlSession implements SqlSession {

    private Configuration cfg;

    public DefaultSqlSession(Configuration cfg) {
        this.cfg = cfg;
    }

    /**
     * 用于创建代理对象
     * @param daoInterfaceClass dao的接口
     * @param <T>
     * @return
     */
    @Override
    public <T> T getMapper(Class<T> daoInterfaceClass) {
        return null;
    }

    @Override
    public void close() {

    }
}

第五步

(5)编写SqlSession接口的实现类DefaultSqlSession

image-20210101225334369

先编写用来获取连接的工具类(在DefaultSqlSession类中用到):

/**
 * 用于创建数据源的工具类
 */
public class DataSourceUitl {
    /**
     * 用于获取一个连接
     * @param cfg cfg中放着连接信息
     * @return
     */
    public static Connection getConnection(Configuration cfg){
        try {
            Class.forName(cfg.getDriver());
            Connection con = DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
            return con;
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }
}

负责执行SQL语句,并且封装结果集的工具类(在MapperProxy类中用到):

/**
 * 负责执行SQL语句,并且封装结果集
 */
public class Executor {
    //查询所有数据并把每一行封装到对应的实体类中,并把实体类放到集合中返回(可以单个可以多个)
    public <E> List<E> selectList(Mapper mapper, Connection conn) {
        PreparedStatement pstm = null;
        ResultSet rs = null;
        try {
            //1.取出mapper中的数据
            String queryString = mapper.getQueryString();//select * from user
            String resultType = mapper.getResultType();//com.Luo.domain.User
            Class domainClass = Class.forName(resultType);
            //2.获取PreparedStatement对象
            pstm = conn.prepareStatement(queryString);
            //3.执行SQL语句,获取结果集
            rs = pstm.executeQuery();
            //4.封装结果集
            List<E> list = new ArrayList<E>();//定义返回值
            while(rs.next()) {
                //实例化要封装的实体类对象
                E obj = (E)domainClass.newInstance();

                //取出结果集的元信息:ResultSetMetaData
                ResultSetMetaData rsmd = rs.getMetaData();
                //取出总列数
                int columnCount = rsmd.getColumnCount();
                //遍历总列数
                for (int i = 1; i <= columnCount; i++) {
                    //获取每列的名称,列名的序号是从1开始的
                    String columnName = rsmd.getColumnName(i);
                    //根据得到列名,获取每列的值
                    Object columnValue = rs.getObject(columnName);
                    //给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)
                    PropertyDescriptor pd = new PropertyDescriptor(columnName,domainClass);//要求:实体类的属性和数据库表的列名保持一种
                    //获取它的写入方法
                    Method writeMethod = pd.getWriteMethod();
                    //把获取的列的值,给对象赋值
                    writeMethod.invoke(obj,columnValue);
                }
                //把赋好值的对象加入到集合中
                list.add(obj);
            }
            return list;
        } catch (Exception e) {throw new RuntimeException(e);
        } finally {release(pstm,rs);}
    }

    //关闭PreparedStatement和结果集
    private void release(PreparedStatement pstm,ResultSet rs){
        if(rs != null){
            try {rs.close();}catch(Exception e){e.printStackTrace();}
        }
        if(pstm != null){
            try {pstm.close();}catch(Exception e){ e.printStackTrace();}}}
}

DefaultSqlSession类:

使用了动态代理!让UserDao接口有一个执行数据库语句的代理对象!

/**
 * SqlSession接口的实现类
 */
public class DefaultSqlSession implements SqlSession {

    private Configuration cfg;
    //con定义在这里是为了方便close。
    private Connection con;

    public DefaultSqlSession(Configuration cfg) {
        this.cfg = cfg;
        //创建对象的时候就给con赋值。创建一个DataSourceUitl传入cfg(cfg中放着连接信息)来获取连接对象
        con= DataSourceUitl.getConnection(cfg);
    }

    /**
     * 用于创建代理对象
     * @param daoInterfaceClass dao的接口
     * @param <T>
     * @return
     */
    @Override
    public <T> T getMapper(Class<T> daoInterfaceClass) {
        Class[] infeerfaces = {daoInterfaceClass};
        //参数一:代理的是谁就是谁的类加载器,而类的字节码通过参数接受也就是daoInterfaceClass
        //参数二:代理者要实现和被代理者一样的接口,而传过来的参数本身就是一个接口
        //参数三:需要一个InvocationHandler接口的实现类.(这个方法需要一个Mapper对象[以便于执行查询方法]和数据库连接对象)
        return (T) Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(), infeerfaces, new MapperProxy(cfg.getMappers(), con));
    }

    /**
     * 用于释放资源
     */
    @Override
    public void close() {
        if (con!=null) {
            try {
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

动态代理Proxy.newProxyInstance的第三个参数InvocationHandler接口的实现类MapperProxy:

这里实现了方法增强的逻辑,调用selectList方法并且返回封装到对应的实体类的集合。

public class MapperProxy implements InvocationHandler {
    //map的key是接口的全限定类名+方法名(分析中的映射信息)
    private Map<String, Mapper> mappers;
    //需要一个数据库连接对象
    private Connection con;
    //这里需要获取配置类里边的Mappers(通过构造方法传入)
    public MapperProxy(Map<String, Mapper> mappers,Connection con) {
        this.mappers = mappers;
        this.con=con;
    }

    /**
     * 用于对方法进行增强的,我们的增强其实就是调用selectList方法
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //1.获取方法名(例如是调用的findAll方法那就是findAll)
        String methodName = method.getName();
        //2.获取方法所在类的名称(例如是UserDao调用的接口,那么名称就是com.Luo.dao.UserDao)
        String className = method.getDeclaringClass().getName();
        //3.组合key(例如是UserDao调用的findAll方法那就是com.Luo.dao.UserDao.findAll)
        String key=className+"."+methodName;
        //4.通过key获取mappers中的Mapper对象
        /*mappers原先存在的key是从配置文件中读取的全限定类名+方法名,而1-3的操作是获取接口的全限定类名+方法名,
        根据接口的全限定类名+方法名得到配置文件的mapper对象,mapper对象里边放的是sql语句和<实体类>的全限定类名,
        那么把这个mapper交给工具类,工具类调用方法得到list集合。*/
        Mapper mapper = mappers.get(key);
        //5.判断是否有mapper(如果没有那说明接口中的方法名和配置文件中配置的方法名不一,致则抛出异常并结束程序)
        if (mapper==null){
            throw new IllegalArgumentException("传入的参数有误(检查接口的方法名和配置中的方法名是否一致)");
        }
        //6.调用工具类查询所有
        Executor executor=new Executor();
        List<Object> list = executor.selectList(mapper, con);
        return list;
    }
}

完成后的测试

正常进行测试:

image-20210101232010269

测试结果:

image-20210101232104799

如果接口中的方法名和配置文件中的方法名不一致的测试情况:

配置文件:

image-20210101232233685

接口:

image-20210101232315844

测试类:

image-20210101232338737

测试结果:

image-20210101232533063

最终视图

image-20210101230736234

流程回顾

image-20210102161534511

最后修改:2021 年 01 月 21 日 04 : 36 PM
如果觉得我的文章对你有用,请随意赞赏