1. #和$的区别
#{}表示一个占位符号
- 通过#{}可以实现 preparedStatement 向占位符中设置值,自动进行 java 类型和 jdbc 类型转换,
- #{}可以有效防止 sql 注入。 #{}可以接收简单类型值或 pojo 属性值。
- 可以自动对值添加 ’ ’ 单引号
${}表示拼接 sql 串
- 通过${}可以将 parameterType 传入的内容拼接在 sql 中且不进行 jdbc 类型转换,
- ${}可以接收简单类型值或 pojo 属性值,如果 parameterType 传输单个简单类型值,${}括号中只能是 value。
- 比如order by id 这种的,以id排序 那么这个id 是没有单引号的,就是简单的SQL拼接,所以我们应该使用${} 而不是#{}
2. paramerterType 和 resultType
2.1 paramerterType
SQL 语句传参,使用标签的 parameterType 属性来设定。该属性的取值可以
是基本类型,引用类型(例如:String 类型),还可以是实体类类型(POJO 类)。
2.2 resultType
resultType 属性可以指定结果集的类型,它支持基本类型和实体类类型。
2.3 resultMap
通过resultMap,我们可以指定查询结果字段和实体属性字段的映射关系。
<resultMap id="userResult" type="User">
<id column="id" property="id" />
<result property="nickname" column="nickname" />
<result property="schoolName" column="school_name" />
</resultMap>
3. mybatis-config.xml 配置文件
3.1 读取配置文件
创建db.properties
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/erp16?useUnicode=true&characterEncoding=UTF-8
username=root
password=root
配置文件读取
<properties resource="db.properties" />
3.2 typeAliases属性
<typeAliases>
<!-- 单个别名定义 -->
<typeAlias alias="user" type="com.tledu.zrz.pojo.User"/>
<!-- 批量别名定义,扫描整个包下的类,别名为类名(首字母大写或小写都可以) -->
<package name="com.tledu.zrz.pojo"/>
<package name="其它包"/>
</typeAliases>
3.3 mapper属性
Mappers是我们所说的映射器,用于通过mybatis配置文件去找到对应的mapper文件,关于这个属性有三种用法。
3.3.1 Resource
使用相对于类路径的资源如:<mapper resource="com/tledu/zrz/dao/IUserDao.xml" />
3.3.2 class
使用 mapper 接口类路径
如:<mapper class="com.tledu.zrz.dao.UserDao"/>
3.3.3 package
注册指定包下的所有 mapper 接口
如:<package name="com.tledu.zrz.mapper"/>
4 动态sql
4.1 if
<select id="list" parameterType="User" resultMap="userResult">
select * from t_user where 1=1
<if test="username != null and username != ''">
and username = #{username}
</if>
<if test="nickname != null and nickname != ''">
and nickname like concat('%',#{nickname},'%')
</if>
</select>
4.2 choose、when、otherwise
<select id="list" parameterType="User" resultMap="userResult">
select * from t_user where 1=1
<choose>
<when test="id != null">
and id = #{id}
</when>
<when test="username != null and username != ''">
and username = #{username}
</when>
<otherwise>
and nickname = #{nickname}
</otherwise>
</choose>
</select>
4.3 where
在我们where条件不确定的时候,我们每次都需要加上一个1=1才能保证后面的拼接不会出现错误,使用where标签后,我们就不需要考虑拼接的问题了,直接在里面添加条件即可。通过where标签也可以让代码更具有语义化,方便维护代码
<select id="list" parameterType="User" resultMap="userResult">
select * from t_user where
<where>
<if test="username != null and username != ''">
and username = #{username}
</if>
<if test="nickname != null and nickname != ''">
and nickname like concat('%',#{nickname},'%')
</if>
</where>
</select>
4.4 set
在进行更新操作的时候,也可以用set标签添加更新条件,同样我们也就不需要在进行字符串拼接了,set标签会帮我们进行拼接。
<update id="updateNickname">
update t_user
<set>
<if test="nickname != null and nickname != ''">
nickname = #{nickname},
</if>
<if test="username != null and username != ''">
username = #{username},
</if>
</set>
where id = #{id}
</update>
4.4 foreach
进行批量操作
<insert id="batchInsert">
insert into t_user (username, password, nickname) VALUES
<foreach collection="list" index="idx" item="item" separator=",">
(#{item.username},#{item.password},#{item.nickname})
</foreach>
</insert>
in 查询
<select id="list" parameterType="User" resultMap="userResult">
select * from t_user
<where>
<if test="user.username != null and user.username != ''">
and username = #{user.username}
</if>
<if test="user.nickname != null and user.nickname != ''">
and nickname like concat('%',#{user.nickname},'%')
</if>
and id in
<foreach collection="idList" item="item" separator="," open="(" close=")">
#{item}
</foreach>
</where>
</select>
5 联查
5.1 1对1
在实现1对1映射的时候,可以通过association属性进行设置。在这里有三种方式
在地址表中,每个地址对应有一个创建用户,每次查询地址的时候希望查询到创建用户的内容
5.1.1 使用select
<resultMap id="address" type="Address">
<id column="id" property="id" />
<association property="user" column="user_id" javaType="User" select="com.tledu.erp.dao.IUser2Dao.selectById"/>
</resultMap>
5.1.2 直接进行联查,在association中配置映射字段
<resultMap id="address" type="Address" autoMapping="true">
<id column="id" property="id" />
<association property="user" column="user_id" javaType="User" >
<id column="user_id" property="id" />
<result column="school_name" property="schoolName" />
</association>
</resultMap>
<select id="selectOne" resultMap="address">
select * from t_address left join t_user tu on tu.id = t_address.user_id where t_address.id = #{id}
</select>
autoType代表自动封装,如果不填写,则需要添加所有的对应关系。
这种方式的问题是,当association需要被多次引用的时候,就需要进行多次重复的配置,所以我们还有第三种方式,引用resultMap。
5.1.3 嵌套的resultType
<resultMap id="addressMap" type="Address" autoMapping="true">
<id column="id" property="id"/>
<association property="user" column="user_id" resultMap="userMap">
</association>
</resultMap>
<resultMap id="userMap" type="User" autoMapping="true">
<id column="user_id" property="id" />
<result column="school_name" property="schoolName"/>
</resultMap>
<select id="selectOne" resultMap="addressMap">
select t_address.id,
addr,
phone,
postcode,
user_id,
username,
password,
nickname,
age,
sex,
school_name
from t_address
left join t_user tu on tu.id = t_address.user_id
where t_address.id = #{id}
</select>
- 通过别名保证查询的每一个元素是唯一的,以防止出现错乱的情况
- mybatis官网提醒,需要设置id提高查询性能
5.1 1对多
对于address来说,一个地址对应一个创建用户,但是对于User来说,一个用户可能对应创建了多条地址信息,这种情况,在mybatis中就可以通过collections解决。
5.2.1 使用select
在AddressDao中添加
<select id="selectByUserId" resultType="Address">
select * from t_address where user_id = #{userId}
</select>
在UserDao中添加
<resultMap id="userResult" type="User" autoMapping="true">
<id column="id" property="id"/>
<result property="nickname" column="nickname"/>
<result property="schoolName" column="school_name"/>
<collection property="addressList" column="id" autoMapping="true" select="com.tledu.erp.dao.IAddressDao.selectByUserId" >
</collection>
</resultMap>
<select id="selectById" parameterType="int" resultMap="userResult">
select *
from t_user
where id = #{id}
</select>
5.2.2 直接进行联查,在collection中配置映射字段
<resultMap id="userResult" type="User" autoMapping="true">
<id column="id" property="id"/>
<result property="nickname" column="nickname"/>
<result property="schoolName" column="school_name"/>
<collection property="addressList" column="phone" ofType="Address" autoMapping="true">
<id column="address_id" property="id" />
</collection>
</resultMap>
<select id="selectById" parameterType="int" resultMap="userResult">
select tu.id,
username,
password,
nickname,
age,
sex,
school_name,
ta.id as address_id,
addr,
phone,
postcode,
user_id
from t_user tu
left join t_address ta on tu.id = ta.user_id
where tu.id = #{id}
</select>
5.1.3 嵌套的resultType
<resultMap id="userResult" type="User" autoMapping="true">
<!-- <id column="id" property="id"/>-->
<result property="nickname" column="nickname"/>
<result property="schoolName" column="school_name"/>
<collection property="addressList" column="phone" ofType="Address" resultMap="addressResultMap" autoMapping="true">
</collection>
</resultMap>
<resultMap id="addressResultMap" type="Address" autoMapping="true">
<id column="address_id" property="id" />
</resultMap>
<select id="selectById" parameterType="int" resultMap="userResult">
select tu.id,
username,
password,
nickname,
age,
sex,
school_name,
ta.id as address_id,
addr,
phone,
postcode,
user_id
from t_user tu
left join t_address ta on tu.id = ta.user_id
where tu.id = #{id}
</select>
6.mybatis中的连接池
数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并将这些连接组成一个连接池,由应用程序动态地对池中的连接进行申请、使用和释放。
对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。并且应用程序可以根据池中连接的使用率,动态增加或减少池中的连接数。
总结:
- 连接池是面向数据库连接的
- 连接池是为了优化数据库连接资源
分类 :
- UNPOOLED 不使用连接池的数据源
- POOLED 使用连接池的数据源
- JNDI 使用JNDI实现的数据库连接池
6.1 UNPOOLED连接过程分析
- 初始化驱动: 判断driver驱动是否已经加载到内存中,如果还没有加载,则会动态地加载driver类,并实例化一个Driver对象,使用DriverManager.registerDriver()方法将其注册到内存中,以供后续使用。
- 创建Connection对象: 使用DriverManager.getConnection()方法创建连接。
- 配置Connection对象: 设置是否自动提交autoCommit和隔离级别isolationLevel。
- 返回Connection对象
6.2 POOLED 数据源 连接池
- 先看是否有空闲(idle)状态下的PooledConnection对象,如果有,就直接返回一个可用的PooledConnection对象;否则进行第2步。
- 查看活动状态的PooledConnection池activeConnections是否已满;如果没有满,则创建一个新的PooledConnection对象,然后放到activeConnections池中,然后返回此PooledConnection对象;否则进行第三步;
- 看最先进入activeConnections池中的PooledConnection对象是否已经过期:如果已经过期,从activeConnections池中移除此对象,然后创建一个新的PooledConnection对象,添加到activeConnections中,然后将此对象返回;否则进行第4步。
- 线程等待,循环2步
7. mybatis中的事务和隔离级别
事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
原子性(atomicity)。一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
隔离性(isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
三大问题 :
- 脏读
- 不可重复读
- 幻读
隔离级别 :
脏读 | 不可重复读 | 幻读 | 说明 | |
Read uncommitted | √ | √ | √ | 直译就是"读未提交",意思就是即使一个更新语句没有提交,但是别 的事务可以读到这个改变.这是很不安全的。允许任务读取数据库中未提交的数据更改,也称为脏读。 |
Read committed | × | √ | √ | 直译就是"读提交",可防止脏读,意思就是语句提交以后即执行了COMMIT以后,别的事务才能读到这个改变. 只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 |
Repeatable read | × | × | √ | 直译就是"可以重复读",这是说在同一个事务里面先后执行同一个查询语句的时候,得到的结果是一样的.在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读。 |
Serializable | × | × | × | 直译就是"序列化",意思是说这个事务执行的时候不允许别的事务并发执行. 完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞 |
8.缓存
8.1 一级缓存
一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flush 或 close,它就存在。
8.2 二级缓存
二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的
8.3 在映射文件中开启缓存支持
<mapper namespace="com.tledu.erp.mapper.IAddressMapper">
<!-- 开启缓存支持-->
<cache />
</mapper>
查询语句中需要指定useCache="true"
<select id="selectOne" resultMap="addressResultMap" useCache="true">
select *
from t_address
where id = #{id}
</select>
8.4 总结
- 在使用二级缓存的时候,需要注意配置mybatis-config.xml中 开启二级缓存
- <setting name="cacheEnabled" value="true"/>
- 然后再mapper映射文件中使用catch标签标注开启,并对需要换成的语句添加useCache=”true”
- 在mapper的映射文件中使用<cache />,代表当前mapper是开启二级缓存的
- 在需要二级缓存的查询上增加useCache = true,代表当前查询是需要缓存的
- 并且对应封装数据的实体类需要实现Serializable 接口
- 对待缓存的数据,实现Serialization接口,代表这个数据是可序列化
- 只有当sqlSession close之后,二级缓存才能生效
- 当执行增删改操作的时候,必须执行commit()才能持久化到数据库中,同时二级缓存清空
- session.clearCache()无法清除二级缓存,如果需要清除二级缓存,可以通过sqlSessionFactory.getConfiguration().getCache("缓存id").clear();
- 但是当我们查询语句中,执行commit() 或者是close()关闭session,都不会清空二级缓存
9. 分页
基于这些属性设计分页的实体类
@Data
public class PageInfo<T> {
/**
* 每页有多少个
*/
private int pageSize;
/**
* 当前是在第几页
*/
private int currentPage;
/**
* 数据的总数
*/
private int total;
/**
* 数据列表
*/
private List<T> list;
// 获取偏移量
public int getOffset() {
return (this.currentPage - 1) * this.pageSize;
}
}
创建分页查询的方法
/**
* 分页查询
*
* @param user 查询条件
* @param offset 起始位置
* @param pageSize 每页容量
* @return 用户列表
*/
List<User> page(@Param("user") User user, @Param("offset") int offset, @Param("pageSize") int pageSize);
/**
* 统计总数
*
* @param user 查询条件
* @return 总数
*/
int count(@Param("user") User user);
<select id="page" resultType="User">
select *
from t_user
<where>
<if test="user.nickname != null and user.nickname != ''">
and nickname like concat('%',#{user.nickname},'%')
</if>
<if test="user.username != null and user.username != ''">
and username = #{user.username}
</if>
</where>
limit #{offset},#{pageSize};
</select>
<select id="count" resultType="int">
select count(*)
from t_user
<where>
<if test="user.nickname != null and user.nickname != ''">
and nickname like concat('%',#{user.nickname},'%')
</if>
<if test="user.username != null and user.username != ''">
and username = #{user.username}
</if>
</where>
</select>
测试
@Test
public void page(){
PageInfo<User> pageInfo = new PageInfo<User>();
pageInfo.setCurrentPage(1);
pageInfo.setPageSize(10);
User user = new User();
user.setNickname("尚云");
// 加上筛选条件,根据nickname 或 username进行筛选
List<User> list = userMapper.page(user,pageInfo.getOffset(),pageInfo.getPageSize());
pageInfo.setList(list);
pageInfo.setTotal(userMapper.count(user));
System.out.println(pageInfo);
}