MyBatis
1 概念
mybatis 是一个优秀的基于java的持久层(和数据库交互)框架,它内部封装了jdbc,使开发者只需要关注sql语句本身,而不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。
mybatis通过xml或注解的方式将要执行的各种statement配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的sql语句。
最后mybatis框架执行sql并将结果映射为java对象并返回。采用ORM思想解决了实体和数据库映射的问题,对jdbc 进行了封装,屏蔽了jdbc api 底层访问细节,使我们不用与jdbc api 打交道,就可以完成对数据库的持久化操作。
2 基本使用
-
开发步骤
①添加MyBatis的坐标
②创建user数据表
③编写User实体类
④编写映射文件UserMapper.xml
⑤编写核心文件SqlMapConfig.xml
⑥编写测试类
-
环境搭建
2.1 导入MyBatis的坐标和其他相关坐标
<!--mybatis坐标-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!--mysql驱动坐标-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
<scope>runtime</scope>
</dependency>
<!--单元测试坐标-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--日志坐标-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
2.2 创建User数据表
2.3 创建User类(属性名需与数据表的字段名相同)
2.4 编写UserMapper映射文件
<?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="userMapper">
<!--
注意:
property: 配置为实体类的属性名
column: 配置为SQL列名
若对象属性和列名一致时, 可以不配置. 但是为了代码的阅读性,建议都配置上.
-->
<resultMap id="userMap" type="com.itheima.domain.User">
<id property="userId" column="id"></id>
<result property="userName" column="username"></result>
<result property="userAddress" column="address"></result>
<result property="userSex" column="sex"></result>
<result property="userBirthday" column="birthday"></result>
</resultMap>
<select id="findAll" resultMap="userMap">
select * from User
</select>
</mapper>
2.5 编写MyBatis核心文件 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="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///test"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
</configuration>
2.6 编写测试代码
//加载核心配置文件
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
//获得sqlSession工厂对象
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(resourceAsStream);
//获得sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//执行sql语句
List<User> userList = sqlSession.selectList("userMapper.findAll");
//打印结果
System.out.println(userList);
//释放资源
sqlSession.close();
3 注意事项
- #与$的区别
1. #{} 是占位符, 而${}是拼接SQL
# 方式:
reparing: select * from user where username like ?
$ 方式:
select * from user where username like '%王%'
2. ${} 存在SQL注入的风险
3. 当输入参数类型为普通数据类型(包含基本数据类型和string.)
#{} 变量名可以随意写
${} 变量名只能为value
4. 当输入参数为POJO实体类型时.
#{} 是对象中的属性名
${} 也是对象中的属性名
2 MyBatis保存并返回id
<insert id="saveUser" parameterType="com.itheima.domain.User">
<!-- 保存成功以后, 获取字段为id的值,赋给User对象的id属性 -->
<selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
select last_insert_id();
</selectKey>
insert into user (username,password) values (#{username},#{password})
</insert>
4 MyBatis核心配置文件
1 层级关系
- configuration 配置
- properties 属性
- settings 设置
- typeAliases 类型别名
- typeHandlers 类型处理器
- objectFactory 对象工厂
- plugins 插件
- environments 环境
- environment 环境变量
- transactionManager 事务管理器
- dataSource 数据源
- environment 环境变量
- databaseIdProvider 数据库厂商标识
- mappers 映射器
2 environments标签
`其中,事务管理器(transactionManager)类型有两种:
•JDBC:这个配置就是直接使用了JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作用域。
•MANAGED:这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接,然而一些容器并不希望这样,因此需要将 closeConnection 属性设置为 false 来阻止它默认的关闭行为。
其中,数据源(dataSource)类型有三种:
•UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。
•POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。
•JNDI:这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。
3 mapper标签
该标签的作用是加载映射的,加载方式有如下几种:
•使用相对于类路径的资源引用,例如:
•使用完全限定资源定位符(URL),例如:
•使用映射器接口实现类的完全限定类名,例如:
•将包内的映射器接口实现全部注册为映射器,例如:
4 properties标签:该标签可以加载外部的properties文件
<properties resource="jdbc.properties"></properties>
5 typeAliases标签:设置类型别名
<typeAlias type="com.itheima.domain.User" alias="user"></typeAlias>
<!-- 别名统一配置 -->
<typeAliases>
<!--
使用typeAliases配置别名,它只能配置domain中类的别名
其中: 类名就是别名(不区分大小写)
-->
<package name="com.itheima.domain"></package>
</typeAliases>
5 代理模式开发
1 Mapper 接口开发方法只需要程序员编写Mapper 接口(相当于Dao 接口),由Mybatis 框架根据接口定义创建接口的动态代理对象,代理对象的方法体同上边Dao接口实现类方法。
Mapper接口开发需要遵循以下规范:
Mybaits底层使用动态代理方式,帮我们实现了接口的方法,因此,我们必须严格按照步骤开发
环境搭建的注意事项:
第一个:创建UserMapper.xml 和 创建UserMapper.java时名称保持一致。
在Mybatis中它把持久层的操作接口名称和映射文件也叫做:Mapper
所以:UserDao 和 UserMapper是一样的
第二个:在idea中创建目录的时候,它和包是不一样的
包在创建时:com.itheima.dao它是三级结构 ✔
目录在创建时:com.itheima.dao是一级目录 ✖
可以直接创建目录: ✔✔✔
com/itheima/dao/UserMapper.xml
第三个:mybatis的映射配置文件位置必须和dao接口的包结构相同
第四个:映射配置文件的mapper标签namespace属性的取值必须是dao接口的全限定类名
第五个:映射配置文件的操作配置(select),id属性的取值必须是dao接口的方法名
第六个: resultType输出类型: 输出类型是接口的方法的返回值类型.
如果接口中有返回值,必须有输出类型resultType.
第七个: parameterType 参数类型:
如果接口中有查询参数, 必须有参数类型.
2 映射文件统一配置
<mappers>
<!--
<mapper resource="cn/itcast/UserMapper.xml"/>
<mapper resource="cn/itcast/RoleMapper.xml"/> 每个文件单独配置,很麻烦
注意:
1. package标签配置的是路径或包名
name="com/itheima/dao" 正确
name="com.itheima.dao" 正确
2. 使用package配置方式,映射配置文件名称,必须和接口名称完全一致.否则解析错误
以下两种方式都可以:
<package name="com/itheima/dao"></package>
<package name="com.itheima.dao"></package>
-->
<package name="com.itheima.dao"></package>
</mappers>
3 多参数查询
接口
User login(@Param("username") String name,@Param("password") String pwd);
映射配置文件
<select id="login" resultType="user" >
select * from user where username = #{username} and password = #{password}
</select>
4 动态Sql
- 标签
<select id="findByCondition" resultType="user" parameterType="user">
select * from user where 1 = 1
<if test="username !=null and username !=''">
and username like #{username}
</if>
<if test="password !=null ">
and password like #{password}
</if>
</select>
<!--
注意: <if>标签test属性写的是对象的属性名, 如果是包装类对象要使用OGNL表达式的写法.
即: userName是对象的属性,严格区分大小写,和数据库字段无关.
-->
- 标签
<select id="findByCondition" resultType="user" parameterType="user">
SELECT * from user
<where>
<if test="username !=null">
and username like #{username}
</if>
<if test="password !=null">
and password like #{password}
</if>
</where>
</select>
<!--
注意: <if>标签test属性写的是对象的属性名, 如果是包装类对象要使用OGNL表达式的写法.
即: userName是对象的属性,严格区分大小写,和数据库字段无关.
-->
- 标签
<!--
直接传递数组查询:
parameterType="list", 其中list表示List类型.(固定值,可以接受集合和数组)
collection="array" 其中array: 指定参数类型为数组(固定值)
-->
<select id="findUserInIds" resultType="user" parameterType="list">
select * from user
<where>
<if test="array!=null and array.length>0">
<foreach collection="array" open="and id in (" item="id" close=")" separator=",">
#{id}
</foreach>
</if>
</where>
</select>
foreach标签的属性含义如下:
标签用于遍历集合,它的属性:
•collection:代表要遍历的集合/数组元素,注意编写时不要写#{}
•open:代表语句的开始部分
•close:代表结束部分
•item:代表遍历集合的每个元素,生成的变量名
•sperator:代表分隔符
- 标签
<!--更新用户-->
<update id="update" parameterType="user">
update user
<set>
<if test="username!=null">
username = #{username},
</if>
<if test="password!=null">
password = #{password},
</if>
</set>
where id=#{id}
</update>
<!-- 跟where标签存在意义一样-->
5 SQL片段抽取
Sql中可将重复的片段抽取出来,使用时用include引用即可.
<!--抽取sql片段简化编写-->
<sql id="selectUser">select * from User</sql>
<select id="findById" parameterType="int" resultType="user">
<include refid="selectUser"></include> where id=#{id}
</select>
<select id="findByIds" parameterType="list" resultType="user">
<include refid="selectUser"></include>
<where>
<foreach collection="array" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>
6 plugin标签
MyBatis可以使用第三方的插件来对功能进行扩展,分页助手PageHelper是将分页的复杂操作进行封装,使用简单的方式即可获得分页的相关数据
开发步骤:
①导入通用PageHelper坐标
<!-- 分页助手 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.2</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>0.9.1</version>
</dependency>
②在mybatis核心配置文件中配置PageHelper插件
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!--选择数据库方言-->
<property name="helperDialect" value="mysql"/>
<!--
reasonable:默认值是false
true:自动处理首页和尾页,如果是第一页,再点上一页还是第一页不需要在前端去做是否为首页的判断
如果是尾页,再点下一页还是最后一页,也不需要在前端做是否为尾页的判断
-->
<property name="reasonable" value="true"/>
</plugin>
</plugins>
③ 修改映射配置文件
<select id="pageQuery" resultType="user">
select * from user
</select>
④测试分页代码实现
@Test
public void testPageHelper(){
//设置分页参数
PageHelper.startPage(1,3);
List<User> users = mapper.pageQuery();
for (User user : users) {
System.out.println(user);
}
}
获得分页相关的其他参数
//其他分页的数据
PageInfo<User> pageInfo = new PageInfo<User>(users);
System.out.println("总条数:"+pageInfo.getTotal());
System.out.println("总页数:"+pageInfo.getPages());
System.out.println("当前页:"+pageInfo.getPageNum());
System.out.println("每页显示长度:"+pageInfo.getPageSize());
6 多表查询
1 一对多查询
<?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.itheima.dao.UserMapper">
<resultMap id="userMap" type="User">
<!--用户信息-->
<id property="id" column="id"/>
<id property="username" column="username"/>
<id property="password" column="password"/>
<id property="birthday" column="birthday"/>
<!--
订单信息
property: 对象属性名称
ofType: 设置集合泛型 List<Order> orders
-->
<collection property="orders" ofType="order">
<id property="id" column="oid"/>
<id property="ordertime" column="ordertime"/>
<id property="total" column="total"/>
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
SELECT *,o.id oid FROM USER u LEFT JOIN orders o ON u.id=o.uid
</select>
</mapper>
2 多对多查询
<?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.itheima.dao.UserMapper">
<resultMap id="userMapper" type="user">
<!--用户信息-->
<id column="user_id" property="id"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<!--角色信息-->
<collection property="roles" ofType="role">
<id column="role_id" property="id"></id>
<result column="rolename" property="rolename"></result>
<result column="roledesc" property="roledesc"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="userMapper">
SELECT * FROM USER u,user_role ur,role r WHERE u.id = ur.user_id AND ur.role_id = r.id
</select>
</mapper>
3 总结 MyBatis多表配置方式:
一对一配置:使用做配置
一对多配置:使用+做配置
多对多配置:使用+做配置
7 注解查询
1 插入后返回id
/**
* 保存用户,并返回最后保存用户的id
* before
* 1. false : 表示插入成功以后执行
* 2. true: 表示插入之前执行
*/
@SelectKey(keyProperty = "id", keyColumn = "id", resultType = Integer.class, before = false, statement = "select last_insert_id()")
@Insert("insert into user (username,password,birthday) values(#{username},#{password},#{birthday})")
public void saveLastUser(User user);
2 一对一查询
@Select("select * from orders")
@Results({
@Result(id = true, property = "id", column = "id"),
@Result(property = "ordertime", column = "ordertime"),
@Result(property = "total", column = "total"),
@Result(
property = "user", //要封装的属性名称 private User user;
javaType = User.class, //要封装的实体类型
column = "uid", //根据那个字段去查询user表的数据
//select属性 代表查询那个接口的方法获得数据
one = @One(select = "com.itheima.dao.UserMapper.findById")
)
})
List<Order> findAll();
3 一对多查询
// UserMapper配置
public interface UserMapper {
@Results({
@Result(property = "id",column = "id",id = true),
@Result(property = "username",column = "username"),
@Result(property = "password",column = "password"),
@Result(property = "birthday", column = "birthday"),
@Result(property = "orders",column = "id",javaType = List.class,
many = @Many(select = "com.itheima.dao.OrderMapper.findById")
)
})
@Select("select * from user")
List<User> findAllUserAndOrder();
}
//OrderMapper配置
public interface OrderMapper {
@Select("select * from orders where uid = #{uid}")
public Order findById();
}
4 多对多查询
// UserMapper配置
public interface UserMapper {
@Select("select * from user")
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "username",column = "username"),
@Result(property = "password",column = "password"),
@Result(property = "birthday",column = "birthday"),
@Result(property = "roleList",column = "id",javaType = List.class,
many = @Many(select = "com.itheima.mapper.RoleMapper.findByUid"))
})
List<User> findAllUserAndRole();}
// RoleMapper配置
public interface RoleMapper {
@Select("select * from role r,user_role ur where r.id=ur.role_id and ur.user_id=#{uid}")
List<Role> findByUid(int uid);
}
8 一级缓存和二级缓存
一级缓存基于sqlSession默认开启,在操作数据库时需要构造SqlSession对象,在对象中有一个HashMap用于存储缓存数据。不同的SqlSession之间的缓存数据区域是互相不影响的。一级缓存的作用域是SqlSession范围的,当在同一个sqlSession中执行两次相同的sql语句时,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次查询时会从缓存中获取数据,不再去底层数据库查询,从而提高查询效率。需要注意的是,如果SqlSession执行了DML操作(增删改),并且提交到数据库,MyBatis则会清空SqlSession中的一级缓存,这样做的目的是为了保证缓存中存储的是最新的信息,避免出现脏读现象。当一个SqlSession结束后该SqlSession中的一级缓存也就不存在了。关闭一级缓存后,再次访问,需要再次获取一级缓存,然后才能查找数据,否则会抛出异常。
二级缓存是mapper级别的缓存。使用二级缓存时,多个SqlSession使用同一个Mapper的sql语句去操作数据库,得到的数据会存在二级缓存区域,它同样是使用HashMap进行数据存储。相比一级缓存SqlSession,二级缓存的范围更大,多个Sqlsession可以共用二级缓存,二级缓存是跨SqlSession的。二级缓存的作用域是mapper的同一个namespace。不同的sqlSession两次执行相同的namespace下的sql语句,且向sql中传递的参数也相同,即最终执行相同的sql语句,则第一次执行完毕会将数据库中查询的数据写到缓存,第二次查询会从缓存中获取数据,不再去底层数据库查询,从而提高效率。