mybatis从入门到精通(第1~5章)
1.ORM
前言
ORM之前的JDBC操作步骤
- 注册数据库驱动类:url,username,password等连接信息
- DriverManager打开数据库连接connection
- 通过连接创造statement对象
- 通过statement执行sql语句得到resultSet对象
- 通过resultSet对象读取数据转化成javaBean对象
- 关闭resultSet,statement,connection
在上述步骤中1~4以及步骤6在每次执行查询语句中都是重复的,在增删改中也会出现类似的代码,一般会把前四个步骤和第六步分装成DBUtil类,步骤5是完成了关系模型到对象模型的转换,不好封装。为了解决繁琐的代码ORM框架由此而生
1.简介
ORM(object relation mapping 对象关系映射)
主要功能 :
根据映射配置文件,完成数据库在对象模型和关系模型之间的映射,况且前言中的步骤都是粗暴的完成了访问数据库然后关闭连接,在实际开发中需要 集成缓存,数据源,数据库连接池等组件进行优化 ORM的数据源和缓存都可在配置文件中自己进行配置,不需要修改源代码,提高了维护性和开发效率。
mybatis优点
-
动态的拼接sql,比如说多个条件查询时,要判断参数为不为空,哪里要加and 加in 加where 等等,如果是普通的jdbc是一个很繁琐的过程且没有技术含量,mybatis根据执行时传入的参数拼凑成完整的,可以执行的sql
-
支持代码生成器
在学习第2章 Mybatis的基本用法时,我们写了很多单表的增、删、改、査方法,基本上每个表都要有这些方法,这些方法都很规范并且也比较类似。
当数据库表的字段比较少的时候,写起来还能接受,一旦字段过多或者需要在很多个表中写这些基本方法时,就会很麻烦,不仅需要很大的代码量,而且字段过多时很容易出现错乱。尤其在新开始一个项目时,如果有几十个甚至上百个表需要从头编写,这将会带来很大的工作量,这样的工作除了能让我们反复熟练这些基本方法外,完全就是重复的体力劳动。
2 springboot集成mybatis
1. 集成步骤
-
配置数据源和mapper.xml文件放的位置
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatis_study?useUnicode=true&characterEncoding=utf8&useSSL=true&&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&serverTimezone=GMT%2B8 username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.yogurt.mybatis_study.pojo configuration: #驼峰模式到羊肉串模式 map-underscore-to-camel-case: true
-
编写mapper类并在启动类上加上 @MapperScan 注解
@Repository public interface CountryMapper { List<Country> selectAll(); }
-
在resource/mapper/文件夹下编写XxxMapper.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.yogurt.mybatis_study.mapper.CountryMapper"> <select id="selectAll" resultType="country"> select * from mybatis_study.country </select> </mapper>
-
测试
@Autowired CountryMapper countryMapper; @Test void contextLoads() { List<Country> countries = countryMapper.selectAll(); for (Country country : countries) { log.info(country.toString()); } }
2. springboot+mybatis打印sql日志
Spring Boot官方推荐优先使用带有 -spring 的文件名作为你的日志配置(如使用 *logback-spring.xml* ,而不是logback.xml),命名为logback-spring.xml的日志配置文件,spring boot可以为它添加一些spring boot特有的配置项
根据不同的日志系统,你可以按如下规则组织配置文件名,就能被正确加载:
Logback:logback-spring.xml,
logback-spring.groovy
, logback.xml
, logback.groovy
Log4j:log4j-spring.properties
, log4j-spring.xml
, log4j.properties
, log4j.xml
Log4j2:log4j2-spring.xml
, log4j2.xml
JDK (Java Util Logging):logging.properties
默认的命名规则,并且放在 src/main/resources 下如果你即想完全掌控日志配置,但又不想用logback.xml作为Logback配置的名字,application.yml可以通过logging.config属性指定自定义的名字
logging:
config: classpath:logbackXXX.xml
针对DAO的包进行DEBUG日志设置 TRACE的level可以打印结果Debug只能打印sql语句
<logger name="com.yogurt.mybatis_study.mapper" level="TRACE" />
完整配置请看springboot常用的配置文件
也可在需要打印的类上加上lombok的注解@SLF4J来控制日志打印
log.info(country.toString());
3.mybatis基础
1.需要注意的点
特殊的类型“byte[]”。这个类型一般对应数据库中的BLOB、 LONGVARBINARY以及一些和二进制流有关的字段类型
因为接口方法是可以重载的,所以接口中可以出现多个同名但参数不同的方法,但是XML中id的值不能重复,因而接口中的所有同名方法会对应着XML中的同一个id的方法。最常见的用法就是,同名方法中其中一个方法增加一个 Rowbound类型的参数用于实现分页查询。
2.增删改查
1.Select
根据用户id获取用户拥有的所有角色
SysUser selectById(Long id);
<resultMap id="userMap" type="com.yogurt.mybatis_study.model.SysUser">
<id property="id" column="id"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
</resultMap>
//不推荐使用*号,效率慢
<select id="selectById" resultMap="userMap">
select * from mybatis_study.sys_user where id = #{id}
</select>
<select>:映射查询语句使用的标签。
id:命名空间中的唯一标识符,可用来代表这条语句。
resultMap:用于设置返回值的类型和映射表的列表名和bean类字段之间的关系
select标签中的 select+ from sys user where id=#{id}是查询语句
#{id}: Mybatis SQL中使用预编译参数的一种方式,大括号中的id是传入的参数名。
resultMap包含的所有属性如下:
id:必填,并且唯一。在 select标签中,resultMap指定的值即为此处id所设置的值。
type:必填,用于配置查询列所映射到的Java对象类型。
extends:选填,可以配置当前的 resultmap继承自其他的 resultmap,属性值为继承 resultmap的id
automapping:选填,可选值为true或 false,用于配置是否启用非映射字段(没有在 resultmap中配置的字段)的自动映射功能,该配置可以覆盖全局的automappingbehavior配置。
resultMap包含的所有标签如下
constructor:配置使用构造方法注入结果,包含以下两个子标签。
idArg:id参数,标记结果作为id(唯一值),可以帮助提高整体性能。
arg:注入到构造方法的一个普通结果。
id:一个id结果,标记结果作为id(唯一值),可以帮助提高整体性能。
result:注入到Java对象属性的普通结果
association:一个复杂的类型关联,许多结果将包成这种类型。
collection:复杂类型的集合。
discriminator:根据结果值来决定使用哪个结果映射
case:基于某些值的结果映射。
constructor:通过构造方法注入属性的结果值。构造方法中的 idArg、arg参数分别对应着 resultmap中的id、 result标签,它们的含义相同,只是注入方式不同。
resultmap中的id和 result标签包含的属性相同,不同的地方在于,id代表的是主键(或唯一值)的字段(可以有多个),它们的属性值是通过 setter方法注入的。
id、result标签包含的属性:
column:从数据库中得到的列名,或者是列的别名。
property:映射到列结果的属性。可以映射简单的如“ username”这样的属性,也可以映射一些复杂对象中的属性,
例如“ address.street.number”,这会通过“.”方式的属性嵌套赋值。
Javatype:一个Java类的完全限定名,或一个类型别名(通过 typealias配置或者默认的类型)。
如果映射到一个 Javabean, Mybatis通常可以自动判断属性的类型。如果映射到Hashmap,则需要明确地指定Javatype属性。
jdbctype:列对应的数据库类型。JDBC类型仅仅需要对插入、更新、删除操作可能为空的列进行处理。这是 JDBC jdbctype的需要,而不是 Mybatis 的需要。
typehandler:使用这个属性可以覆盖默认的类型处理器。这个属性值是类的完全限定名或类型别名。
名称映射规则:
可以通过在resu1tMap中配置 property属性和 column属性的映射,或者在SQL设置别名这两种方式实现将查询列映射到对象属性的目的。
使用id查找该用户的所有角色
//根据用户id获取用户拥有的所有角色
List<SysRole> selectRolesByUserId(Long userId);
<select id="selectRolesByUserId" resultType="SysRole">
select r.id,r.role_name,r.create_by,r.create_time
from mybatis_study.sys_user u
inner join mybatis_study.sys_user_role ur on u.id = ur.user_id
inner join mybatis_study.sys_role r on r.id = ur.role_id
where u.id = #{userId}
</select>
除了查询所有的role之外还要找到该用户名和用户密码
方法
直接在SysRole类中添加SysUser字段
//根据用户id获取用户拥有的所有角色还有用户名和密码
List<SysRole> selectRolesByUserId2(Long userId);
<select id="selectRolesByUserId2" resultType="SysRole">
select r.id,r.role_name,r.create_by,r.create_time,u.user_name as "user.userName",u.user_password as "user.userPassword"
from mybatis_study.sys_user u
inner join mybatis_study.sys_user_role ur on u.id = ur.user_id
inner join mybatis_study.sys_role r on r.id = ur.role_id
where u.id = #{userId}
</select>
需要注意的点:u.user_name as "user.userName",u.user_password as "user.userPassword" 要对这两列进行名称映射
2.Insert
插入一条数据返回受影响的行数
//插入提条记录
int insert(SysUser sysUser);
<insert id="insert">
insert into mybatis_study.sys_user(id,user_name, user_password, user_email, user_info, head_img, create_time)
values (#{id},#{userName},#{userPassword},#{userEmail},#{userInfo},#{headImg, jdbcType=BLOB},#{createTime, jdbcType=TIMESTAMP})
</insert>
<insert>元素,这个标签包含如下属性:
id:命名空间中的唯一标识符,可用来代表这条语句
parametertype:即将传入的语句参数的完全限定类名或别名。这个属性是可选的,因为 Mybatis可以推断出传入语句的具体参数,因此不建议配置该属性。
flushCache:默认值为true,任何时候只要语句被调用,都会清空一级缓存和二级缓存
timeout:设置在抛出异常之前,驱动程序等待数据库返回请求结果的秒数
statementtype:对于 STATEMENT、 PREPARED、 CALLABLE, Mybatis会分别使用对应的 Statement、 Preparedstatement、Callablestatement,默认值为PREPARED.
useGeneratedKeys:默认值为 false。如果设置为true, Mybatis会使用JDBC的 getgeneratedkeys方法来取出由数据库内部生成的主键
keyproperty: Mybatis通过 getgeneratedkeys获取主键值后将要赋值的属性名。
如果希望得到多个数据库自动生成的列,属性值也可以是以逗号分隔的属性名称列表。
keycolumn:仅对 INSERT和 UPDATE有用。通过生成的键值设置表中的列名,这个设置仅在某些数据库(如 Postgresql)中是必须的,当主键列不是表中的第一列时需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
databaseId:如果配置了 databaseidprovider(4.6节有详细配置方法), Mybatis会加载所有的不带 databaseid的或匹配当前 databaseid的语句。如果同时存在带databaseid和不带 databaseid的语句,后者会被忽略。
为了防止类型错误,对于一些特殊的数据类型,建议指定具体的 jdbcType值。例如headImg指定BLOB类型, createTime指定 TIMESTAMP类型。
插入一条数据返回自增的主键值到UserId中
方式一:userGneratedKeys
/**
*useGeneratedKeys这种方法只适用于支持主键自增的数据库
* @param sysUser user对象
* @return 返回数据库生成的自增主键值
*/
int insert2(SysUser sysUser);
<!--
由于要使用数据库返回的主键值,所以SQL上下两部分的列中去掉了id列和对应的#(id}属性。
这种回写方法只适用于支持主键自增的数据库
-->
<insert id="insert2" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
insert into mybatis_study.sys_user(user_name, user_password, user_email, user_info, head_img, create_time)
values (#{userName},#{userPassword},#{userEmail},#{userInfo},#{headImg},#{createTime})
</insert>
测试
@Test
void userTest4() {
SysUser user = new SysUser();
user.setUserName("test1");
user.setUserPassword("123456");
user.setUserEmail("123@qq.com");
user.setUserInfo("test info");
user.setHeadImg(new byte[]{1,2,3});
user.setCreateTime(new Date());
int insertID = userMapper.insert2(user);
System.out.println("insertID -> "+insertID);
Assert.assertNotNull(user);
//id回写,所以id不为空
Assert.assertNotNull(user.getId());
}
方式二:selectKey
<!--
selectKey适用于数据库不支持主键自增的情况,还能返回主键的值
order="AFTER"
mysql数据库是在语句插入到数据库之后才生成主键的值
oracle是在插入之前生成主键的值
select LAST_INSERT_ID()是mysql的写法。oracle的写法不一样,可以百度
-->
<insert id="insert3">
insert into mybatis_study.sys_user(user_name, user_password, user_email, user_info, head_img, create_time)
values (#{userName},#{userPassword},#{userEmail},#{userInfo},#{headImg},#{createTime})
<selectKey keyColumn="id" keyProperty="id" resultType="Long" order="AFTER">
select LAST_INSERT_ID()
</selectKey>
</insert>
3.update
根据id修改user
int UpdateById(SysUser sysUser);
<update id="UpdateById">
update mybatis_study.sys_user set user_name = #{userName}, user_password = #{userPassword}
, user_email = #{userEmail} ,user_info = #{userInfo},
head_img = #{headImg,jdbcType=BLOB},
create_time = #{createTime,jdbcType=TIMESTAMP}
where id = #{id}
</update>
4. delete
根据id删除user
int deleteById(Long id);
<delete id="deleteById">
delete from mybatis_study.sys_user where id = #{id}
</delete>
3. 多参数的解决办法
通过@param注解来实现多参数传递
给参数配置@param注解后, Mybatis就会自动将参数封装成Map类型,@ Param注解值会作为Map中的key,因此在SQL部分就可以通过配置的注解值来使用参数。当只有一个参数的时候,mybatis不关心参数叫什么名字,而是把这个唯一参数值拿来用
根据用户id和角色的enabled状态来查询角色列表
Enabled需要自定义typeHandler之后再说
List<SysRole> selectRolesByUserIdAndEnabled(@Param("userId") Long id,@Param("enabled") Enabled enabled);
<select id="selectRolesByUserIdAndEnabled" resultType="SysRole">
select r.id,r.create_time,r.enabled,r.role_name,r.create_by
from mybatis_study.sys_user u
inner join mybatis_study.sys_user_role ur on u.id = ur.user_id
inner join mybatis_study.sys_role r on ur.role_id = r.id
where u.id = #{userId} and r.enabled = #{enabled}
</select>
4. 使用注解开发
1.select
select中resultMap的实现是基于resluts注解
根据id查找Role
//配置resultMap
@Results(id = "roleResultMap", value = {
@Result(property = "id", column = "id", id = true),
@Result(property = "roleName", column = "role_name"),
@Result(property = "enabled", column = "enabled"),
@Result(property = "createBy", column = "create_by"),
@Result(property = "createTime", column = "create_time")
})
@Select("select * from mybatis_study.sys_role where id = #{id}")
SysRole selectById(Long id);
在.xml文件中有id和reslutType属性 这边省略了reslutType属性个人猜测是通过反射方法的返回值实现的
Results可以通过id属性全局复用的
//使用resultMap
@ResultMap("roleResultMap")
@Select("select * from mybatis_study.sys_role")
List<SysRole> selectAll();
这边即使不填resultMap也没事,就想知道为啥能拿到reslutType因为泛型是运行时擦除的???
2.insert
插入一条记录,不回写id值
@Insert("insert into mybatis_study.sys_role(id, role_name, enabled, create_by, create_time)
" +
"VALUE(#{id},#{roleName},#{enabled},#{createBy},#{createTime})")
int insert(SysRole role);
插入一条记录,回写id值
方式一 指定useGeneratedKeys
@Insert("insert into mybatis_study.sys_role(role_name, enabled, create_by, create_time)
" +
"VALUE(#{roleName},#{enabled},#{createBy},#{createTime})")
@Options(useGeneratedKeys = true,keyProperty = "id")
int insert2(SysRole role);
方式二 : selectKey
@Insert("insert into mybatis_study.sys_role(role_name, enabled, create_by, create_time)
" +
"VALUE(#{roleName},#{enabled},#{createBy},#{createTime})")
@SelectKey(statement = "SELECT LAST_INSERT_ID()",keyProperty = "id",keyColumn = "id",resultType = Long.class,before = false)
int insert3(SysRole role);
方式一和方式二的区别在上一节中讲了
3.update和delete
@Update("update mybatis_study.sys_role set " +
"role_name = #{roleName}, enabled = #{enabled}, create_by = #{createBy}, create_time = #{createTime}" +
"where id = #{id}")
int updateById(SysRole role);
@Delete("delete from mybatis_study.sys_role where id = #{id}")
int deleteById(Long id);
4.使用@Provider注解开发
使用@provider注解开发的好处是使mysql和接口的定义分开,使项目便于维护,但是修改sql语句时需要重新编译代码,个人觉得还是没有.xml文件好使
使用@provider注解开发步骤:
- 定义xxxMapper接口
- 定义xxxProvider对象,并根据xxxMapper接口写sql语句
- 在mapper上添加@xxxProvider注解
根据id查找privilege
-
xxxMapper
@SelectProvider(type = PrivilegeProvider.class,method = "selectById") SysPrivilege selectById(Long id);
@Provider注解有两个属性type配置的是method方法所在的类(该类必须包含空的构造方法),method方法返回值必须是String
-
xxxProvider
public class PrivilegeProvider { public PrivilegeProvider() { } public String selectById(Long id){ return new SQL(){ { SELECT("id,privilege_name,privilege_url"); FROM("sys_privilege"); WHERE("id = #{id}"); } }.toString(); } }
5.动态sql语句
Mybatis3之后的版本采用了功能强大的OGNL( Object-graph Navigation Language)表达式语言消除了许多其他标签,以下是 Mybatis的动态SQL在XML中支持的几种标签。
choose (when, oterwise)
trim( where、set)
foreach
bind
不仅学习标签的用法,还要学习OGNL
1.if
假设现在有一个新的需求:实现一个用户管理高级查询功能,根据输入的条件去检索用户信息。这个功能还需要支持以下三种情况:当只输入用户名时,需要根据用户名进行模糊查询当只输入邮箱时,根据邮箱进行完全匹配:当同时输入用户名和邮箱时,用这两个条件去查询匹配的用户。
//根据动态条件(用户名和邮箱)查询用户信息
List<SysUser> selectUserByNameAndEmail(@Param("username") String username,@Param("email") String userEmail);
<select id="selectUserByNameAndEmail" resultType="SysUser">
select * from mybatis_study.sys_user
where 1 = 1
<if test="username != null and username != ''">
and user_name like concat('%',#{username},'%')
</if>
<if test="email != null and email != ''">
and user_email = #{email}
</if>
</select>
这里需要注意的是1 = 1 这个条件,如果username和email都为空又没有1 = 1 这个条件会报错,后面可以用<where>
代替
现在要实现这样一个需求:只更新有变化的字段。需要注意,更新的时候不能将原来有值但没有发生变化的字段更新为空或null.通过if标签可以实现这种动态列更新。
//只更新有变化的字段。更新的时候不能将原来有值但没有发生变化的字段更新为空或null.
int updateUserByIdSelective(SysUser user);
<update id="updateUserByIdSelective">
update mybatis_study.sys_user
set
<if test="userName != null and userName != ''">
user_name = #{userName},
</if>
<if test="userPassword != null and userPassword != ''">
user_password = #{userPassword},
</if>
<if test="userEmail != null and userEmail != ''">
user_email = #{userEmail},
</if>
<if test="userInfo != null and userInfo != ''">
user_info = #{userInfo},
</if>
<if test="headImg != null">
head_img = #{headImg},
</if>
<if test="createTime != null">
create_time = #{createTime},
</if>
id = #{id}
where id = #{id}
</update>
这里需要注意id = #{id}
这个条件,如果前面的都为空,或者有的不为空,这个条件都可以是sql语句不报错误
在数据库表中插入数据的时候,如果某一列的参数值不为空,就使用传入的值,如果传入参数为空,就使用数据库中的默认值(通常是空),而不使用传入的空值。
//在数据库表中插入数据的时候,如果某一列的参数值不为空,就使用传入的值,如果传入参数为空,就使用数据库中的默认值(通常是空),而不使用传入的空值。
//这里以email为例
int insertIfNullGetDefault(SysUser user);
<insert id="insertIfNullGetDefault" useGeneratedKeys="true" keyProperty="id">
insert into mybatis_study.sys_user
(user_name, user_password,
<if test="userEmail != null and userEmail != ''">
user_email,
</if>
user_info, head_img, create_time)
value
(#{userName},#{userPassword},
<if test="userEmail != null and userEmail != ''">
#{userEmail},
</if>
#{userInfo},#{headImg},#{createTime})
</insert>
这里注意userName在列部分加了if
标签那么在value
部分也要加
2. choose
实现if...else、if...else的逻辑,要想实现这样的逻辑,就需要用到 choose when otherwise标签。 choose元素中包含when和 otherwise两个标签,一个 choose中至少有一个when,有0个或者1个otherwise。
在已有的 sys_user表中,除了主键id外,我们认为 user name(用户名)也是唯一的,所有的用户名都不可以重复。现在进行如下查询:当参数id有值的时候优先使用id查询,当id没有值时就去判断用户名是否有值,如果有值就用用户名查询,如果用户名也没有值,就使SQL查询无结果。
SysUser selectByIdOrUserName(SysUser user);
<select id="selectByIdOrUserName" resultType="SysUser">
select * from mybatis_study.sys_user
where 1 = 1
<choose>
<when test="id != null">
and id = #{id}
</when>
<when test="userName != null">
and user_name = #{userName}
</when>
<otherwise>
and 1 = 2
</otherwise>
</choose>
</select>
要加otherwise
这个条件否则条件不满足时会返回全部的user但是返回值只返回一个所以会报错
3.where set trim
1. where
where
标签的用法:当where标签包含的元素有返回值时,加上where,如果后面的字符串是以and or 开头的就将他们剔除
之前的需求:实现一个用户管理高级查询功能,根据输入的条件去检索用户信息。这个功能还需要支持以下三种情况:当只输入用户名时,需要根据用户名进行模糊查询当只输入邮箱时,根据邮箱进行完全匹配:当同时输入用户名和邮箱时,用这两个条件去查询匹配的用户。
<select id="selectUserByNameAndEmail" resultType="SysUser">
select * from mybatis_study.sys_user
<where>
<if test="username != null and username != ''">
and user_name like concat('%',#{username},'%')
</if>
<if test="email != null and email != ''">
and user_email = #{email}
</if>
</where>
</select>
2. set
set
标签的用法是:如果set标签包含的元素有返回值则插入set, 如果set里面的字符串是以,
结尾的就去掉,
之前的需求:只更新有变化的字段。需要注意,更新的时候不能将原来有值但没有发生变化的字段更新为空或null.通过if标签可以实现这种动态列更新。
<update id="updateUserByIdSelective">
update mybatis_study.sys_user
<set>
<if test="userName != null and userName != ''">
user_name = #{userName},
</if>
<if test="userPassword != null and userPassword != ''">
user_password = #{userPassword},
</if>
<if test="userEmail != null and userEmail != ''">
user_email = #{userEmail},
</if>
<if test="userInfo != null and userInfo != ''">
user_info = #{userInfo},
</if>
<if test="headImg != null">
head_img = #{headImg},
</if>
<if test="createTime != null">
create_time = #{createTime},
</if>
id = #{id},
</set>
where id = #{id}
</update>
id = #{id},
这一表达式不能丢掉,也不能放进if标签,如果没有这条表达式SQL语句还是会出错,set
标签并没有解决这个问题
3. trim
where
,set
其实是trim
的两种具体的表现形式
trim标签有如下属性:
- prefix:当trim元素内包含内容时,会给内容增加 prefix指定的前缀。
- prefixOverrides:当trim元素内包含内容时,会把内容中匹配的前缓字符串去掉。
- suffix:当trim元素内包含内容时,会给内容增加 suffix指定的后缀。
- suffixOverrides:当trim元素内包含内容时,会把内容中匹配的后缀字符串去掉。
WHERE
标签的实现
<trim prefix="where" prefixOverrides="and |or ">
...
</trim>
and 和 or 后面的空格不能省略,以免碰到开头有 andes 或者 order 这样的单词 那就会变成 es 或者 der 这样SQL就报错了
SET
标签的实现
<trim prefix="set" suffixOverrides=",">
...
</trim>
4. foreach
SQL语句中有时会使用IN关键字,例如idin(1,2,3)。可以使用${ids}方式直接获取值,但这种写法不能防止SQL注入,想避免SQL注入就需要用#(}的方式,这时就要配合使用 foreach标签来满足需求。
foreach可以对数组、Map或实现了 Iterable接口(如ist、set)的对象进行遍历。数组在处理时会转换为List对象,因此 foreach遍历的对象可以分为两大类: Iterable类型和Map类型。这两种类型在遍历循环时情况不一样,这一节会通过3个例子来讲解foreach的用法。
1. foreach实现in集合
根据用户id集合查询
//根据用户id集合查询
List<SysUser> selectByIdList(List<Long> idList);
<select id="selectByIdList" resultType="SysUser">
select * from mybatis_study.sys_user
where id in
<foreach collection="list" separator="," open="(" close=")" index="i" item="id">
#{id}
</foreach>
</select>
foreach包含以下属性。
- collection:必填,值为要迭代循环的属性名。这个属性值的情况有很多。
- item:变量名,值为从迭代对象中取出的每一个值。
- index:索引的属性名,在集合数组情况下值为当前索引值,当迭代循环的对象是Map类型时,这个值为Map的key(键值)。
- open:整个循环内容开头的字符串。
- close:整个循环内容结尾的字符串。
- separator:每次循环的分隔符。
collection属性值怎么填写
在ParamNameResolver中有如下代码
public static Object wrapToMapIfCollection(Object object, String actualParamName) {
if (object instanceof Collection) {
ParamMap<Object> map = new ParamMap<>();
map.put("collection", object);
if (object instanceof List) {
map.put("list", object);
}
Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
return map;
} else if (object != null && object.getClass().isArray()) {
ParamMap<Object> map = new ParamMap<>();
map.put("array", object);
Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
return map;
}
return object;
}
可见当集合是list是collection属性值是 list,当集合是map是collection属性值是 _parameter,当集合是array是collection属性值是 array
更推荐使用@param指定的值作为collection的属性值,这样意图更加清晰
2.foreach实现批量插入数据
批量插入用户信息,并返回插入用户的主键值
//批量插入用户信息
int insertList(@Param("userList") List<SysUser> userList);
<insert id="insertList" useGeneratedKeys="true" keyProperty="id">
insert into mybatis_study.sys_user(user_name, user_password, user_email, user_info, head_img, create_time) values
<foreach collection="userList" item="user" separator=",">
(#{user.userName},#{user.userPassword},#{user.userEmail},#{user.userInfo},#{user.headImg, jdbcType=BLOB},#{user.createTime, jdbcType=TIMESTAMP})
</foreach>
</insert>
item指定循环变量名后要使用 属性.属性
的方式 user.userName
批量返回用户的主键值不是所有的数据库都支持的(要求数据库支持主键自增且实现的JDBC提供的接口),JDBC虽然提供了接口,但是有些数据库厂商是没有实现的,MYSQL支持这种功能。
3. foreach实现动态Update
当集合是map时,index是map的key值,利用这一点我们可以实现动态Update
通过map更新列
//动态update
int updateByMap(Map<String,Object> map);
<update id="updateByMap">
update mybatis_study.sys_user set
<foreach collection="_parameter" separator="," item="val" index="key">
${key} = #{val}
</foreach>
where id = #{id}
</update>
这里使用的是map的默认值 _parameter
5. bind
bind标签可以使用OGNL表达式创建一个变量并将其绑定到上下文中
之前实现需求5.1
的时候,在字符拼接的时候是这样使用的
<if test="username != null and username != ''">
and user_name like concat('%',#{username},'%')
</if>
concat在mysql数据库中支持三个字符的拼接,但是orcle不支持,所以更换数据库还得重写SQL,使用bind可以解决这个问题,使用bind还能防止SQL注入
6. OGNL表达式
常用的OGNL表达式如下
e1 or e2
e1 and e2
e1 == e2 或 e1 eq e2
e1 != e2 或 e1 neq e2
e1 lt e2:小于
e1 lte e2:小于等于,其他表示为 gt(大于)、gte(大于等于)
e1+e2、e1*e2、e1/e2、e1 - e2、e1%e2
!e 或 not e:非,取反
e.method(args):调用对象方法
e.property:对象属性值
e1[e2]: 按索引取值(list、数组和Map)
@class@method(args): 调用类的静态方法
@class@field: 调用类的静态字段值
<if test="username != null and username != ''">
...
</if>
通过ONGL表达式可以写为
<if test="@com.yogurt.util.StringUtil@isNotEmpty(username)">
...
</if>
还可以打印映射XML中的参数
<update id="updateByMap">
<bind name="print" value="@com.yogurt.util.StringUtil@print(_parameter)"/>
update mybatis_study.sys_user set
<foreach collection="_parameter" separator="," item="val" index="key">
${key} = #{val}
</foreach>
where id = #{id}
</update>
其中StringUtil的代码如下
public class StringUtil {
public static boolean isEmpty(String str){
return str == null || str.length() == 0;
}
public static boolean isNotEmpty(String str){
return !isEmpty(str);
}
public static void print(Object parameter){
System.out.println(parameter);
}
}
7. 多数据库的实现
多数据的实现在这里留个坑,在需要使用的时候再来记笔记
6.Mybatis代码生成器
由于自定义的内容太多。本章只讲springboot和Mybatis-Generator的一个入门案例
-
再POM文件中引入mybatis和mysql的依赖
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
-
引入Mybatis-Generator插件
<plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.2</version> <executions> <execution> <id>mybatis-generator</id> <phase>deploy</phase> <goals> <goal>generate</goal> </goals> </execution> </executions> <configuration> <!-- Mybatis-Generator 工具配置文件的位置 --> <configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> <!--JDBC driver mybatis-generator --> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> </dependency> </dependencies> </plugin>
-
在上面指定的配置文件目录下写generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- targetRuntime="MyBatis3Simple" 生成简单的增删改查代码 如果指定为Mybatis3 MBG会生成和Example相关的对象和方法(可以使用该对象和其方法生成sql语句) defaultModelType="flat" 一张表生成一个实体类 --> <context id="MySqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <!-- 配置前后分隔符如表的名字为 user info 那么sql语句会加分隔符 `user info` 这样不会报错 --> <property name="beginningDelimiter" value="`"/> <property name="endingDelimiter" value="`"/> <property name="javaFileEncoding" value="UTF-8"/> <!-- name="suppressDate" 阻止生成包含时间戳的注释 name="addRemarkComments" 是否添加数据库表的备注信息 也可以自定义commentGenerator,官网上找 --> <commentGenerator> <property name="suppressDate" value="true"/> <property name="addRemarkComments" value="true"/> </commentGenerator> <!--jdbcConnection : 指定MBG要连接的数据库的信息 这个标签下如果包含property的标签,那么该标签的属性会添加到jdbc驱动的属性中 --> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/mybatis_study?useUnicode=true&characterEncoding=utf8&useSSL=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&serverTimezone=GMT%2B8" userId="root" password="123456"> </jdbcConnection> <!-- targetPackage="com.yogurt.model" model生成包的路径 targetProject="srcmainjava" 项目路径 --> <javaModelGenerator targetPackage="com.yogurt.model" targetProject="srcmainjava"> <!-- name="trimStrings" 判断是否对数据库查询结果进行trim操作 默认false 改为true后 public void setCountryCode(String countryCode) { this.countryCode = countryCode == null ? null : countryCode.trim(); } --> <property name="trimStrings" value="true" /> </javaModelGenerator> <sqlMapGenerator targetPackage="mapper" targetProject="srcmain esources"/> <!-- 配置Java客户端生成器( Mapper接口)的属性 type="XMLMAPPER" mapper接口与mapper.xml文件完全分离,易于维护,推荐使用 --> <javaClientGenerator type="XMLMAPPER" targetPackage="com.yogurt.dao" targetProject="srcmainjava"/> <!-- table可以有多个,每个数据库中的表都可以写一个table,tableName表示要匹配的数据库表 ,也可以在tableName属性中通过使用%通配符来匹配所有数据库表,只有匹配的表才会自动生成文件 --> <table tableName="country" enableCountByExample="true" enableUpdateByExample="true" enableDeleteByExample="true" enableSelectByExample="true" selectByExampleQueryId="true"> <!--如果设置为true,那么MBG会使用从数据库元数据获取的列名作为生成的实体对象的属性 为false(默认)会将返回的名称转换为驼峰模式--> <property name="useActualColumnNames" value="false" /> <!-- generatedKey:指定自动生成主键的属性,指定这个标签,在生成的insert语句中就会添加selectKey标签,用于主键回写 --> <generatedKey column="id" sqlStatement="Mysql" identity="true" /> </table> </context> </generatorConfiguration>
-
在application.yml中添加datasource以及基本的mybatis配置
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatis_study?useUnicode=true&characterEncoding=utf8&useSSL=true&&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&serverTimezone=GMT%2B8 username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.yogurt.model configuration: #mybtais配置 map-underscore-to-camel-case: true
-
点击插件生成mapper接口、实体类和mapper.xml文件
-
成果
-
测试
@Autowired CountryMapper countryMapper; @Test void contextLoads() { List<Country> countries = countryMapper.selectAll(); for (Country country : countries) { log.info(country.toString()); } }
-
测试成功
还有很多自定义配置请看官网或者mybatis从入门到精通