1,概念
MyBatis是一个数据持久层(ORM)框架,封装了jdbc。把实体类和SQL语句之间建立了映射关系,是一种半自动化的ORM实现。MyBATIS需要开发人员自己来写sql语句,这可以增加了程序的灵活性,在一定程度上可以作为ORM的一种补充。
是由Apache开源项目iBatis3迁移到Github,命名为MyBatis。
1)优点
- 支持定制化SQL、存储过程及高级映射;
sql写在xml中,解耦、并可重用。 - 避免了几乎所有的JDBC代码和手动设置参数以及获取结果集;
- 可以使用简单的XML或注解用于配置和原始映射,将接口和java的POJO映射成数据库种的记录;
- 能与各种数据库兼容;
mybatis使用jdbc来连接数据库,只要jdbc支持的数据库mybatis也都支持。
2)缺点
- sql编写工作量大,复杂查询要求有一定的sql功底;
- sql语句依赖于具体的数据库,移植性差。
3)和其他持久化层技术对比
ORM(Object Relation Mapping)
O :object 对象
R: relation 关系
M: mapping 映射
ORM的作用就是把类转成SQL语句、可以把SQL语句转成类
- JDBC
SQL夹杂在java代码中耦合度高,导致硬编码内伤;
维护困难不易修改;
代码冗长,开发效率低。 - Hibernate和JPA(全自动的ORM框架)
操作简单,开发效率高。
但过于复杂的sql需要绕过框架,完全由框架内部产生sql不易特殊化吹;
反射操作太多导致数据库性能下降。 - MyBatis(半自动的ORM框架)
性能出色(sql优化优势更大);开发效率逊色Hibernate和JPA。
sql和java编码分开,功能边界清晰。==》java做业务,sql做数据。
4)字符转义:<![CDATA[ ]]>
使用<![CDATA[]]>来包含不被xml解析器解析的内容:”<”和”&”。但要注意的是:
(1) 此部分不能再包含”]]>”;
(2) 不允许嵌套使用;
(3)”]]>”这部分不能包含空格或者换行。
比如<![CDATA[<]]> 表示文本内容“<”
5)类型映射
1>基本类型映射
JDBC Type | Java Type | 备注 |
---|---|---|
CHAR | String | |
VARCHAR | String | |
LONGVARCHAR | String | |
NUMERIC | java.math.BigDecimal | |
DECIMAL | java.math.BigDecimal | |
BIT | boolean | |
BOOLEAN | boolean | |
TINYINT | byte | |
SMALLINT | short | |
INTEGER | int | |
BIGINT | long | |
REAL | float | |
FLOAT | double | |
DOUBLE | double | |
BINARY | byte[] | |
VARBINARY | byte[] | |
LONGVARBINARY | byte[] | |
DATE | java.sql.Date | |
TIME | java.sql.Time | |
TIMESTAMP | java.sql.Timestamp | |
CLOB | Clob | |
BLOB | Blob | |
ARRAY | Array | |
DISTINCT | mapping of underlying type | |
STRUCT | Struct | |
REF | Ref | |
DATALINK | java.net.URL[color=red][/color] | |
OTHER | TypeHandler(类型处理器)来自定义 | 数组: org.apache.ibatis.type.ArrayTypeHandler |
2>自定义TypeHandler
自定义的方式有两种,一种是实现TypeHandler这个接口,另一个就是继承BaseTypeHandler这个便捷的抽象类。
举例:年龄的类型处理器。
- 定义类型处理器;
//指定与其关联的JDBC 类型列表。如果在jdbcType 属性中也同时指定,则注解上的配置将被忽略。
@MappedJdbcTypes(JdbcType.INTEGER)
//指定与其关联的 Java类型列表。如果在javaType 属性中也同时指定,则注解上的配置将被忽略。
@MappedTypes(String.class)
public class GenderTypeHandler extends BaseTypeHandler {
//设置参数,这里将Java的String类型转换为JDBC的Integer类型
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws
SQLException {
ps.setInt(i, StringUtils.equals(parameter.toString(),"男")?1:2);
}
//以下三个参数都是将查询的结果转换
@Override
public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
return rs.getInt(columnName)==1?"男":"女";
}
@Override
public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getInt(columnIndex)==1?"男":"女";
}
@Override
public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getInt(columnIndex)==1?"男":"女";
}
}
- 在配置文件application.properties中添加一行配置:
将typeHandler注入到mybatis中。
## 设置自定义的Typehandler所在的包,启动的时候会自动扫描配置到Mybatis中
mybatis.type-handlers-package=cn.cb.demo.typehandler
- xml引用——更新
<insert id="insertUser">
insert into user_info(user_id, gender)
values(
#{userId,jdbcType=VARCHAR},
#{gender,jdbcType=INTEGER,typeHandler=cn.cb.demo.typehandler.GenderTypeHandler})
</insert>
- xml引用——查询
<resultMap id="userResultMap" type="cn.cb.demo.domain.UserInfo">
<result column="user_id" property="userId"/>
<!-- 指定typeHandler属性为全类名-->
<result column="gender" property="gender" typeHandler="cn.cb.demo.typehandler.GenderTypeHandler"/>
<!-- jsonb类型-->
<result column="search_content" jdbcType="OTHER" property="searchContent" typeHandler="com.common.JsonbMapTypeHandler"/>
</resultMap>
3>demo——JsonListTypeHandler
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.ibatis.executor.result.ResultMapException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeException;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
//注意,此处用泛型,如果传值过小,会用List<Integer>接数据,用List<Long>会报错,可以用Number接数据,也可以重新写一个TypeHandler(继承BaseTypeHandler<List<Long>>,重写getNullableResult方法:result = value == null ? null : JSON.parseObject(value, new TypeReference<List<Long>>(){});)
public class JsonListTypeHandler extends BaseTypeHandler<List<?>> {
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, List<?> objects, JdbcType jdbcType) throws SQLException {
if (objects == null) {
try {
preparedStatement.setNull(i, JdbcType.OTHER.TYPE_CODE);
} catch (SQLException e) {
throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
+ "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
+ "Cause: " + e, e);
}
} else {
try {
preparedStatement.setObject(i, JSONObject.toJSONString(objects), JdbcType.OTHER.TYPE_CODE);
} catch (Exception e) {
throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType
+ " . "
+ "Try setting a different JdbcType for this parameter or a different configuration property. "
+ "Cause: " + e, e);
}
}
}
@Override
public List<?> getNullableResult(ResultSet resultSet, String s) throws SQLException {
List<?> result;
try {
String value = resultSet.getString(s);
result = value == null ? null : JSONObject.parseObject(value, List.class);
} catch (Exception e) {
throw new ResultMapException(
"Error attempting to get column '" + s + "' from result list. Cause: " + e, e);
}
if (resultSet.wasNull()) {
return new ArrayList<>();
} else {
return result;
}
}
@Override
public List<?> getNullableResult(ResultSet resultSet, int i) throws SQLException {
List<?> result;
try {
String value = resultSet.getString(i);
result = value == null ? null : JSONObject.parseObject(value, List.class);
} catch (Exception e) {
throw new ResultMapException(
"Error attempting to get column #" + i + " from result list. Cause: " + e, e);
}
if (resultSet.wasNull()) {
return new ArrayList<>();
} else {
return result;
}
}
@Override
public List<?> getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
List<?> result;
try {
String value = callableStatement.getString(i);
result = value == null ? null : JSONObject.parseObject(value, List.class);
} catch (Exception e) {
throw new ResultMapException(
"Error attempting to get column #" + i + " from callable statement. Cause: " + e, e);
}
if (callableStatement.wasNull()) {
return new ArrayList<>();
} else {
return result;
}
}
}
2,使用
mybatis的sql操作有三种:
- 注解;
- xml;
- QueryWrapper + xml (推荐)
1)引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
2)配置文件
配置连接、事务方式等。
3)po
@TableName(value = "tbl_user", autoResultMap = true)
public class User{
...
}
4)Mapper及常见注解
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
关于sql操作,可以使用注解,也可以直接在xml中写sql,通过xml中标签的id来绑定。
对于简单sql可以直接使用mapper的包装类,对于复杂sql写xml中更灵活,所以不推荐使用注解进行开发。
org.apache.ibatis.annotations包常见注解见后文。
5)映射文件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.test.mapper.postgresql.UserMapper "> --路径,必须和类名完全一致
各种标签,对应mapper类的各种方法。注意:
1.id要和mapper接口中的方法名完全一致
</mapper>
1>添加(insert标签)
<!--int insertUser();-->
<insert id="insertUser">
insert into t_user values(null,'admin','男')
</insert>
2>删除(delete标签)
<!--int deleteUser();-->
<delete id="deleteUser">
delete from t_user where id = 7
</delete>
3>修改(update 标签)
<!--int updateUser();-->
<update id="updateUser">
update t_user set username='abc' where id = 7
</update>
<update>
update user
<set>
<if test="username != null">username=#{username}</if>
</set>
</update>
set元素会前置set关键字,同时也会删除无关的逗号
等价于:
<trim prefix="set" suffixoverrides=",">
...
</trim>
4>查询对象(select 标签)
<!--User getUserById();-->
<select id="getUserById" resultType="com.luo.bean.User">
select* from t_user where id =7
</select >
查询必须设置返回值:
- resultType 默认映射关系、
- resultMap 自定义映射关系、一对多多对一映射关系)。
查询集合:
<!--List<User> getAllUser();-->
<!--或者在上文通过<resultMap id="User">定义,下文使用resultMap="User" -->
<select id="getAllUser" resultType="com.luo.bean.User">
select* from t_user
</select >
<select id="existsById" parameterType="long" resultType="long"></select>
5>foreach标签
批量操作,如批量增删改。sql会自动拼在缓存中,一次性提交。
批量更新或插入:
<insert id="insertBatch" parameterType="java.util.List">
INSERT INTO t_user
(id, name)
VALUES
<foreach collection ="userList" item="user" separator =",">
(#{user.id}, #{user.name})
</foreach >
</insert>
7>sql标签
<sql id="sql-1">
...
</sql>
其他标签可以直接引用sql片段:
<...>
<include refid="sql-1"></include>
</...>
8>if标签
- 判断字符串是否为空
<if test="title != null and test != '' ">
and title like '%'|| #{title}||'%'
</if>
- 判断集合是否为空
<if test="partAttributeList != null and partAttributeList.size > 0">
- boolean 类型的判断
即使是包装类型Boolean,也不需要判断null的问题
<if test="hasRelated">
9>choose-when标签
<choose>
<when test="title != null">
and ...
</when>
<when test="author != null">
and...
</when>
<otherwise>
and .....
</otherwise>
</choose>
10>where标签
- where元素只会在至少有一个子元素条件返回SQL子句的情况下,才会插入“where子句”。
- 若语句的开头为“and”或“or”,where元素也会将他们去除;
<where>
<if test="params.name != null and params.name != '' ">
and ...
</if>
<if test="...">
and ...
</if>
</where>
11>resultMap 标签(结果集映射)
基本类型不需要写resultMap可以自动映射。
<resultMap id="userInfo" type="com.luo.test.User">
#column="数据库字段名" property="对象类的属性名"
<result column="id" jdbcType="INTEGER" property="id"/>
<result column="create_time" jdbcType="DATE" property="createTime"/>
...
</resultMap>
<select id="getUserInfo" resultMap="userInfo" parameterType="String">...</select>
12>bind标签
对传入的参数处理后让下文可以使用新的变量。
<if test="userName != null and userName != ''">
<bind name="nameLike" value="'%' + userName + '%'"/>
and user_name like #{nameLike}
</if>
13>函数使用
<insert id="testFun">
DO $$
DECLARE num INTEGER=0;
BEGIN
select count(*) into num as num from (select distinct cardnum from
bus_data) as A;
insert into ic_test
select num, dt
from bus_data limit 100;
END $$;
</insert>
6)QueryWrapper
继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件
及 LambdaQueryWrapper, 可以通过 new QueryWrapper().lambda() 方法获取。
1>常见查询操作
操作方法 | 说明 | 举例 |
---|---|---|
eq | 等于 | wrapper.eq("last_name", "皮皮虾"); |
ne | 不等于 | |
gt | 大于 | |
ge | 大于等于 | |
lt | 小于 | |
le | 小于等于 | |
like | ’%值%’ | |
notLike | ‘%值%’ | |
likeLeft | ‘%值’ | |
likeRight | ‘值%’ | |
between | 在值1和值2之间 | betweenWrapper.between("age", 10, 20); |
notBetween | 不在值1和值2之间 | |
isNull | 字段 IS NULL | isNullWrapper.isNull("email"); |
isNotNull | 字段 IS NOT NULL | |
in | 字段 IN (v0, v1, …) | inWrapper.in("age", 8, 16, 26); queryWrapper.in("type",typeList) |
notIn | 字段 NOT IN (value.get(0), value.get(1), …) | |
or | 或者 | orWrapper.gt(“age”, 20).or().eq(“gender”, 1); |
and | 和 | |
orderByAsc | 升序:ORDER BY 字段, … ASC | Wrapper.orderByAsc("id"); |
orderByDesc | 降序:ORDER BY 字段, … DESC | |
inSql | 字段 IN ( sql语句 ) | inSqlWrapper .inSql("select id from employee where id < 10"); |
notInSql | 字段 NOT IN ( sql语句 ) | |
exists | 拼接 EXISTS ( sql语句 ) | existsWrapper.exists(“select last_name,gender from employee where id = 1”); |
notExists | 拼接 NOT EXISTS ( sql语句 ) |
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
//eq() 等于
wrapper.eq("last_name", "皮皮虾");
Employee one = employeeService.getOne(wrapper);
System.out.println(one);
2>LambdaQueryWrapper
public List<ServiceDictionary> findServiceDictionaryByTypeOrId(DictionaryDto dictionaryDto) {
LambdaQueryWrapper<ServiceDictionary> lambdaQueryWrapper = Wrappers.lambdaQuery();
//设置某个字段的值 lambdaQueryWrapper.in(!CollectionUtils.isEmpty(dictionaryDto.getTypes()),ServiceDictionary::getDictionaryTypeName,dictionaryDto.getTypes());
lambdaQueryWrapper.in(!CollectionUtils.isEmpty(dictionaryDto.getIds()),ServiceDictionary::getId,dictionaryDto.getIds());
lambdaQueryWrapper.orderBy(true,true,ServiceDictionary::getSort);
return serviceDictionaryMapper.selectList(lambdaQueryWrapper);
}
LambdaQueryWrapper<Setting> wrapper = new LambdaQueryWrapper<Setting>().eq(Setting::getId, settingDto.getId())
//添加自定义sql
.apply(!Strings.isNullOrEmpty(sql), sql);
Integer count = settingMapper.selectCount(wrapper);
使用wrapper结合sql一起使用来进行查询:
- mapper映射
通过@Param(Constants.WRAPPER) Wrapper wrapper传参
List<ResultDataDTO> selectData(@Param(Constants.WRAPPER) Wrapper<ResultDataDTO> wrapper);
- mapper.xml
通过${ew.customSqlSegment}
进行wrapper的引用
<select id="selectData" resultType="com.test.dto.ResultDataDTO">
SELECT a.*,
b.DeviceName
FROM 表1 a
INNER JOIN 表2 b
ON a.DeviceNum = b.DeviceNum ${ew.customSqlSegment}
</select>
- wrapper构造并传入
QueryWrapper<ResultDataDTO> wrapper = new QueryWrapper<>();
//如果wrapper中传入的字段在查询的多个表中存在,那么要带上前缀,即下面代码中的a.DeviceNum,这个a对应xml文件中的那个表的别名a。如果没有这样的话,会报column in where clause is amiguous
wrapper.eq("a.DeviceNum", "30");
//联表查或者把其它wrapper条件构造器放入xml中时使用
xxxMapper.selectData(wrapper);
7)缓存
缓存分为一级缓存和二级缓存,默认情况下,只有一级缓存开启(SqlSession级别的缓存,也称本地缓存)
二级缓存需要手动开启和配置,是基于namespace级别的缓存
为了提高扩展性,MyBatis定义了缓存接口Cache,实现Cache可以自定义二级缓存
1>一级缓存
仅仅对一个会话中的数据进行缓存(SqlSession):
映射语句文件(mapper.xml)中所有的select语句的结果将会被缓存,insert、udpate和delete会刷新缓存
缓存会使用最近最少使用算法刷新(LRU least Recently Used)清除不需要的缓存
缓存不会定时进行刷新(也就是说,没有刷新间隔)
缓存会保存列表和对象(无论查询方法返回哪一种)的1024个引用
缓存会被视为读/写缓存,这意味着获取到的对象不是共享的,可以安全的被调用者修改,而不干扰其他调用者或线程所做的潜在修改
缓存失效情况:
1、查询不同
2、增删改操作
3、查询不使用mapper.xml
4、手动清除缓存sqlSession.clearCache();
2>二级缓存
要启用全局的二级缓存,只需要在SQL映射文件(mapper.xml)中添加即可
注意:二级缓存只作用于cache标签所在的mapper.xml文件的语句中。
如果使用JavaAPI和xml混用,在共同接口的语句将不会被默认缓存,需要使用@CacheNamespaceRef指定缓存作用区域。
<setting name="CacheEnable" value="true"/>
<cache
清除策略:LRU、FIFO、SOFT、WEAK
eviction="FIFO"
刷新间隔
flushInterval="60000"
引用数目
size="512"
只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这就提升了可观的性能
而可读写的缓存就会通过序列化返回缓存对象的拷贝,速度上会慢一些,但是更安全,因此默认值是false
readOnly="true"/>
8)分页
1>limit语句
<select id="..." parameterType="map">
...
limit #{size} offset (#{page}-1)*#{size}
</select>
注意:
如果返回值是Page<>,因为查询了total,检索效率会有影响。直接通过limit查速度就会快很多。
2>Wrapper的Page参数
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
//注意:page从1开始
Page<TTemplateData> page = new Page<>(condition.getPage(), condition.getPageSize());
IPage<TTemplateData> templateIPage = tTemplateDataMapper.selectPage(page, templateWrapper);
3>xml的Page参数
Page<TLabel> page = new Page<>(condition.getPage(), condition.getPageSize());
IPage<TLabel> labelIPage = tLabelMapper.selectPageList(page,keys,condition.getLabelTypeName());
IPage<TLabel> selectPageList(@Param("page") Page<TLabel> page,@Param("labelName") String labelName,@Param("labelTypeName") String labelTypeName);
<select id = "selectPageList" resultMap="BaseResultMap">
select l.*,t.label_type_name labelTypeName from t_label l left join t_label_type t on l.label_type_id = t.id and t.status = 0
where l.status = 0
<if test="labelName != null and labelName != ''">
and l.label_name like concat('%',#{labelName},'%')
</if>
<if test="labelTypeName != null and labelTypeName != ''">
and t.label_type_name = #{labelTypeName}
</if>
</select>
3,MyBatis-plus
1)介绍
MyBatis-plus简单来说就是一款 Mybatis 增强工具,用于简化开发,提高效率。
在我们使用Mybatis时会发现,每当要写一个业务逻辑的时候都要在DAO层写一个方法,再对应一个SQL,即使是简单的条件查询、即使仅仅改变了一个条件都要在DAO层新增一个方法。MybatisPlus这个框架上手快、不用写sql语句和xml文件。直接创建查询类,就可以迅速连接数据库了。无阿点是:MybatisPlus这种过度封装的框架,强行把service层,dao层,entity层绑定在一起,耦合度太高了。
2)常用注解
见后文。
3)使用
Mybatis-Plus通过EntityWrapper(简称EW,MP封装的一个查询条件构造器)或者条件(与EW类似)来让用户自由的构建查询条件,简单便捷,没有额外的负担,能够有效提高开发效率。
public interface UserMapper extends BaseMapper<User> { }
基本CRUD:
//插入:成功会自动回写主键到实体类
result = userMapper.insert(user);
也可以直接:user.insert();
//更新:
result = userMapper.updateById(user);
也可以直接:result = user.updateById();
//删除
result = userMapper.deleteById(user.getId());
也可以直接:result = t2.deleteById();
//查询
User exampleUser = userMapper.selectById(user.getId());或者直接:User exampleUser = t1.selectById();
//批量查询:查询姓名为‘张三’的所有用户记录
List<User> userList = userMapper.selectList(
new EntityWrapper<User>().eq("name", "张三")
); 或者直接:List<User> userList1 = user.selectList(...);
//分页查询: 10 条姓名为‘张三’的用户记录
List<User> userList = userMapper.selectPage(
new Page<User>(1, 10),
new EntityWrapper<User>().eq("name", "张三")
);或者直接:user.selectPage(...);
// 分页查询 10 条姓名为‘张三’、性别为男,且年龄在18至50之间的用户记录
List<User> userList = userMapper.selectPage(
new Page<User>(1, 10),
new EntityWrapper<User>().eq("name", "张三")
.eq("sex", 0)
.between("age", "18", "50")
);
4,原理
5,常用注解
1)类注解
1>@Mapper
在mapper接口使用@Mapper注解,编译之后会生成相应的接口实现类。xml文件可以不写。
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
使用 @Mapper,最终 Mybatis 会有一个拦截器,会自动的把 @Mapper 注解的接口生成动态代理类。
2>@MapperScan
每个接口可以不写@Mapper注解,统一在启动类上用@MapperScan注解。@MapperScan 配置一个或多个包路径,自动的扫描这些包路径下的类,自动的为它们生成代理类。
import org.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
//直接注入某个类
@MapperScan(basePackageClasses = com.lwh.mapper.UserMapper.class)
//直接注入某个路径
@MapperScan({"com.xttblog.mapper","com.xttblog.dao"})
@MapperScan(value = {"com.fanr.graduation.mapper"})
@ComponentScan(basePackages = {"com.xttblog.*"})
当使用了 @MapperScan 注解,将会生成 MapperFactoryBean, 如果没有标注 @MapperScan 也就是没有 MapperFactoryBean 的实例,就走 @Import 里面的配置,具体可以在 AutoConfiguredMapperScannerRegistrar 和 MybatisAutoConfiguration 类中查看源代码进行分析。
2)PO注解
1>@TableName
实体类对应表名,注解在类上。
import com.baomidou.mybatisplus.annotation.TableName;
//autoResultMap = true表示:xml 字段映射 resultMap ID
@TableName(value = "tbl_user", autoResultMap = true)
// 注解指定表名
public class User extends Model<User> {
}
2>@TableId
主键字段,注解在属性上。
通过value字段写不同的命名方式(可省略);通过type设置自增算法(默认雪花算法)。
import com.baomidou.mybatisplus.annotation.TableId;
@TableId(value="uid",type=IdType.AUTO)
private long id;
值 | 描述 |
---|---|
IdType.AUTO | 数据库ID自增 |
IdType.NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) |
IdType.INPUT | insert前自行set主键值 |
IdType.ASSIGN_ID | 分配ID(主键类型为Number(Long和Integer)或String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法) |
IdType.ASSIGN_UUID | 分配UUID,主键类型为String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认default方法) |
除了通过type属性值来配置主键的规则外,还可以通过application.yml文件来配置:
mybatis-plus:
global-config:
db-config
##注解设置
id-type:auto
##对于没有值的数据自动赋予空,设置为false可能查不出来导致返回字段不全的问题。
call-setters-on-nulls: true
3>@TableField
设置字段属性,通过value命名(value=
可以省略),默认驼峰命名法。
//实体临时字段
@TableField(exist=false)
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
import org.apache.ibatis.type.JdbcType;
//fill为自动填充策略,
//FieldFill: DEFAULT:默认不处理;INSERT:插⼊时填充字段;UPDATE:更新时填充字段;INSERT_UPDATE:插⼊和更新时填充字
@TableField(jdbcType = JdbcType.TIMESTAMP, fill = FieldFill.INSERT)
private Date timestamp;//pg中为timestamp类型
@TableField(value = "category_name", jdbcType = JdbcType.VARCHAR)
private String categoryName;
@TableField(jdbcType = JdbcType.INTEGER)
private Integer times;//pg中对应为int4
@TableField(value = "user_info", typeHandler = FastjsonTypeHandler.class)
private List<JSONObject> userList;
@TableField(jdbcType = JdbcType.VARCHAR, typeHandler = JsonbTypeHandler.class)
private Map<String, String> map = Collections.EMPTY_MAP;//pg中对应为jsonb
@TableField(jdbcType = JdbcType.VARCHAR,typeHandler = JsonListTypeHandler.class)
private List<String> list = new ArrayList<>();//pg中对应为jsonb
@TableField(jdbcType = JdbcType.BIGINT)
private Long time = System.currentTimeMillis();//pg中对应为int8
@TableField(jdbcType=JdbcType.BOOLEAN)
private Boolean state;
@TableField(jdbcType = JdbcType.ARRAY)
private int[] arr= new int[0];//pg中对应int4[]
@Type(type = "DisplayAndJumpLinkJsonType")
private DisplayAndJumpLink displayAndJumpLink;
值 | 描述 |
---|---|
value | 字段值(驼峰命名方式,该值可无) |
el | 是否为数据库表字段( 默认 true 存在,false 不存在 ) |
exist | 是否为数据库表字段( 默认 true 存在,false 不存在 ) |
strategy | 字段验证 ( 默认 非 null 判断,查看 com.baomidou.mybatisplus.enums.FieldStrategy ) |
fill | 字段填充标记 ( 配合自动填充使用 ) |
4>@TableLogic
逻辑删除,在属性上命名。
- 数据库中创建逻辑删除状态列,设置默认值为0; is_deleted int
- 实体类添加注解
@TableLogic
private Integer isDeleted;//删除时执行的是update语句
5>其它
3)Mapper注解
1>@Param
相当于xml中 parameterType
。
//xml中通过#{id1}引用
User getUserById(@Param("id1")Long id);
//xml中通过#{people.id}引用具体字段
User getUserById(@Param("people")People people);
sql中参数的两种方式:
${}
本质是字符串拼接,容易引起sql注入,不推荐使用。
#{}
本质是占位符赋值
$
转换#
的方式:
name like concat('%',#{keyword,jdbcType=VARCHAR},'%')
<if test="ids != null">
and id in
<foreach collection="ids" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
2>@Select
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
public interface UserMapper extends BaseMapper<User> {
//1. 可以直接使用 `@Select` 注解。
//2. xml中可以使用select标签进行绑定与映射,id一定要与方法名(getUserById)保持一致。
@Select("select* from t_user where id =7")
User getUserById(@Param("id")Long id);
}
3>@Options
设置缓存时间,能够为对象生成自增的key。
@Insert("INSERT INTO city (name, state, country) VALUES(#{name}, #{state}, #{country})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void insert(City city);
--useGenerateKey=true : 设置是否使用JDBC的getGenereatedKeys方法获取主键并赋值到keyProperty设置的领域模型属性中
--useCache = true表示本次查询结果被缓存以提高下次查询速度,
--flushCache = false表示下次查询时不刷新缓存,
--timeout = 10000表示查询结果缓存10000秒。
4>@Insert
我希望通过dao层的接口插入的数据能够返回主键的id(最终插入成功后从instance_id这个字段里面把数据放到传入对象的instanceId成员变量里面):
@Insert("insert into instance (infos)"
+ " ("
+ " @{infos},"
+ " NOW()"
+ ")")
@Options(useGeneratedKeys = true, keyProperty = "instanceId", keyColumn = "instance_id")
int addInstance(Instance instance);
5>@Update
@Update("sql")
public void createIfNotExistsTable();
6>@Delete
@Delete("delete from t_user where id=#{id}")
int deleteUser(@Param("id") Integer id);
7>@ResultType
指定返回类型
@Select(value = "SELECT s.ID,s.CATEGORY_NAME AS name, s.PARENT_ID,p.CATEGORY_NAME AS parent_name FROM cd_category s LEFT JOIN cd_category p ON p.ID=s.PARENT_ID where FIND_IN_SET(s.id, query_children_category(${id}))")
@ResultType(ItemCategoryModel.class)
public List<ItemCategoryModel> getItemCategoryTree(@Param(value = "id") Integer id);
@Select("select role_id AS roleId from user_role where user_id = #{userId}")
@ResultType(Long.class)
List<Long> selectRoleIdListByUserId(@Param("userId") Long userId);
8>@Results和@Result
当数据库字段名与实体类对应的属性名不一致时,可以使用@Results映射来将其对应起来。column为数据库字段名,porperty为实体类属性名,jdbcType为数据库字段数据类型,id为是否为主键。
@SelectProvider(type=SqltoolMetadataSqlProvider.class, method="selectByExample")
@Results({
@Result(column="ID", property="id", jdbcType=JdbcType.INTEGER, id=true),
@Result(column="NAME", property="name", jdbcType=JdbcType.VARCHAR)
})
List<SqltoolMetadata> selectByExampleWithRowbounds(SqltoolMetadataCriteria example, RowBounds rowBounds);
9>@ResultMap
当这段@Results代码需要在多个方法用到时,为了提高代码复用性,我们可以为这个@Results注解设置id,然后使用@ResultMap注解来复用这段代码。
@Select({"select id, name, class_id from my_student"})
@Results(id="studentMap", value={
@Result(column="id", property="id", jdbcType=JdbcType.INTEGER, id=true),
@Result(column="class_id", property="classId", jdbcType=JdbcType.INTEGER)
})
List<Student> selectAll();
@Select({"select id, name, class_id from my_student where id = #{id}"})
@ResultMap(value="studentMap")
Student selectById(integer id);
10>@One
当我们需要通过查询到的一个字段值作为参数,去执行另外一个方法来查询关联的内容,而且两者是一对一关系时,可以使用@One注解来便捷的实现。
@Select({"select id, name, class_id from my_student"})
@Results(id="studentMap", value={
@Result(column="id", property="id", jdbcType=JdbcType.INTEGER, id=true),
@Result(column="class_id", property="myClass", javaType=MyClass.class,
one=@One(select="com.example.demo.mapper.MyClassMapper.selectById"))
})
List<Student> selectAllAndClassMsg();
11>@Many
与@One类似,只不过如果使用@One查询到的结果是多行,会抛出TooManyResultException异常,这种时候应该使用的是@Many注解,实现一对多的查询。
@Select({"select id, name, class_id from my_student"})
@Results(id="studentMap", value={
@Result(column="id", property="id", jdbcType=JdbcType.INTEGER, id=true),
@Result(column="class_id", property="classId", jdbcType=JdbcType.INTEGER),
@Result(column="id", property="gradeList", javaType=List.class,
many=@Many(select="com.example.demo.mapper.GradeMapper.selectByStudentId"))
})
List<Student> selectAllAndGrade();
12>@SelectProvider
用自定义的provider类构造SQL语句,sql语句由type指定的类中的method方法返回。
// mapper类:
@SelectProvider(type = SalesOrderProvider.class, method = "selectSalesInformation")
List<SalesInformation> selectSalesInformation(@Param("createDateStart") String createDateStart, @Param("createDateEnd") String createDateEnd);
//SalesOrderProvider类
public String selectSalesInformation(@Param("createDateStart") String createDateStart, @Param("createDateEnd") String createDateEnd){
StringBuffer sql = new StringBuffer();
sql.append(" SELECT * from user");
return sql.toString();
}
13>@InsertProvider
批量新增:
@InsertProvider(type = BatchPreparationMsg.class, method = "batchAdd")
Integer batStuAdd(@Param("list") List<PreparationMsg> list);
//BatchPreparationMsg类:
public String batchAdd(@Param("list") List<TaskTemplate> list){
StringBuilder sb = new StringBuilder();
sb.append("insert into pep_preparation_msg(REF_TABLE,REF_ID,HOTEL_ID,TASK_ID,TYPE,NAME,CONTENT,USERGROUP,SHOW_FLAG,CREATE_USER,CREATE_TIME) values");
MessageFormat mf = new MessageFormat(
"(#'{'list[{0}].refTable},#'{'list[{0}].refId},#'{'list[{0}].hotelId},#'{'list[{0}].taskId},#'{'list[{0}].type},#'{'list[{0}].name}," +
"#'{'list[{0}].content},#'{'list[{0}].usergroup},#'{'list[{0}].showFlag},#'{'list[{0}].createUser},#'{'list[{0}].createTime})"
);
for (int i = 0; i < list.size(); i++) {
sb.append(mf.format(new Object[]{i}));
if (i < list.size() - 1) {
sb.append(",");
}
}
return sb.toString();
}
14>@UpdateProvider
类似@InsertProvider,批量更新。
15>@DeleteProvider
类似@InsertProvider,批量删除。
16>@SelectKey
类似标签<selectKey>
@SelectKey(statement="SELECT LAST_INSERT_ID()", keyProperty="clusterId", before=false, resultType=Integer.class)
- statement属性:填入将会被执行的 SQL 字符串数组。
- keyProperty属性:填入将会被更新的参数对象的属性的值。
- before属性:填入 true 或 false 以指明 SQL 语句应被在插入语句的之前还是之后执行。
- resultType属性:填入 keyProperty 的 Java 类型。
- statementType属性:填入Statement、 PreparedStatement 和 CallableStatement 中的 STATEMENT、 PREPARED 或 CALLABLE 中任一值填入 。默认值是 PREPARED。
注意:
@SelectKey注解用在已经被 @Insert 或 @InsertProvider 或 @Update 或 @UpdateProvider 注解了的方法上。若在未被上述四个注解的方法上作 @SelectKey 注解则视为无效。
如果向数据库中插入一条数据,同时有希望返回该条记录的主键,该怎么处理了?有两种情况:
(1)数据库主键不是自增列,需要预先生成
(2)是自增列,插入之后才能获知
这两种情况都可以通过SelectKey解决,第一个种就是before,第二张是after。
17>@MapKey
mybatis返回map类型,希望多条数据放到Map<K,T>中而不是List<Map<K,T>>。
@Select("select id, name from users")
@MapKey("id")
Map<Integer, User<String>> getAMapOfUsers();
18>@ConstructorArgs
根据构造方法构造对象返回。
@Select("select id, name from product where name = #{value}")
@ConstructorArgs({
@Arg(id = true, column="id", javaType = ProductId.class, jdbcType=JdbcType.INTEGER),
@Arg(column="name")
})
Product getProductByNameUsingConstructor(String name);
19>@CacheNamespaceRef和@CacheNamespace
CacheNamespace的引用,表示开启二级缓存,相当于<cache>
标签。
<mapper namespace="cn.mybatis.mydemo.mapper.StudentMapper">
<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"></cache>
</mapper>
当然,前提还需要在全局配置文件中开启缓存:
<setting name="cacheEnabled" value="true"/>
或者这样使用:
@CacheNamespaceRef(name="com.luo.test.UserMapper"
public interface UserMapper extends BaseMapper<User> {
}
6,原理
1)MyBatis工作原理
2)MyBatis插件 工作原理
MyBatis 通过使用拦截器可以自定义增强MyBatis 的功能。MyBatis 通过JDK动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能。每当执行这4种对象(ParameterHandler、ResultSetHandler、Executor、StatementHandler)的方法时,就会进入拦截方法(InvocationHandler的invoke()方法。
运行拦截的对象:
对象 | 描述 |
---|---|
ParameterHandler | sql参数组装的过程 |
ResultSetHandler | 返回结果集的组装 |
Executor | 上层的对象,sql执行全过程,包括组装参数、组装结果集返回、执行SQL过程 |
StatementHandler | 执行SQL的过程,最常用的拦截对象 |
自定义插件实现:
/**
* @Intercepts 注解标记这是一个拦截器,其中可以指定多个@Signature
* @Signature 指定该拦截器拦截的是四大对象中的哪个方法
* type:拦截器的四大对象的类型
* method:拦截器的方法,方法名
* args:入参的类型,可以是多个,根据方法的参数指定,以此来区分方法的重载
*/
@Intercepts(
{
@Signature(type = ParameterHandler.class,method ="setParameters",args = {PreparedStatement.class})
}
)
public class ParameterInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("拦截器执行:"+invocation.getTarget());
//目标对象
Object target = invocation.getTarget();
//获取目标对象中所有属性的值,因为ParameterHandler使用的是DefaultParameterHandler,因此里面的所有的属性都封装在其中
MetaObject metaObject = SystemMetaObject.forObject(target);
//使用xxx.xxx.xx的方式可以层层获取属性值,这里获取的是mappedStatement中的id值
String value = (String) metaObject.getValue("mappedStatement.id");
//如果是指定的查询方法
if ("cn.cb.demo.dao.UserMapper.selectByUserId".equals(value)){
//设置参数的值是admin_1,即是设置id=admin_1,因为这里只有一个参数,可以这么设置,如果有多个需要需要循环
metaObject.setValue("parameterObject", "admin_1");
}
//执行目标方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
//如果没有特殊定制,直接使用Plugin这个工具类返回一个代理对象即可
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
注入Mybatis:
/**
* @Configuration:这个注解标注该类是一个配置类
*/
@Configuration
public class MybatisConfig{
/**
* @Bean : 该注解用于向容器中注入一个Bean
* 注入Interceptor[]这个Bean
* @return
*/
@Bean
public Interceptor[] interceptors(){
//创建ParameterInterceptor这个插件
ParameterInterceptor parameterInterceptor = new ParameterInterceptor();
//放入数组返回
return new Interceptor[]{parameterInterceptor};
}
}
测试:
@Test
void contextLoads() {
//传入的是1222
UserInfo userInfo = userMapper.selectByUserId("1222");
System.out.println(userInfo);
//测试代码传入的是1222,由于插件改变了入参,因此查询出来的应该是admin_1这个人。
}