MyBatis简介

182 阅读8分钟

MyBatis是什么

JDBC

在前面学习到关于持久层(即与数据库交互)的技术中,我们有JDBC技术和Spring对jdbc的简单封装——JdbcTemplate。

下面是一个使用了jdbc的未经封装的传统使用方法:

Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
    //加载数据库驱动
    Class.forName("com.mysql.jdbc.Driver");
    //通过驱动管理类获取数据库链接
    connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8","ro
    ot", "root");
    
    String sql = "select * from user where username = ?";
    //获取预处理 statement
    preparedStatement = connection.prepareStatement(sql);
    //设置参数,第一个参数为 sql 语句中参数的序号(从 1 开始),第二个参数为设置的
    参数值
    preparedStatement.setString(1, "王五");
    //向数据库发出 sql 执行查询,查询出结果集
    resultSet = preparedStatement.executeQuery();
    //遍历查询结果集
    while(resultSet.next()){
     System.out.println(resultSet.getString("id")+"
     "+resultSet.getString("username"));
    }
} catch (Exception e) {
    e.printStackTrace();
}finally{
//释放资源
    if(resultSet!=null){
        try {
            resultSet.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if(preparedStatement!=null) {
        try {
            preparedStatement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if(connection!=null){
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

传统jdbc程序中出现的问题

(1)数据库连接创建,释放的频繁会造成系统资源浪费,从而影响系统性能,这个问题可以使用数据库连接池解决;

(2)在实际开发中sql语句常常是变化的,而jdbc中的sql语句是硬编码到java代码里,这使得代码不易维护。

(3)sql语句中的where条件是不定量的,如果修改了sql语句条件则还需要修改代码,这使得系统不易维护。

MyBatis 框架概述

MyBatis是一个基于Java的持久层框架,它内部封装了jdbc,开发者只需关注sql语句本身,无需耗费精力去处理加载驱动,创建连接等繁杂过程。 mybatis可以通过xml文件或注解的方式将要执行的各种statement配置完成,并通过java 对象和stament中sql语句的动态参数进行映射生产最终执行的sql语句句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并 返回。

第一个MyBatis程序

基于注解的程序

首先参考该文章创建一个Maven项目,然后导入依赖:

<dependencies>
<!--    数据库   -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
    
     <!-- mybatis -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.4.5</version>
    </dependency> 
    
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
</dependencies>

在resources文件夹下编写配置文件

<?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">

        <environment id="mysql">
            <!--使用了 JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作用域-->
            <transactionManager type="JDBC"/>
            <!--使用数据池,复用实例-->
            <dataSource type="POOLED">
                <!--<property name="driver" value="com.mysql.jdbc.Driver"/>-->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/stuman?useUnicode=true&amp;characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="123"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.on1.dao"/>
    </mappers>
</configuration>

数据表user

CREATE TABLE `account`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

数据表对应的bean类Account

public class Account {
    private Integer id;
    private String name;
    private String password;
    //省略 get/set方法
}

dao层类:

public interface AccountDao {
    @Select("select * from account")
    public List<Account> findAll();

    @Insert("insert into account(name, password) values (#{name}, #{password})")
    public void saveAccount(Account account);
}

测试一下:

public class MyBatisTest {
    InputStream in;
    SqlSession sqlSession;
    AccountDao accountDao;
    
    @Before
    public void init() throws Exception {
        //        加载配置文件
        in = Resources.getResourceAsStream("mybatisConfig.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        sqlSession = factory.openSession();
        //        获取代理对象
        accountDao = sqlSession.getMapper(AccountDao.class);
    }

    @After
    public void destroy() throws Exception {
        sqlSession.close();
        in.close();
    }
    
    @Test
    public void testFindAll() throws Exception{
        List<Account> accountList = accountDao.findAll();
        for(Account account1 : accountList) {
            System.out.println(account1);
        }
    }
    
     @Test
    public void testSave() {
        Account account = new Account();
        account.setName("tmo");
        account.setPassword("No123");
        accountDao.saveAccount(account);

        sqlSession.commit();
    }
}

此时程序的项目结构:

基于xml配置文件的使用

修改mybatisConfig.xml中的mappers标签内容

 <mappers>
    <mapper resource="com/on1/dao/AccountDao.xml"/>
</mappers>

注释掉AccountDao接口中的两条注解,然后在resources中编写com.on1.AccountDao.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="com.on1.dao.AccountDao">
    <select id="findAll" resultType="com.on1.bean.Account">
        select * from account;
    </select>

    <insert id="saveAccount" parameterType="com.on1.bean.Account">
        insert  into account(name, password) values (#{name}, #{password})
    </insert>
</mapper>

测试一下:

public class day1 {
    InputStream in;
    SqlSession sqlSession;
    AccountDao accountDao;

    @Before
    public void init() throws Exception {
        //        加载配置文件
        in = Resources.getResourceAsStream("mybatisConfig.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        sqlSession = factory.openSession();
        //        获取代理对象
        accountDao = sqlSession.getMapper(AccountDao.class);
    }

    @After
    public void destroy() throws Exception {
        sqlSession.close();
        in.close();
    }

    @Test
    public void testFindAll() throws Exception {
        List<Account> accountList = accountDao.findAll();
        for (Account account1 : accountList) {
            System.out.println(account1);
        }
    }

    @Test
    public void testSave() {
        Account account = new Account();
        account.setName("tmo");
        account.setPassword("No123");
        accountDao.saveAccount(account);

        sqlSession.commit();
    }
}

此时的项目结构:

注:在使用基于注解的程序时,需要移除xml的映射文件(此例中的AccountDao.xml)

删除和修改,模糊查询操作

在AccountDao类添加方法:

public void updateAccount(Account account);

public void deleteAccountById(Integer id);

public List<Account> findByName(String name);

在映射文件中添加:

<update id="updateAccount" parameterType="com.on1.bean.Account">
    update account set name = #{name}, password = #{password} where id = #{id}
</update>

<delete id="deleteAccountById" parameterType="java.lang.Integer">
    delete  from account where id = #{id}
</delete>

<!--    根据name模糊查询-->
<select id="findByName" parameterType="string" resultType="com.on1.bean.Account">
        select * from account where name like #{name}
</select>

测试方法:

    @Test
    public void testUpdateAccount() throws Exception {
        Account account = new Account();
        account.setName("on1");
        account.setPassword("20190215");
        account.setId(3);
        accountDao.updateAccount(account);

        sqlSession.commit();
    }

    @Test
    public void testDeleteAccountById() throws Exception {
        accountDao.deleteAccountById(2);
        sqlSession.commit();
    }
    
    @Test
    public void testFindAccountByName() {
        List<Account> accountList = accountDao.findByName("%王a%");
        for(Account account : accountList)
            System.out.println(account);
    }

添加数据后获取自的id值

本例中的id项是自增的,我们可以获取添加数据后自增的id值:

在映射文件中的添加标签修改语句:

    <insert id="saveAccount" parameterType="com.on1.bean.Account">
        <selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
            select 1677744;
        </selectKey>
        insert  into account(name, password) values (#{name}, #{password})
    </insert>

测试方法:

    @Test
    public void testSave() {
        Account account = new Account();
        account.setName("tmo");
        account.setPassword("No123");
        System.out.println("提交前的account值:" + account);
        accountDao.saveAccount(account);

        sqlSession.commit();
        System.out.println("提交后的account值:" + account);
    }

控制台输出:

查询条件构成bean对象

Mybatis使用ognl表达式来解析对象字段的值,#{}或${}括号里的值为bean属性名。

ognl表达式:它通过对象的取值方法来获取数据,在写法上省略了get:当我们获取账户的名称时,类中的写法是account.getName(),而ognl表达式写法则是account.name。

由于在mybatis中通过parameterType提供了属性所属的类,因此可以直接只写name

在开发中我们所要查找的数据可能是由种种查询条件构成,而这些查询条件可以构成一个bean对象,因此我们可以构成一个属性是查询条件的bean,而bean中也可以包含bean。

比如我们希望根据账户名查询账户信息,查询条件放到Bank类的account属性。

条件类Bank

public class Bank {
    private Account account;
    //省略get/set方法
}

AccountDao添加方法:

public List<Account> findByBean(Bank bank);

映射文件中添加语句:

<select id="findByBean" parameterType="com.on1.bean.Bank" resultType="com.on1.bean.Account">
        select * from account where name like #{account.name}
</select>

测试:

@Test
    public void testFindAccountByBean() {
        Bank bank = new Bank();
        Account account = new Account();
        account.setName("%王");
        bank.setAccount(account);
        List<Account> accountList = accountDao.findByBean(bank);
        for(Account tmp : accountList)
            System.out.println(tmp);
    }

resultMap标签

在前面的例子中,我们建立的Account类的属性名与数据表account的列名是一致的,那么如果是不一致的,比如此处我们修改Account类的属性名:

public class Account {
    private Integer uId;
    private String uName;
    private String uPassword;
    //省略 get/set方法
}

映射文件中的相关标签不变:

    <select id="findAll" resultMap="accountMap">
        select * from account;
    </select>

测试类不变,运行后的结果是错误的。

针对此问题,我们有两种解决方法:

(1)在映射文件中建立bean类属性名与数据表列名的对应关系:

    <resultMap id="accountMap" type="com.on1.bean.Account">
        <id property="uId" column="id"></id>
        <result property="uName" column="name"></result>
        <result property="uPassword" column="password"></result>
    </resultMap>
    
    <select id="findAll" resultMap="accountMap">
        select * from account;
    </select>

(2)在sql语句中起别名

<select id="findAll" resultType="com.on1.bean.Account">
    select id as uId,  name  as uName, password as uPassword from account;
</select>

properties标签

在前面配置连接数据库基本信息时,我们是直接在标签里面写入对应的值。此外我们还可以把连接信息写在一个文件里,然后通过properties标签来引入这个文件。

我们在resources文件夹下新建文件jdbcConfig.properties,并写入连接信息:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/stuman?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=123

然后修改配置文件mybatisConfig.xml:

<configuration>
    <properties resource="jdbcConfig.properties">
    </properties>
     
    <environments default="mysql">
        <environment id="mysql">
            <!--配置事务类型:使用了 JDBC 的提交和回滚设置-->
            <transactionManager type="JDBC"/>
            <!--配置数据池-->
            <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="com/on1/dao/AccountDao.xml"/>
    </mappers>
</configuration>

typeAliases标签

我们可以在配置文件中给一些bean类起别名,这样在映射文件中直接写别名。

    <typeAliases>
<!--        指定别名后不区分大小写-->
        <typeAlias type="com.on1.bean.Account" alias="account"></typeAlias>
    </typeAliases>

映射文件:

此外也可以通过package标签来指定要配置别名的包

<typeAliases>
<!--        指定要配置别名的包,别名为类名,不区分大小写-->
    <package name="com.on1.bean"/>
</typeAliases>

连接池分类

在连接池配置中,我们需要表明连接池的类型

myBatis内部分别定义了实现java.sql.DataSource接口的UnpooledDataSource,PooledDataSource 类来表示 UNPOOLED、POOLED 类型的数据源。

Mybatis将它的数据源分为三类:

  • UNPOOLED: 不使用连接池的数据源
  • POOLED: 使用连接池的数据源
  • JNDI: 使用 JNDI 实现的数据源

当type=”POOLED”:MyBatis 会创建 PooledDataSource 实例;

type=”UNPOOLED” : MyBatis 会创建 UnpooledDataSource 实例;

type=”JNDI”:MyBatis 会从 JNDI 服务上查找 DataSource 实例,然后返回使用;

UnpooledDataSource类

当我们将连接池类型设置为UNPOOLED并输出数据表里的所有数据时,log4j的输出日志为:

可以发现UNPOOLED类型的连接池是每次创建一个新连接,然后关闭连接。

部分源码

public class UnpooledDataSource implements DataSource {
    private String driver;
    private String url;
    private String username;
    private String password;
    
    public Connection getConnection() throws SQLException {
        return this.doGetConnection(this.username, this.password);
    }
    
    private Connection doGetConnection(String username, String password) throws SQLException {
        Properties props = new Properties();
        if (this.driverProperties != null) {
            props.putAll(this.driverProperties);
        }

        if (username != null) {
            props.setProperty("user", username);
        }

        if (password != null) {
            props.setProperty("password", password);
        }

        return this.doGetConnection(props);
    }
    
    private Connection doGetConnection(Properties properties) throws SQLException {
        this.initializeDriver();    //注册驱动
        Connection connection = DriverManager.getConnection(this.url, properties);
        this.configureConnection(connection);
        return connection;
    }
    //省略其他属性和方法
}

PooledDataSource类

当我们将连接池类型设置为POOLED并输出数据表里的所有数据时,log4j的输出日志为:

因此它是从连接池中获取一个连接来使用,使用完后返还连接

部分源码

public class PooledDataSource implements DataSource {
    public Connection getConnection() throws SQLException {
        return this.popConnection(this.dataSource.getUsername(), this.dataSource.getPassword()).getProxyConnection();
    }
    
     private PooledConnection popConnection(String username, String password) throws SQLException {
        boolean countedWait = false;
        PooledConnection conn = null;
        long t = System.currentTimeMillis();
        int localBadConnectionCount = 0;
        
        while(conn == null) {
            synchronized(this.state) {
                PoolState var10000;
                //如果还有空闲的连接
                if (!this.state.idleConnections.isEmpty()) {
                //idleConnections是一个ArrayList集合
                //连接池是一个存储连接的(线程安全)集合,该集合符合先进先出原则
                     conn = (PooledConnection)this.state.idleConnections.remove(0);
                     if (log.isDebugEnabled()) {
                        log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
                    }
                /*
                    否则如果连接池中活跃的连接数量小于设定的最大值,
                    则创建一个新连接
                */
                }else if (this.state.activeConnections.size() < this.poolMaximumActiveConnections) {
                    conn = new PooledConnection(this.dataSource.getConnection(), this);
                    if (log.isDebugEnabled()) {
                        log.debug("Created connection " + conn.getRealHashCode() + ".");
                    }
                    /*
                        如果超出最大活动连接数,无法创建连接
                        则获取使用时间最长的活动连接,
                    */
                }else {
                    PooledConnection oldestActiveConnection = (PooledConnection)this.state.activeConnections.get(0);
                    //获取使用时间最长的活动连接,并计算使用时间
                    long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
                    //若使用时间超出最大可回收时间,直接回收该连接
                    //回收过期次数增加
                    if (longestCheckoutTime > (long)this.poolMaximumCheckoutTime) {
                        ++this.state.claimedOverdueConnectionCount;
                        var10000 = this.state;
                        // 统计过期回收时间增加
                        var10000.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
                        var10000 = this.state;
                        // 统计使用时间增加
                        var10000.accumulatedCheckoutTime += longestCheckoutTime;
                        // 将连接从活动队列中移除
                        this.state.activeConnections.remove(oldestActiveConnection);
                        if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                            try {
                                oldestActiveConnection.getRealConnection().rollback();
                            } catch (SQLException var16) {
                                log.debug("Bad connection. Could not roll back");
                            }
                        }
                        //省略该方法的一部分