MyBatis
MyBatis的基础
根据MyBatis的官方介绍:
整个测试项目结构如下:使用Maven架构项目
pom.xml : 所需依赖如下
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
mybatis-config.xml : mybatis的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "mybatis-3-config.dtd" >
<configuration>
<!-- 可以配置多个运行环境,但是每个SqlSessionFactory 实例只能选择一个运行环境 -->
<environments default="work">
<environment id="work">
<transactionManager type="JDBC"></transactionManager>
<!-- UNPOOLED POOLED JNDI -->
<dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mytest" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="StudentMapper.xml"/>
</mappers>
</configuration>
StudentMapper.xml : 映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "mybatis-3-mapper.dtd" >
<mapper namespace="com.test.mybatis.dao.StudentMapper">
<!--保存一个学员信息-->
<insert id="saveStudent" parameterType="com.test.mybatis.pojo.Student">
insert into student values(default,#{username},#{address},#{age},#{hobby})
</insert>
</mapper>
测试代码:
public class MainTest {
public static void main(String[] args) throws IOException {
Student stu = new Student("Tonny","断背山",22,"吃喝嫖赌");
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = factory.openSession();
sqlSession.insert("saveStudent", stu);
sqlSession.commit();
sqlSession.close();
}
}
运行后,我们向数据库插入了一个学员信息,检测程序运行情况:
MyBatis的执行流程
我们看大概的的执行过程:源码部分真的太难了,我这菜鸟暂时还是看不懂,只能知道这个大概
1.将sql语句和数据库相关的配置保存在配置文件中
并将其作为属性交给SqlSessionFactory接口的实现DefaultSqlSessionFactory;
3.SqlSessionFactory工厂在创建SqlSession对象时提供相关属性:
1> Configuration对象 贯穿MyBatis执行的整个流程
2> dirty :true sql执行成功后 ,可以事务提交
false sql语句发送失败,事务进行回滚
3> Executor执行器对象(真正动手的对象)
创建 Statement对象,在创建过程中依靠MapperStatement对象将赋值内容和sql占位符进行绑定
\4. SqlSession.commit() 根据 dirty的Boolean 值确定是提交还是回滚
第二轮总结性笔记
通过MyBatis核心配置文件如何找到映射文件的几种方式
在核心配置文件中有一个标签 <mappers> </mappers>
其中允许存放的类型有如下几种,分别代表了不同的读取策略和响应的规则:
关于上面这只目录结构我想说的是:
这里有坑1:在Resourse创建文件夹的时候,给我创建成了com.test.nybatis.dao 这一个文件夹,导致我半天没找到错误
这里有坑2:如果我们把映射文件放在Java目录中的Dao目录下,在打包时不会打包,需要配置相关插件才行:
当我们的实体类和数据库的字段类型不一致时,类型转换器就能起到很好的一个的转换作用,达到数据类型一致
今天我们的列子是 数据库中字段数据类型为 int 实体类中的属性类型为 Boolean
下面我们通过 TypeHandler 对其进行转换
写一个类实现 TypeHandler接口,并重写其中的几个方法,我们对其中两个方法重新实现
/**
* 自定义类型转换器
*/
public class MyTypeHandler implements TypeHandler {
/**
* 这个方法,是在想数据库中插入类型不匹配时会调用的转换方法
*/
@Override
public void setParameter(PreparedStatement preparedStatement, int i, Object o, JdbcType jdbcType) throws SQLException {
if (o == null){
preparedStatement.setInt(i, 0);
return;
}
Boolean flag = (Boolean)o;
Integer value = flag?1:0;
preparedStatement.setInt(i, value);
System.out.println("经过了类型转换器");
}
/**
* 这个方法,是在想数据库中查询结果不匹配实体类时会调用的转换方法
*/
@Override
public Object getResult(ResultSet resultSet, String s) throws SQLException {
System.out.println("经过了类型转换器 int 变 Boolean");
int gender = resultSet.getInt(s); //从结果集中获取全部为int的字段
Boolean flag = Boolean.FALSE;
if (gender == 1){ //如果性别为1 则返回 true封装到Student实体中
flag = Boolean.TRUE;
}
return flag;
}
然后再核心配置文件中注册这个类型转换器:
<!--注册自定义类型转换器-->
<typeHandlers>
<typeHandler handler="com.test.mybatis.Handler.MyTypeHandler" javaType="Boolean" jdbcType="NUMERIC" />
</typeHandlers>
在映射文件中指定使用该类型转换器的场合:
<resultMap id="StudentResultMap" type="Student">
<result column="gender" property="gender" typeHandler="com.test.mybatis.Handler.MyTypeHandler"></result>
</resultMap>
<!--插叙一个学员信息,resultMap指定了一个的一个结果类型 在该类型中设有类型转换器-->
<select id="findStuById" parameterType="int" resultMap="StudentResultMap">
select * from student where id = #{id}
</select>
然后我们测试一下 是否可以将实体类中的 Boolean类型的数据通过类型转换器转换为 int 类型插入到数据库中
注意查看gender属性和gender字段的值:
类型转换器生效,boolean 转换为 int 插入到数据库
值得注意的事,类型转换器会对整个表都为某个类型的字段做转换,不能指定为单个字段做转换
但是可以在查询的时候为指定某一个字段转换 ResultMap标签
ObjectFactory对象工厂的运用
这个就比较简单了,在MyBatis为我们对象关系映射的时候会创建实体类的对象实列,我们可以操作这个创建过程
下面的列子是数据库中有一个字段在实体类中并没有相应的属性与其对应,但我们可以为其赋值最后一并作为查询接轨返回
/**
* 继承高抽象类 重写create方法,有点代理的意思在里面
* 还是利用父类创建结果封装对象,我们对其增强了
*/
public class MyObjectFactory extends DefaultObjectFactory {
@Override
public Object create(Class type) {
com.test.mybatis.pojo.Student stu = null;
if (type == Student.class) {
com.test.mybatis.pojo.Student student = (Student) super.create(type); //利用父类方法创建对象
student.setCountry("China"); //数据库是没有这个字段的,我们手动封装属性
return student; //将代理对象返回
}
return super.create(type);
}
}
其次就是在核心配置文件中注册这个对象工厂类 :
<objectFactory type="com.test.mybatis.Factory.MyObjectFactory" />
然后我们查询看看: 注意看 country属性,在数据库是没有该字段的,我们为其赋值了
Plugins拦截器的使用
plugins的意思为插件,其实就是拦截的意思,所有的插件都是利用代理的方式,通过拦截,实现增强。
/**
* @Intercepts 表示当前类为一个Intercepts实现类,其职位 @Signature数组
* @Signature 通过方法名 接口 参数类型 指定要拦截的方法
*/
@Intercepts({
@Signature(method="query",type = Executor.class,args = {
MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class
})
})
public class MyPlugins implements Interceptor {
/**
* 被拦截时要执行的方法
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
ArrayList list = (ArrayList) invocation.proceed(); //获取拦截对象
Iterator iterator = list.iterator();
if (iterator.hasNext()){
Student stu = (Student) iterator.next();
String old = stu.getAddress();
stu.setAddress("北海道");
System.out.println("经过Plugins拦截器--address的原值:" + old + " " + "增强后的值:" + stu.getAddress());
}
return list;
}
/**
* 封装目标对象的方法,可以返回本身或者代理对象
* target : 目标
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
我们写了一个自己的拦截器来覆盖父类的拦截诶起,实现自己的逻辑,将其注册:
<!--注册找一个拦截器 Plugins-->
<plugins>
<plugin interceptor="com.test.mybatis.Plugins.MyPlugins" />
</plugins>
然后测试查询一下:
这里值得注意的是该拦截器只能拦截四中类型的接口:
Executor.class :
MyBatis的执行器,真正干活的接口,SqlSession只是个牌面
StatementHandler.class :
在Executor执行查询的时候,会创建它
它的任务就是和数据库对话。在它这里会使用parameterHandler和ResultHandler对象为我们绑定SQL参数和组装最后的结果返回。
ParameterHandler.class :
对预编译语句进行参数绑定,完成预编译参数的设置
ResultSetHandler.class :一般不用,不做介绍
第三轮总结性笔记
#{}和 ${}的区别及使用场景
在MyBatis中提供了两种方式读取配置参数到 sql 语句中,完成查询,
# { } : 实现的是向prepareStatement中的预处理语句中设置参数值,sql语句中#{}表示一个占位符即?,
通过预编译的方式,可以防止sql注入的安全问题
$ { } : 采用直接赋值的方式注入值,无法防止sql注入,但是也有存在的必要性,
比如指定动态的表名或者动态的指定排序的字段 也需要 $ { }
这里值得注意的是:因为是拼接注入,如果是字符串记得要把值引号引起来,不然报异常
ResultType 和 ResultMap不做讲解
因为太简单了,难的写Demo,ResultMap一般就用于数据库字段和实体类属性不一致时,我们进行手动绑定
动态SQL
加入我们有一个电脑商品实体类,它有几个简单的属性,品牌、价格、显卡级别、处理器级别、屏幕
当一个用户只对他的品牌做了要求,则我们的查询条件只动态包含 : where 品牌 = ?
当用户对品牌和显卡做了要求 : where 品牌 = ? and 显卡 = ?
如何实现这种动态的sql语句的编写呢?动态Sql就是面对的这种情况,下面看代码:
if 的使用:
choose 、 when 、otherwise : 对应比喻为 switch case default
简单的就不演示,一笔带过,稍微复杂的记一下:
where : 可以自动的将第一个条件前面的逻辑运算符(or,and)去掉
set : 可以把拼接sql后的 , 自动屏蔽掉
trim : 刚开始以为是去除字符串前的空格,NO! 他表示替换的意思
上面三个都具有一定的对比性,所以写在一起,下面看看一个比较复杂的
foreach 遍历集合,将得到的值填充到sql语句中
foreach 遍历集合,将得到的值填充到sql语句中
遍历集合中的元素作为查询条件:
当然还可以遍历数组, collection="array",其他无特别变化
之外还可以遍历Map,下面我们一起来看看:
这里注意在mapper中要使用注解才可以在映射文件中,调用对应的方法,获得值,不然会报错,我卡了一会在这儿
一对多环境 :collection
在很多时候,我们都会使用到级联查询,今天的列子是班级和学生,一个班级有多个学生,一对多的关系
在班级实体类中除了有班级的信息以外,还有一个 List<Student> 属性,用来表示该班级下多个学生的集合
映射文件如下所示:
关于上面这个映射文件我有点疑惑的是:按理说Student建立映射关系时,字段和属性名相同,应该可以省略绑定的啊,但是好像不行
还有另一种方式可以实现多对一的映射关系: 特别的地方我已经高亮显示了
下面这一种方式,在配置了延迟加载是是具有延迟加载功效的,
property=stuList 表示多的一方的数据封装集合的属性名
ofType=Student 表示集合中的对象实体
select=**** 这里采用了省略的方法,其实应该是全路径接口.方法名这种方式,然后该方法会再调用与之关联的sql标签
比如:select="com.test.dao.ClassDao.findStuByClassId"
column=****的意思就是以那个上方那个属性作为检索条件,必须得和该findStuByClassId的入参一致
多对一的关系,其实多个学生对应一个班级,但是我们在查询的时候,和一对一差不多
当然除了使用association以外,我们也可以专门新建一个Vo类用来接受检索的结果集
多对多环境:一对多 + 一张中间表
列子我就不写了,简单口述一下即可,我们有一个用户表,一个角色表,一个用户可能有多个角色,而一个角色可能被多个用户所拥有,
这个时候我们就创建一个用户角色中间表,将多对多的关系拆成两个一对多的关系即可
实现为:在用户类中添加一个List<jiaose>属性 ,在角色类中添加一个List<用户>属性,完成拆分
MyBatis的注解开发
常用注解说明
@Insert :实现新增
@Update :实现更新
@Delete :实现删除
@Select :实现查询
@Result :实现结果集封装
@Results :可以与@Result 一起使用,封装多个结果集
@ResultMap :实现引用@Results 定义的封装
@One :实现一对一结果集封装
@Many :实现一对多结果集封装
@SelectProvider : 实现动态 SQL 映射
@CacheNamespace :实现注解二级缓存的使用,标注在接口上
public interface IUserDao {
//查询所有
//注解详细说明: @Results代替的是<resultMap>,该注解中可以使用单个@Result也可以使用多个成集合,如下所示
//@Result注解内:id(是否为主键字段)、column(数据库字段名)、property(属性名)
//这里如果涉及到一对一级联查询为 : @Result(column=" ",property=" ",one=@One(select="com.test.dao.IUserDao.findById"))
//这里如果涉及到一对多级联查询为 : @Result(column=" ",property=" ",many=@Many(select="com.test.dao.IAccDao.findByUid"))
//如果想使用延迟加载为(一对一同理): many=@Many(select="com.test.dao.IAccDao.findByUid",fetchType=FetchType.LAZY
//fetchType 属性:代表加载方式,一般如果要延迟加载都设置为 LAZY 的值
@Select("select * from user")
@Results(id="userMap",
value= {
@Result(id=true,column="id",property="userId"),
@Result(column="username",property="userName"),
@Result(column="sex",property="userSex"),
@Result(column="address",property="userAddress"),
@Result(column="birthday",property="userBirthday")
})
List<User> findAll();
//根据id插叙用户
@Select("select * from user where id = #{uid} ")
@ResultMap("userMap")
User findById(Integer userId);
//插入一条用户信息,并返回主键
@Insert("insert into user(username,sex,birthday,address)values(#{username},#{sex},#{birthday},#{address})")
@SelectKey(keyColumn="id",keyProperty="id",resultType=Integer.class,before =
false, statement = { "select last_insert_id()" })
int saveUser(User user);
//修改用户信息
@Update("update user set
username=#{username},address=#{address},sex=#{sex},birthday=#{birthday} where id=#{id} ")
int updateUser(User user);
//删除用户信息
@Delete("delete from user where id = #{uid} ")
int deleteUser(Integer userId);
//使用聚合函数
@Select("select count(*) from user ")
int findTotal();
结束语
MyBatis就记到这儿结束了,在日后的工作中肯定还会再次深层次的研究,到时候再补上