MyBatis基本入门

这篇文章讲的是MyBatis,首先我们认识下它

什么是MyBatis?


  是一个基于Java的持久层框架,支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。

  MyBatis可以使用简单的XML或注解来配置和映射原生信息,将接口和Java的POJO映射成数据库中的记录。 每个MyBatis应用程序主要都是使用SqlSessionFactory实例的,一个SqlSessionFactory实例可以通过SqlSessionFactoryBuilder获得。SqlSessionFactoryBuilder可以从一个xml配置文件或者一个预定义的配置类的实例获得。

  用xml文件构建SqlSessionFactory实例是非常简单的事情。推荐在这配置中使用类路径资源(classpath、resource),但你可以使用任何reader实例(就是说读取的流),包括用文件路径或file://开头的url创建的实例。MyBatis有一个实用类—Resources,它有很多方法,可以方面地从类路径及其它位置加载资源。

MyBatis的特点


  1. 简单易学,没有任何第三方依赖。只需要两个jar跟映射的sql配置文件即可。

  2. 解除sql与程序代码的耦合,通过提供DAL层(数据访问层,主要用于数据库的访问),将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。sql和代码的分离,提高了可维护性。

  3. 提供映射标签,支持对象与数据库orm字段关系映射(即数据库字段与JavaBean属性名不匹配的情况)

  4. 内部封装了JDBC,使开发者只需要关注sql语句本身,而无须花费精力去处理加载驱动、创建链接、创建statement等繁杂的过程。

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

MyBatis入门案例


搭建MyBatis开发环境

1、创建maven工程
MyBatis基本入门_第1张图片
2、打开pom.xml,导入相关的依赖jar包,我们导入三个,分别是mybatis的核心jar包、mysql数据库的包(用于连接数据库)、junit单元测试的包(用于方便单元测试)
MyBatis基本入门_第2张图片
3、编写User.java实体类,重写toString,并提供相应的get和set方法

package cn.levi.domain;
import java.io.Serializable;
import java.util.Date;
/**
 * User实体类
 *
 * @author Levi
 *
 */
public class User implements Serializable {
     private Integer id; // 用户ID
     private String username;// 用户名
     private Date birthday;// 生日
     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;
     }
     @Override
     public String toString() {
          return "User [id=" + id + ", username=" + username + ", birthday=" + birthday + "]";
     }
}

4、创建IUserDao.java 用户持久层接口,并提供查询所有用户的抽象方法

package cn.levi.dao;
import java.util.List;
import cn.levi.domain.User;
/**
 * 用户持久层接口
 *
 * @author Levi
 *
 */
public interface IUserDao {
     // 查询所有用户
     List findAllUsers();
}

5、在配置文件目录创建SqlMapConfig.xml配置文件(名字可以改成其他的),并导入相关dtd约束

config的dtd约束

添加约束文件后的空白SqlMapConfig.xml


6、通过约束文件的提示,我们要对主配置文件相关项进行编写



<configuration>
     
     <environments default="mysql">
          
          <environment id="mysql">
              
              <transactionManager type="JDBC">transactionManager>
              
              <dataSource type="POOLED">
                   
                   <property name="driver" value="com.mysql.jdbc.Driver" />
                   <property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
                   <property name="username" value="root" />
                   <property name="password" value="admin" />
              dataSource>
          environment>
     environments>
configuration>

configuration:
  就像是Mybatis的总管(根标签),Mybatis的所有配置信息都存放在这里

environments:
  environments里可以配置多个environment,每个environment对应一个数据库环境

  default属性表示默认的数据库环境,与某个environment的id相对应

environment:
  environment通过id属性与其他数据库环境区别。
  它有两个子节点:transactionManager(事务管理器)和dataSource(数据源)

transactionManager:
  在MyBatis中有两种事务管理器类型
  第一种:JDBC
  这个配置直接简单使用了JDBC的提交和回滚设置。它依赖于从数据源得到的连接来管理事务范围。

  第二种:MANAGED
  这个配置几乎没做什么。它从来不提交或回滚一个连接。而它会让容器来管理事务的整个生命周期(比如Spring或JEE应用服务器的上下文)

dataSource:
  在MyBatis中有三种数据源类型
  第一种:UNPOOLED
   这个数据源的实现是每次被请求时简单打开和关闭连接

  第二种:POOLED
   mybatis实现的简单的数据库连接池类型,它使得数据库连接可被复用,不必在每次请求时都去创建一个物理的连接

  第三种:JNDI
  通过jndi从tomcat之类的容器里获取数据源

需要配置的属性
  driver:这是JDBC驱动的Java类的完全限定名
  例如:com.mysql.jdbc.Driver

  url:这是数据库的JDBC URL地址
  例如:jdbc:mysql://localhost:3306/mybatis

  username:登录数据库的用户名
  例如:root

  password:登录数据库的密码
  例如:admin

  defaultTransactionIsolationLevel:默认的连接事务隔离级别

7、在配置文件目录,创建与IUserDao相同目录结构的IUserDao.xml映射配置文件,并导入相关dtd约束

mapper的dtd约束

添加约束文件后的空白IUserDao.xml


8、通过约束文件的提示,在映射配置文件中编写用于执行IUserDao时所需要的sql语句




<mapper namespace="cn.levi.dao.IUserDao">
     
     <select id="findAllUsers" resultType="cn.levi.domain.User">
          select * from user;
     select>
mapper>

什么是映射配置文件?
  它是Java实体类与数据库对象之间的桥梁。
  在实际的使用过程中,一般一个映射配置文件对应一个数据库操作Dao接口

mapper:
  映射配置文件的根节点
  在根节点中支持9个元素insert、update、delete、select、cache、cache-ref、resultMap、parameterMap、sql

namespace属性:
  填写对应接口的全限定类名。因实际开发中,可能会有很多个mapper,同时很多个mapper中也会存在名字相同的方法,所以为了加以区分,这里必须填写接口的全限定类名

select:
  配置查询使用的标签

id属性:(必须配置项)
  命名空间中的唯一标识,mapper中的namespace指定到某个dao接口,此处的id用于指定到接口中的具体方法,故这里应与方法名一致

parameterType属性:(可选配置项,默认mybatis会自动选择处理)
  将要传入语句的参数的完全限定类名或别名,如果不配置,mybatis会通过ParameterHandler根据参数类型默认选择合适的typeHandler进行处理 parameterType 主要指定参数类型,可以是int, short, long, string等类型,也可以是复杂类型(如对象)

resultType属性:(与resultMap二选一配置)
  用以指定返回类型,指定的类型可以是基本类型,可以是java容器,也可以是javabean

resultMap属性:(与resultType二选一配置)
  resultMap用于引用我们通过resultMap标签定义的映射类型,这也是mybatis组件高级复杂映射的关键(多用于数据库名称与实体类属性名不一致时),后面遇到的地方我们在进行详解

9、现在主配置文件与映射配置文件都编写好了,但是两者并没有任何关系,下面我们要在主配置文件中配置映射文件的位置

在根标签下添加下面标签


<mappers>
    <mapper resource="cn/levi/dao/IUserDao.xml" />
mappers>

配置后的主配置文件



<configuration>
     
     <environments default="mysql">
          
          <environment id="mysql">
              
              <transactionManager type="JDBC">transactionManager>
              
              <dataSource type="POOLED">
                   
                   <property name="driver" value="com.mysql.jdbc.Driver" />
                   <property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
                   <property name="username" value="root" />
                   <property name="password" value="admin" />
              dataSource>
          environment>
     environments>
     
     <mappers>
          <mapper resource="cn/levi/dao/IUserDao.xml" />
     mappers>
configuration>

mappers:(必须配置项)
  用于指定在mybatis初始化时,需要引入哪些mapper映射配置文件。有两种配置方法,分别是配置package元素和配置mapper元素

package:
  只需要配置一个name属性用于设置包名,将会自动扫描包下所有的映射配置文件


<mappers>
    <package name="cn.levi.dao"/>
mappers>

mapper:
  通过resource指定具体的配置文件位置,上文我们使用的就是这种方式


<mappers>
    <mapper resource="cn/levi/dao/IUserDao.xml" />
mappers>

  通过class指定接口,然后将接口与对应的xml文件形成映射关系,需要注意的是,这种方式必须保证xml与接口在相同包结构下,且名字必须完全相同。即接口如果为IUserDao.java,那么xml配置文件则必须叫IUserDao.xml


<mappers>
    <mapper class="cn.levi.dao.IUserDao" />
mappers>

  通过url指定配置文件的位置(url地址获取方式:将计算机中的文件拖入浏览器,复制地址栏的地址即可)


<mappers>
    <mapper url="file:///E:/Learn/Java/Workspace/MyBatis01/src/main/resources/cn/levi/dao/IUserDao.xml" />
mappers>

10、因为MyBatis通过动态代理的方式,对接口的方法进行增强,实现对应的功能,所以我们不需要编写接口的实现类,直接编写测试类MyBatisTest.java进行测试即可

package cn.levi.test;
import java.io.InputStream;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import cn.levi.dao.IUserDao;
import cn.levi.domain.User;
/**
 * 测试MyBatis入门案例
 *
 * @author Levi
 *
 */
public class MyBatisTest {
     // 测试查找所有用户方法
     @Test
     public void findAllUsers() throws Exception {
          // 1、获取主配置文件的流对象
          InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
          // 2、使用构建者模式,创建SqlSession工厂
          SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
          // 3、使用工厂生产一个SqlSession对象
          SqlSession session = factory.openSession();
          // 4、获取持久层接口的代理对象
          IUserDao mapper = session.getMapper(IUserDao.class);
          // 5、执行持久层接口中的查询所有用户的方法
          List users = mapper.findAllUsers();
          // 6、遍历集合,打印查询到的对象
          for (User user : users) {
              System.out.println(user);
          }
          // 7 、释放资源
          session.close();
          is.close();
     }
}

测试结果控制台打印
MyBatis基本入门_第3张图片

使用搭建好的MyBatis环境进行单表CRUD操作

1、在持久层接口中添加CRUD相关的方法

package cn.levi.dao;
import java.util.List;
import cn.levi.domain.User;
/**
 * 用户持久层接口
 *
 * @author Levi
 *
 */
public interface IUserDao {
     // 查询所有用户
     List findAllUsers();
     // 根据名称模糊查询用户(不存在sql注入问题)
     List findUserByName1(String username);
     // 根据名称模糊查询用户(存在sql注入问题)
     List findUserByName2(String username);
     // 根据ID查询用户
     User findById(Integer id);
     // 获取总记录条数
     int findUserCount();
     // 保存用户
     void saveUser(User user);
     // 修改用户
     void editUser(User user);
     // 删除用户
     void delUser(Integer id);
}

2、在映射文件中填写持久层接口中的方法要执行的sql语句




<mapper namespace="cn.levi.dao.IUserDao">
     
     <select id="findAllUsers" resultType="cn.levi.domain.User">
          select * from user;
     select>
     
     <select id="findUserByName1" resultType="cn.levi.domain.User" parameterType="String">
          select * from user where username like #{username};
     select>
     
     <select id="findUserByName2" resultType="cn.levi.domain.User" parameterType="String">
          select * from user where username like '%${value}%';
     select>
     
     <select id="findById" resultType="cn.levi.domain.User" parameterType="Integer">
          select * from user where id=#{id};
     select>
     
     <select id="findUserCount" resultType="int">
          select count(*) from user;
     select>
     
     <insert id="saveUser" parameterType="cn.levi.domain.User">
          insert into user (username,birthday)values(#{username},#{birthday});
     insert>
     
     <update id="editUser" parameterType="cn.levi.domain.User">
          update user set username=#{username},birthday=#{birthday} where id=#{id};
     update>
     
     <delete id="delUser" parameterType="Integer">
          delete from user where id=#{id};
     delete>
mapper>

在sql语句中,我们通过#{}代表占位符,如果只有一个参数,#{参数名随意};如果为多个或返回实体类中的多个属性,需要跟属性名保持一致#{实体类属性名一致}

{}与${}的区别?

{}表示一个占位符号

#{}可以实现preparedStatement向占位符中设置值,自动进行java类型和jdbc类型转换
#{}可以有效防止sql注入
#{}可以接收简单类型值或POJO属性值
如果parameterType传输单个简单类型值,#{}括号中可以是value或其它名称

${}表示拼接sql串

${}可以将parameterType传入的内容拼接在sql中且不进行jdbc类型转换
${}可以接收简单类型值或POJO属性值
如果parameterType传输单个简单类型值,${}括号中只能是value

3、编写测试类中对应的测试方法进行测试,因为方法比较多,所以我们把公共部分抽取出来,我们分成四个模块

第一部分:初始化模块(@Before:Junit中的初始化方法)

// 初始化方法
@Before
public void init() throws Exception {
    // 1、获取主配置文件的流对象
    is = Resources.getResourceAsStream("SqlMapConfig.xml");
    // 2、使用构建者模式,创建SqlSession工厂
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
    // 3、使用工厂生产一个SqlSession对象
    session = factory.openSession();
    // 4、获取持久层接口的代理对象
    mapper = session.getMapper(IUserDao.class);
}

第二部分:释放资源模块(@After:Junit中的释放资源方法)

// 释放资源方法
@After
public void destory() throws Exception {
    session.close();
    is.close();
}

第三部分:成员变量抽取(因为要在多个方法中使用同一个对象)

private InputStream is;
private SqlSession session;
private IUserDao mapper;

第四部分:单元测试模块(@Test:Junit中的测试方法)

// 测试查找所有用户方法
@Test
public void findAllUsers() throws Exception {
     List users = mapper.findAllUsers();
     for (User user : users) {
          System.out.println(user);
     }
}
// 测试根据名称模糊查询用户方法(不存在sql注入问题)
@Test
public void findUserByName1() {
     List users = mapper.findUserByName1("%王%");
     for (User user : users) {
          System.out.println(user);
     }
}
// 测试根据名称模糊查询用户方法(存在sql注入问题)
@Test
public void findUserByName2() {
     List users = mapper.findUserByName2("王");
     for (User user : users) {
          System.out.println(user);
     }
}
// 测试根据ID查询用户方法
@Test
public void findById() {
     User user = mapper.findById(41);
     System.out.println(user);
}
// 测试获取总记录条数方法
@Test
public void findUserCount() {
     int rows = mapper.findUserCount();
     System.out.println(rows);
}

上面四个查询方法中,我们不需要进行提交,但是进行增删改操作后,我们需要手动进行提交才可以,使用session.commit();

// 测试保存用户方法
@Test
public void saveUser() {
     User user = new User();
     user.setUsername("利威尔");
     user.setBirthday(new Date());
     mapper.saveUser(user);
     session.commit();
}
// 测试修改用户方法
@Test
public void editUser() {
     User user = mapper.findById(50);
     user.setUsername("阿克曼");
     user.setBirthday(new Date());
     mapper.editUser(user);
     session.commit();
}
// 测试删除用户方法
@Test
public void delUser() {
     mapper.delUser(50);
     session.commit();
}

完整的测试类代码MyBatisTest.java

package cn.levi.test;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import cn.levi.dao.IUserDao;
import cn.levi.domain.User;
/**
 * 测试MyBatis入门案例
 *
 * @author Levi
 *
 */
public class MyBatisTest {
     private InputStream is;
     private SqlSession session;
     private IUserDao mapper;
     // 初始化方法
     @Before
     public void init() throws Exception {
          // 1、获取主配置文件的流对象
          is = Resources.getResourceAsStream("SqlMapConfig.xml");
          // 2、使用构建者模式,创建SqlSession工厂
          SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
          // 3、使用工厂生产一个SqlSession对象
          session = factory.openSession();
          // 4、获取持久层接口的代理对象
          mapper = session.getMapper(IUserDao.class);
     }
     // 释放资源方法
     @After
     public void destory() throws Exception {
          session.close();
          is.close();
     }
     // 测试查找所有用户方法
     @Test
     public void findAllUsers() throws Exception {
          List users = mapper.findAllUsers();
          for (User user : users) {
              System.out.println(user);
          }
     }
     // 测试根据名称模糊查询用户方法(不存在sql注入问题)
     @Test
     public void findUserByName1() {
          List users = mapper.findUserByName1("%王%");
          for (User user : users) {
              System.out.println(user);
          }
     }
     // 测试根据名称模糊查询用户方法(存在sql注入问题)
     @Test
     public void findUserByName2() {
          List users = mapper.findUserByName2("王");
          for (User user : users) {
              System.out.println(user);
          }
     }
     // 测试根据ID查询用户方法
     @Test
     public void findById() {
          User user = mapper.findById(41);
          System.out.println(user);
     }
     // 测试获取总记录条数方法
     @Test
     public void findUserCount() {
          int rows = mapper.findUserCount();
          System.out.println(rows);
     }
     // 测试保存用户方法
     @Test
     public void saveUser() {
          User user = new User();
          user.setUsername("利威尔");
          user.setBirthday(new Date());
          mapper.saveUser(user);
          session.commit();
     }
     // 测试修改用户方法
     @Test
     public void editUser() {
          User user = mapper.findById(50);
          user.setUsername("阿克曼");
          user.setBirthday(new Date());
          mapper.editUser(user);
          session.commit();
     }
     // 测试删除用户方法
     @Test
     public void delUser() {
          mapper.delUser(50);
          session.commit();
     }
}

4、如何获取自增长主键的值?
上文中我们并没有设置id,但是却可以看到有相关的字段值,因为我们在数据库中对id进行了自增长的设置。
MyBatis基本入门_第4张图片

我们先在测试方法中获取自增长主键ID试试看

// 测试获取自增长主键ID
@Test
public void testId() {
     User user = new User();
     user.setUsername("利威尔");
     user.setBirthday(new Date());
     mapper.saveUser(user);
     System.out.println(user.getId());
     session.commit();
}

控制台打印结果
MyBatis基本入门_第5张图片

从图中我们不难发现,新增的id=52,但是在代码中却打印出null
那么问题又来了,我们如何才能在代码中获取到自增长主键的值呢?
SelectKey在Mybatis中是为了解决Insert数据时不支持主键自动生成的问题,他可以很随意的设置生成主键的方式。
  keyColumn:表示查询语句返回结果的列名,简单说就是sql语句中对应的列名
  keyProperty:表示将属性设置到某个列中,简单说就是实体类中对应的属性名
  resultType:表示返回值的类型

我们在保存用户的方法添加代码如下


<insert id="saveUser" parameterType="cn.levi.domain.User">
    <selectKey keyColumn="id" keyProperty="id" resultType="Integer">
        select last_insert_id();
    selectKey>
    insert into user (username,birthday)values(#{username},#{birthday});
insert>

再次测试 测试结果如下
MyBatis基本入门_第6张图片

使用带有Dao实现类的方式进行单表CRUD操作

1、编写UserDaoImpl.java并且继承IUserDao接口,实现其中的抽象方法

package cn.levi.dao.impl;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import cn.levi.dao.IUserDao;
import cn.levi.domain.User;
/**
 * 用户持久层接口实现类
 *
 * @author Levi
 *
 */
public class UserDaoImpl implements IUserDao {
     // 定义factory不是session 是为了防止单例
     private SqlSessionFactory factory;
     // 只提供有参构造 目的是为了让使用者一定要传个工厂才能创建实现类对象
     public UserDaoImpl(SqlSessionFactory factory) {
          this.factory = factory;
     }
     // 查询所有用户
     @Override
     public List findAllUsers() {
          return factory.openSession().selectList("cn.levi.dao.IUserDao.findAllUsers");
     }
     // 根据名称模糊查询用户(不存在sql注入问题)
     @Override
     public List findUserByName1(String username) {
          return factory.openSession().selectList("cn.levi.dao.IUserDao.findUserByName1", username);
     }
     // 根据名称模糊查询用户(存在sql注入问题)
     @Override
     public List findUserByName2(String username) {
          return factory.openSession().selectList("cn.levi.dao.IUserDao.findUserByName1", username);
     }
     // 根据ID查询用户
     @Override
     public User findById(Integer id) {
          return factory.openSession().selectOne("cn.levi.dao.IUserDao.findById", id);
     }
     // 获取总记录条数
     @Override
     public int findUserCount() {
          return factory.openSession().selectOne("cn.levi.dao.IUserDao.findUserCount");
     }
     // 保存用户
     @Override
     public void saveUser(User user) {
          SqlSession session = factory.openSession();
          session.insert("cn.levi.dao.IUserDao.saveUser", user);
          session.commit();
     }
     // 修改用户
     @Override
     public void editUser(User user) {
          SqlSession session = factory.openSession();
          session.update("cn.levi.dao.IUserDao.editUser", user);
          session.commit();
     }
     // 删除用户
     @Override
     public void delUser(Integer id) {
          SqlSession session = factory.openSession();
          session.delete("n.levi.dao.IUserDao.delUser", id);
          session.commit();
     }
}

成员变量为什么定义的是factory而不是session呢?
从代码中我们可以看出,每个方法都会调用factory.opensession()来获取一个session对象,所以很容易就会想到为什么不把这一部分一起抽取出来,而要选择定义factory呢。原因很简单,如果我们的实现类是单例的,那么它只会执行一次初始化操作,那么就只会存在一个session对象,也就是说session也变成单例的了。当我们操作某一个方法调用完session后进行session.close()操作,这个session就会被销毁。其他方法在想调用session,就会报错了。所以我们通过工厂来解决这个问题,即便工厂是单例的,每次调用方法也都会生成一个新的session对象。

2、编写完接口的实现类,接下来我们就可以编写新的测试类了,对通过接口实现类的方式进行测试

package cn.levi.test;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import cn.levi.dao.IUserDao;
import cn.levi.dao.impl.UserDaoImpl;
import cn.levi.domain.User;
/**
 * 测试通过Dao接口实现类实现的方式
 *
 * @author Levi
 *
 */
public class MyBatisTest2 {
     private InputStream is;
     private IUserDao dao;
     // 初始化方法
     @Before
     public void init() throws Exception {
          // 1、获取主配置文件的流对象
          is = Resources.getResourceAsStream("SqlMapConfig.xml");
          // 2、使用构建者模式,创建SqlSession工厂
          SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
          // 3、使用Dao接口的实现类 传入一个工厂进行初始化
          dao = new UserDaoImpl(factory);
     }
     // 释放资源方法
     @After
     public void destory() throws Exception {
          is.close();
     }
     // 测试查找所有用户方法
     @Test
     public void findAllUsers() throws Exception {
          List users = dao.findAllUsers();
          for (User user : users) {
              System.out.println(user);
          }
     }
     // 测试根据名称模糊查询用户方法(不存在sql注入问题)
     @Test
     public void findUserByName1() {
          List users = dao.findUserByName1("%王%");
          for (User user : users) {
              System.out.println(user);
          }
     }
     // 测试根据名称模糊查询用户方法(存在sql注入问题)
     @Test
     public void findUserByName2() {
          List users = dao.findUserByName2("王");
          for (User user : users) {
              System.out.println(user);
          }
     }
     // 测试根据ID查询用户方法
     @Test
     public void findById() {
          User user = dao.findById(41);
          System.out.println(user);
     }
     // 测试获取总记录条数方法
     @Test
     public void findUserCount() {
          int rows = dao.findUserCount();
          System.out.println(rows);
     }
     // 测试保存用户方法
     @Test
     public void saveUser() {
          User user = new User();
          user.setUsername("利威尔");
          user.setBirthday(new Date());
          dao.saveUser(user);
     }
     // 测试修改用户方法
     @Test
     public void editUser() {
          User user = dao.findById(50);
          user.setUsername("阿克曼");
          user.setBirthday(new Date());
          dao.editUser(user);
     }
     // 测试删除用户方法
     @Test
     public void delUser() {
          dao.delUser(50);
     }
     // 测试获取自增长主键ID
     @Test
     public void testId() {
          User user = new User();
          user.setUsername("利威尔");
          user.setBirthday(new Date());
          dao.saveUser(user);
          System.out.println(user.getId());
     }
}

通过测试,我们也实现了单表CRUD的全部操作。但是这种方式相比之前通过代理Dao实现的方式存在不足。
我们将增删改的提交的代码(session.commit())写到了UserDaoImpl中,但实际开发中,事务的提交是业务层的事,不应该放到持久层中,所以还是建议通过代理Dao的方式而不是Dao实现类的方式

对之前的主配置文件使用properties标签进行适当的改进

1、先来看下之前我们在主配置文件中对数据源的配置部分

<dataSource type="POOLED">
    
    <property name="driver" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
    <property name="username" value="root" />
    <property name="password" value="admin" />
dataSource>

从上面我们可以看出一个问题,我们把数据库的配置文件写死到了xml配置文件中,这样做的弊端是,每次我们要修改数据库信息,都要去主配置文件中找到对应数据源配置的部分,进行修改,这显然不是我们想要的,此时我们可以通过properties标签进行适当的优化。


<properties>
     <property name="jdbc.driver" value="com.mysql.jdbc.Driver" />
     <property name="jdbc.url" value="jdbc:mysql://localhost:3306/mybatis" />
     <property name="jdbc.username" value="root" />
     <property name="jdbc.password" value="admin" />
properties>

数据源部分我们通过EL表达式进行调整

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

这时我们已经把数据从原来的数据源标签移到了properties标签中了,但是与配置文件还是绑定在了一起

有没有办法能把这部分数据库连接信息移到properties的配置文件中去呢?
答案是肯定的
在properties标签中,为我们提供了两种读取配置文件的方式的属性

resource属性:
  用于指定外部的资源文件(properties文件),读取的位置是类路径下,写法是包名/包名/文件名

 
 <properties resource="jdbcConfig.properties">properties>

jdbcConfig.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis
jdbc.username=root
jdbc.password=admin

url属性:
  用于指定外部资源文件,他可以读取任意能访问的路径下的文件,但写法必须是URL的方式(拖拽到浏览器即可)

<properties
    url="file:///E:/Learn/Java/Workspace/MyBatis01/src/main/resources/jdbcConfig.properties">
properties>

如果同时在多处进行配置,读取的优先级是怎样的呢?
1、在properties内部自定义的属性值第一个被读取


<properties>
     <property name="jdbc.driver" value="com.mysql.jdbc.Driver" />
     <property name="jdbc.url" value="jdbc:mysql://localhost:3306/mybatis" />
     <property name="jdbc.username" value="root" />
     <property name="jdbc.password" value="admin" />
properties>

2、然后读取resource路径表示文件中的属性,如果有它会覆盖已经读取的属性;如果resource路径不存在,那么读取url表示路径文件中的属性,如果有它会覆盖第一步读取的属性值

<properties resource="jdbcConfig.properties">properties>

3、最后读取parameterType传递的属性值,它会覆盖已读取的同名的属性

<select id="findUserByName" resultType="cn.levi.domain.User" parameterType="String">
    select * from user where username like '%${jdbc.username}%';
select>

对之前的映射配置文件使用别名进行适当的改进

我们在编写映射配置文件的时候,总是要写cn.levi.domain.User
当配置文件很多的时候,是比较耗费时间与精力的,当需要修改的时候,也要将所有的位置统一进行修改,严重影响了开发效率与出错的几率
这时候就需要我们使用别名的方式来进行优化,在SqlMapConfig.xml中,我们添加下面的标签


<typeAliases>
    <typeAlias type="cn.levi.domain.User" alias="user"/>
typeAliases>

如果有很多实体类的时候,我们就要使用另一种配置方式了


<typeAliases>
    <package name="cn.levi.domain"/>
typeAliases>

这种方式默认将包中的类名作为别名使用,以上两种方式的别名都不区分大小写。

可能有的同学会有疑问,为什么有些类似int类型的也可以呢?
这是因为myBatis已经预先注册好了常用的一些类型的别名

那么我们如何查看都有哪些呢?
打开eclipse,Ctrl + Shift + T 输入TypeAliasRegistry
MyBatis基本入门_第7张图片
点进TypeAliasRegistry,在源码中我们就可以看到MyBatis中定义的别名了

    registerAlias("string", String.class);
    registerAlias("byte", Byte.class);
    registerAlias("long", Long.class);
    registerAlias("short", Short.class);
    registerAlias("int", Integer.class);
    registerAlias("integer", Integer.class);
    registerAlias("double", Double.class);
    registerAlias("float", Float.class);
    registerAlias("boolean", Boolean.class);
    registerAlias("byte[]", Byte[].class);
    registerAlias("long[]", Long[].class);
    registerAlias("short[]", Short[].class);
    registerAlias("int[]", Integer[].class);
    registerAlias("integer[]", Integer[].class);
    registerAlias("double[]", Double[].class);
    registerAlias("float[]", Float[].class);
    registerAlias("boolean[]", Boolean[].class);
    registerAlias("_byte", byte.class);
    registerAlias("_long", long.class);
    registerAlias("_short", short.class);
    registerAlias("_int", int.class);
    registerAlias("_integer", int.class);
    registerAlias("_double", double.class);
    registerAlias("_float", float.class);
    registerAlias("_boolean", boolean.class);
    registerAlias("_byte[]", byte[].class);
    registerAlias("_long[]", long[].class);
    registerAlias("_short[]", short[].class);
    registerAlias("_int[]", int[].class);
    registerAlias("_integer[]", int[].class);
    registerAlias("_double[]", double[].class);
    registerAlias("_float[]", float[].class);
    registerAlias("_boolean[]", boolean[].class);
    registerAlias("date", Date.class);
    registerAlias("decimal", BigDecimal.class);
    registerAlias("bigdecimal", BigDecimal.class);
    registerAlias("biginteger", BigInteger.class);
    registerAlias("object", Object.class);
    registerAlias("date[]", Date[].class);
    registerAlias("decimal[]", BigDecimal[].class);
    registerAlias("bigdecimal[]", BigDecimal[].class);
    registerAlias("biginteger[]", BigInteger[].class);
    registerAlias("object[]", Object[].class);
    registerAlias("map", Map.class);
    registerAlias("hashmap", HashMap.class);
    registerAlias("list", List.class);
    registerAlias("arraylist", ArrayList.class);
    registerAlias("collection", Collection.class);
    registerAlias("iterator", Iterator.class);
    registerAlias("ResultSet", ResultSet.class);

按住Ctrl点击registerAlias,选择Open Implementation,查看这个方法的实现

  public void registerAlias(String alias, Class value) {
    if (alias == null) {
      throw new TypeException("The parameter alias cannot be null");
    }
    // issue #748
    String key = alias.toLowerCase(Locale.ENGLISH);
    if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
      throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
    }
    TYPE_ALIASES.put(key, value);
  }

在这里能看到,MyBatis将传入的字符串进行了转换,无论传入的是大写还是小写,都会默认转成小写字母,这就是我们使用别名时不区分大小写的原因。

相似的还有一个地方可以进行调整
比如之前我们指定映射配置文件的时候,使用的标签如下


<mappers>
    <mapper resource="cn/levi/dao/IUserDao.xml" />
mappers>

如果有很多个接口的话,那么就需要配置很多个mapper标签,现在我们可以同样使用包的方式进行优化

<mappers>
    <package name="cn.levi.dao"/>
mappers>

我们可能还会有疑问,mapper到底是什么东西呢?
目前来看,我们在mapper中写的都是关于dao接口的东西,其实很多人喜欢将dao写成mapper,即把我们写的IUserDao叫成IUserMapper,其实说的都是同一个意思,代表的都是持久层的接口。以后在开发中如果遇到别人写的xxxMapper文件时,你就要明白,其实他写的是个持久层接口就可以了。

MyBatis中的连接池

什么是连接池:
  用于存放连接的容器,它里面全都是Connection对象。
  容器一般指的就是一个集合(连接池的集合是一个List)

连接池的作用和优势
  作用:解决在项目使用过程中获取连接消耗时间的问题
  优势:由于在项目一加载时,已经创建好了连接,所以在项目运行过程中,就不会再消耗创建连接的时间了。

连接池实现可能遇到的问题以及解决
  close方法不能真正的关闭连接(否则用一个关一个,连接池就没有连接可用了)

  解决:
  对原有的close方法进行升级,把它增强,让他在执行时不是真正的关闭,而是加回到池中。
  可以采用:动态代理技术或者装饰者模式来实现

常用的连接池技术
  C3P0、DBCP、基于tomcat服务器(不只是tomcat)的JNDI

JNDI:Java Named and Directory Interface(java命名和目录服务接口)

mybatis中连接池的实现机制
  它用了两个集合来维护:
    idleConnections:空间的连接池
    activeConnections:活动的连接池

mybatis框架:
  会先去空闲池中看是否有空闲的连接
  有:
    直接拿着用
  没有:
     再看活动池中是否到了最大活动数:
      没到:
         创建一个新的连接,并加入到活动池中
      到了:
        取出活动池中最老的一个连接进行重置

mabatis中的连接池配置
  在主配置文件的dataSource标签中,有一个type属性,用于指定是否使用连接池

   取值:
    POOLED:使用连接池
    UNPOOLED:不使用连接池
    JNDI:使用web服务器中提供命名目录接口来获取数据源

MyBatis中的事务控制

在openSession中,我们能看到好多重载的方法

 SqlSession openSession();
 SqlSession openSession(boolean autoCommit);
 SqlSession openSession(Connection connection);
 SqlSession openSession(TransactionIsolationLevel level);

我们选择了比较熟悉的四个进行说明
  第一个空参的,是我们最常用的方式。
  第二个需要传入是否自动提交,当为true时,我们无需进行手动提交;false时与第一种一样
  第三个需要传入一个连接,它可以实现多条语句共用一个Connection,在转账案例中,我们就需要使用同一个连接
  第四个可以设置事务的隔离级别

Java程序控制事务的本质
  设置connection对象的提交为手动
  setAutoCommit(false);

事务控制的要求
  事务的控制位置:应该在业务层。
  要求业务层方法全部走完再决定事务是否提交/回顾

MyBatis传递POJO包装对象

开发中通过pojo传递查询条件,查询条件是综合的查询条件,不仅包括用户查询条件还包括其他的查询条件,这时我们可以使用包装对象传递输入参数,即POJO类中包含POJO

需求:根据用户名查询用户信息,查询条件放到QueryVo的user属性中

第一步:新建一个QueryVo的实体类,里面提供一个User对象属性

package cn.levi.domain;
import java.io.Serializable;
/**
 * 查询的条件对象
 *
 * @author Levi
 *
 */
public class QueryVo implements Serializable {
     private User user;
     public User getUser() {
          return user;
     }
     public void setUser(User user) {
          this.user = user;
     }
}

第二步:在Dao接口中添加通过对应的抽象方法

// 根据查询条件中的对象 查询用户列表
List findByQueryVo(QueryVo vo);

第三步:在映射配置文件中配置对应的方法


<select id="findByQueryVo" resultType="user parameterType="queryvo">
    select * from user where username like #{user.username};
select>

这里我们不能直接使用username like #{username}
因为我们的返回值类型是queryvo,并没有username这个属性值,所以我们要用user.username
这种写法有个专业的叫法是OGNL表达式,它不是MyBatis独有的,是apache的
我们这里使用的仅仅是其中的一种格式
详情可见:https://commons.apache.org/proper/commons-ognl/language-guide.html
MyBatis基本入门_第8张图片

第四步:添加测试方法

// 测试查询用户列表方法(QueryVo)
@Test
public void findByQueryVo() {
     QueryVo vo = new QueryVo();
     User user = new User();
     user.setUsername("%王%");
     vo.setUser(user);
     List users = mapper.findByQueryVo(vo);
     for(User u:users){
          System.out.println(u);
     }
}

第五步:测试结果
MyBatis基本入门_第9张图片

使用ResultMap将字段与属性名进行对应

  在上面的案例中,我们留有一个地方没有讲到。之前的代码中,我们的POJO的属性名称与sql查询的列名是一致的,所以可以直接将指定的查询结果映射为POJO。
  但是很多情况下,我们不可能把所有的POJO属性名都与数据库中对应的列名保持一致,那么该怎么办呢?
  这时候我们就可以使用resultMap将字段名和属性名做一个对应关系,resultMap实质上还需要将查询结果映射到POJO对象中。
  resultMap可以实现将查询结果映射为复杂类型的POJO,比如在查询结果映射对象中包括POJO和list实现一对一查询和一对多查询。

下面我们通过代码来体会resultMap的作用

测试思路分析:
由于原有代码中很多地方都用到了User实体类,改动之后其他的代码也会受到影响。我们这里只是简单的说明下resultMap的作用,所以不准备修改原有的User实体类属性名,但是还要保证与数据库中列的字段名称不一致的需求,这时候我们想到了可以使用别名来达到我们想要的效果

1、我们打开映射配置文件,对刚刚编写的QueryVo的查询方法sql语句进行修改

    
    <select id="findByQueryVo" resultType="user" parameterType="queryvo">
        select id newId,username newUsername,birthday newBirthday from user where username =#{user.username};
    select>

2、测试结果 发现无法查询到数据 结果为null
MyBatis基本入门_第10张图片

3、接下来我们使用resultMap标签将字段进行对应


<resultMap type="user" id="userMap">
    
    <id column="newId" property="id" />
    
    <result column="newUsername" property="username" />
    <result column="newBirthday" property="birthday" />
resultMap>

resultMap中的type属性:
  封装的类型即对应的JavaBean,因为在之前的代码中我们已经通过包的方式配置了别名,所以我们可以直接写user

resultMap中的id属性:
  定义唯一的标识,其他标签也是通过该id值对resultMap进行引用

resultMap的子节点id:
  定义查询结果集的唯一标识,通常只有一个,代表主键id。需要注意的是,多数情况下只需要一个主键id就可以进行唯一的约束,但如果需要用到多个字段为复合唯一约束条件,则这里就要定义多个了

resultMap的子节点result:
  定义除主键id外的其它需要对应的字段

column属性:
  sql查询出来后对应的字段名

property属性:
  表示JavaBean中对应属性的名称

4、定义完成resultMap后,仍然不能实现我们想要的结果,因为我们使用的仍旧是resultType属性值,并没有与resultMap产生关联,这时候我们需要使用到另外一个属性resultMap,属性值是要使用的resultMap的id

    
    <select id="findByQueryVo" resultMap="userMap" parameterType="queryvo">
        select id newId,username newUsername,birthday newBirthday from user
        where username =#{user.username};
    select>

5、到这里我们就完成了resultMap的对应关系,运行测试方法结果如下

测试方法

    // 测试查询用户列表方法(QueryVo)
    @Test
    public void findByQueryVo() {
        QueryVo vo = new QueryVo();
        User user = new User();
        user.setUsername("老王");
        vo.setUser(user);
        List users = mapper.findByQueryVo(vo);
        for (User u : users) {
            System.out.println(u);
        }
    }

运行结果 可以查到正确的数据了
MyBatis基本入门_第11张图片

动态SQL语句之if标签与where标签

当比较简单的sql语句已经无法满足较为复杂的业务逻辑时,我们就需要sql语句也是可以动态变化的。根据实体类的取值不同,使用不同的sql语句进行查询。比如当id不为空时可以根据id查询,当username不为空时,可以在原有基础上在加入根据username进行查询。

下面我们还是在findByQueryVo的基础上添加if标签进行修改
需求:输入名字时,根据名字查询;不输入名字,查询所有用户

映射配置文件

    
    <select id="findByQueryVo" resultType="user" parameterType="queryvo">
        select * from user where 1=1
        <if test="user.username!=null and user.username!=''">
            and username =#{user.username}
        if>
    select>

测试方法

    // 测试查询用户列表方法(QueryVo)
    @Test
    public void findByQueryVo() {
        QueryVo vo = new QueryVo();
        User user = new User();
        user.setUsername("老王");
        vo.setUser(user);
        List users = mapper.findByQueryVo(vo);
        for (User u : users) {
            System.out.println(u);
        }
    }

第一次运行测试方法 包含有user.setUsername(“老王”);
MyBatis基本入门_第12张图片

第二次运行测试方法 注释掉user.setUsername(“老王”);
MyBatis基本入门_第13张图片

注意:

  1. if标签中的test属性写的是对象的属性名
  2. 如果对象是包装类的话(例如本例中的QueryVo中是一个User对象),则对象要使用OGNL表达式的写法(之前讲MyBatis传递POJO包装对象的时候有提到过)
  3. 表达式的结果为布尔值,除此之外所有的非0值都为true(test=”0”结果是false)

上面我们通过添加了一个where1=1来实现拼接操作,但是这样显得很不专业,我们可以使用where标签来解决这个问题,where标签可以自动处理第一个and

    
    <select id="findByQueryVo" resultType="user" parameterType="queryvo">
        select * from user
        <where>
            <if test="user.username!=null and user.username!=''">
                and username =#{user.username}
            if>
        where>
    select>

动态SQL语句之foreach标签

上面我们只是将一个值动态添加到sql语句中作为查询条件,当我们需要将一个集合中的值,作为参数动态添加进来的时候,就需要使用到foreach标签了

我们先在QueryVo中新增一个成员变量,用于存储所有的id值

package cn.levi.domain;

import java.io.Serializable;
import java.util.List;

/**
 * 查询的条件对象
 * 
 * @author Levi
 *
 */
public class QueryVo implements Serializable {
    private User user;

    private List ids;// 所有id的集合

    public List getIds() {
        return ids;
    }

    public void setIds(List ids) {
        this.ids = ids;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

}

然后修改映射配置文件

    <select id="findByQueryVo" resultType="user" parameterType="queryvo">
        select * from user
        <where>
            <foreach collection="ids" open="id in(" item="id" close=")" separator=",">
                #{id}
            foreach>
        where>
    select>

需求:查询id为41、42、43的用户列表
完整sql语句:select * from user where id in(41,42,43);
foreach标签:用户遍历集合

  • collection:代表要遍历的元素集合,注意编写时不要写#{}
  • open:代表语句的开始部分
  • close:代表结束部分
  • item:代表遍历集合的每个元素,生成的变量名,要于foreach标签中#{}的内容保持一致
  • sperator:代表分隔符

修改测试方法

    @Test
    public void findByQueryVo() {
        QueryVo vo = new QueryVo();
        List ids = new ArrayList();
        ids.add(41);
        ids.add(42);
        ids.add(43);
        vo.setIds(ids);
        List users = mapper.findByQueryVo(vo);
        for (User u : users) {
            System.out.println(u);
        }
    }

查看控制台打印结果
MyBatis基本入门_第14张图片

到这里,看起来已经没什么问题了,但是确实是存在问题的
如果我们把测试方法中setIds方法注释掉,即集合为空时,再次运行查看结果
MyBatis基本入门_第15张图片

这时候测试报错了,因为我们的ids现在是null
那么该如何解决呢?
很简单,只需要利用刚刚学过的if标签进行判断即可

    <select id="findByQueryVo" resultType="user" parameterType="queryvo">
        select * from user
        <where>
            <if test="ids!=null and ids.size>0">
                <foreach collection="ids" open="id in(" item="id" close=")" separator=",">
                    #{id}
                foreach>
            if>
        where>
    select>

再次测试,当ids为空时,默认查询所有
MyBatis基本入门_第16张图片

简写的SQL片段及如何重用

当多条sql语句中包含重复的部分时(例如查询所有select * from user),这个时候我们可以将重复的这部分sql语句抽取出来,定义到sql标签中。

    
    <sql id="defaultAll">
        select * from user
    sql>

然后我们修改原来的查询所有用户的方法为

    
    <select id="findAllUsers" resultType="user">
        <include refid="defaultAll">include>
    select>

其中include标签的refid属性值,就是sql标签的id属性值
如果需要引用的是其他mapper.xml映射文件中的sql,则要加上namespace
例如:refid=”cn.levi.dao.IAccountDao.defaultAll”

最近精力有限 先写到这
以后抽时间慢慢补
未完待续——

你可能感兴趣的:(MyBatis基本入门)