简介
- MyBatis3中文文档
MyBatis Generator
:mybatis代码生成(model/dao/mapper),文档
简单使用
1 | String resource = "SqlMapConfig.xml"; |
整合mybatis(SpringBoot)
引入依赖(mybatis-spring-boot-starter为mybatis提供的自动配置插件) ^1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<!-- 自动配置 https://github.com/mybatis/spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<!-- <version>1.3.1</version> -->
</dependency>
<!--mybatis分页插件: https://github.com/pagehelper/Mybatis-PageHelper-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.0.4</version>
</dependency>
<!-- 方式二:会包含mybatis依赖,并且无需再mybatis配置文件中配置此插件。如果使用mybatis-plus插件则不需要此分页插件
自动装配时,会获取初始化后的SqlSessionFactory,然后获取其Configuration,再将插件添加进去(而非在SqlSessionFactory初始化前进行配置的)
-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 方式三:使用mybatis-plus(推荐),使用内置自定义的分页插件 -->扫描配置(二者缺一不可)
- 启动类中加
@MapperScan({"cn.aezo.springboot.mybatis.mapperxml", "cn.aezo.springboot.module.*.mapper"})
进行接口扫描- 此处一层包通配符为
.*.
,多次包为.**.
,和mybatis.mapper-locations通配符配合使用进行多模块管理 - mybatis-plus注解方式也是使用此类
- 此处一层包通配符为
- 还需要加mybatis.mapper-locations的配置,进行xml映射文件扫描
- mybatis-plus复写了此配置,对应
mybatis-plus.mapper-locations
,其默认值为classpath*:/mapper/**/*.xml
,因此一般可不用配置此参数
- mybatis-plus复写了此配置,对应
- 启动类中加
- Springboot配置
1 | # 基于xml配置时需指明映射文件扫描位置;设置多个路径可用","分割,如:"classpath:mapper/*.xml(无法扫描其子目录),classpath:mapper2/*.xml" |
mybatis配置文件:
mybatis-config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37"1.0" encoding="UTF-8" xml version=
<!--在application.properties中使用了mybatis.configuration进行配置,无需此文件(传统配置)-->
<configuration>
<settings>
<!--字段格式对应关系:数据库字段为下划线, model字段为驼峰标识(不设定则需要通过resultMap进行转换)-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!--打印mybatis运行的sql语句到控制台。STDOUT表示调用System.out。-->
<!-- 打印到日志,则需要在如logback.xml中加 <logger name="cn.aezo.video.dao" level="debug"/> 定义打印级别 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
<!--类型别名定义-->
<typeAliases>
<!--定义需要扫描的包-->
<package name="cn.aezo.springboot.mybatis.model"/>
<!--定义后可在映射文件中间的parameterType等字段中使用userInfo代替cn.aezo.springboot.mybatis.model.UserInfo-->
<!--<typeAlias alias="userInfo" type="cn.aezo.springboot.mybatis.model.UserInfo" />-->
</typeAliases>
<plugins>
<!-- 分页插件 -->
<!-- 5.0.0以后使用com.github.pagehelper.PageInterceptor作为拦截器 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!--更多参数配置:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md-->
<!--<property name="pageSizeZero" value="true"/>-->
</plugin>
</plugins>
<!-- 基于databaseId实现数据库兼容. DB_VENDOR为mybatis内置别名,对应VendorDatabaseIdProvider -->
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle" />
</databaseIdProvider>
</configuration>Model:UserInfo/ClassInfo等无需任何注解.(其中HobbyEnum是一个枚举类)
annotation版本(适合简单业务)
annotation版本(适合简单业务)
Dao层:UserMapper.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// @Mapper // 在启动类中定义需要扫描mapper的包:@MapperScan("cn.aezo.springboot.mybatis.mapper"), 则此处无需声明@Mapper
public interface UserMapper {
// 1. 此处注入变量可以使用#或者$
// 2. 一个参数可以省略@Param,多个需要进行指定(反射机制)
// 3. 当未获取到数据时,返回 null
// 4. (使用配置<setting name="mapUnderscoreToCamelCase" value="true"/>因此无需转换) 数据库字段名和model字段名或javaType不一致的均需要@Result转换
// @Results({
// @Result(property = "hobby", column = "hobby", javaType = HobbyEnum.class),
// @Result(property = "nickName", column = "nick_name"),
// @Result(property = "groupId", column = "group_Id")
// })
"select * from user_info where nick_name = #{nickName}") (
UserInfo findByNickName(@Param("nickName") String nickName);
"select * from user_info") (
List<UserInfo> findAll();
"insert into user_info(nick_name, group_id, hobby) values(#{nickName}, #{groupId}, #{hobby})") (
void insert(UserInfo userInfo);
"update user_info set nick_name = #{nickName}, hobby = #{hobby} where id = #{id}") (
void update(UserInfo userInfo);
"delete from user_info where id = #{id}") (
void delete(Long id);
}分页
1
2
3
4
5
6
7
8
9
10
11
12// 分页查询:http://localhost:9526/api/users
"/users") (value =
public PageInfo showAllUser(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "5") Integer pageSize) {
PageHelper.startPage(pageNum, pageSize); // 默认查询第一页,显示5条数据(必须在实例化PageInfo之前)
// ... // 此处如果发起了其他sql则此处的sql会被分页,而下面的sql则不会被分页
List<UserInfo> users = userMapper.findAll(); // 第一条执行的SQL语句会被分页,实际上输出users是page对象
PageInfo<UserInfo> pageUser = new PageInfo<UserInfo>(users); // 将users对象绑定到pageInfo
return pageUser;
}分页查询结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37{
pageNum: 1,
pageSize: 5,
size: 2,
startRow: 1,
endRow: 2,
total: 2,
pages: 1,
list: [
{
id: 1,
groupId: 1,
nickName: "smalle",
hobby: "GAME"
},
{
id: 2,
groupId: 1,
nickName: "aezo",
hobby: "CODE"
}
],
prePage: 0,
nextPage: 0,
isFirstPage: true,
isLastPage: true,
hasPreviousPage: false,
hasNextPage: false,
navigatePages: 8,
navigatepageNums: [
1
],
navigateFirstPage: 1,
navigateLastPage: 1,
firstPage: 1,
lastPage: 1
}
测试
1
2
3
4
5
6
7
8
9
10
public void testFindByNickName() {
UserInfo userInfo = userMapper.findByNickName("smalle");
System.out.println("userInfo = " + userInfo);
}
public void testInsert() throws Exception {
userMapper.insert(new UserInfo("test", 1L, HobbyEnum.READ));
}
基于注解的sql示例(用于简单的查询) ^2
用script标签包围,然后像xml语法一样书写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49// Dao层,mybatis会基于此注解完成对应的实现
// 可以理解为查询sql语句返回的是一个List<Map<String, Object>>(List里面必须为Map或其子类)。如果用Map<String, Object>接受返回值则默认取第一条数据
// 使用双引号重命名字段解决返回数据字段全变大写
"<script>", ({
"select h.help_id as \"help_id\", h.apply_money, h.create_time, h.creator, h.description, h.is_comfort, h.is_valid, h.title, h.update_time, h.updater ",
" , e.name",
" from th_help as h ",
" left join th_event e on e.event_id = h.event_id",
" left join th_group g on g.group_id = h.group_id ",
" where 1=1 ",
// 此时可以使用<if>或<when>
" <if test='plans != null and plans.size() > 0'>", // 其中大于号也可以使用`>`来表示
" and g.plan_id in ",
// in 的使用。item为当前元素,index为下标变量
" <foreach item='plan' index='index' collection='plans' open='(' separator=',' close=')'>",
" #{plan.planId}",
" </foreach>",
" </if>",
// like 的使用。此处必须使用concat进行字符串连接. oracle则需要使用 h.title like concat(concat('%',#{roleName}),'%')
" <if test='help.title != null and help.title != \"\"'> AND h.title like concat('%', #{help.title}, '%')</if>",
" <if test='event.name != null'> AND e.name = #{event.name}", "</if>",
// or 的使用(1 != 1 or .. or ..)
" <if test='help.title != null and help.desc != null or help.start != null and help.end != null'> and (",
" <if test='help.title != null and help.desc != null>",
" help.title = #{help.title}",
" </if>",
" <if test='help.start != null and help.end != null>",
" or help.start = #{help.start} and help.end = #{help.end}",
" </if>",
" ) </if>",
"</script>" })
List<Map<String, Object>> findHelps("help") Help help, ("event") Event event, ("plans") List<Plan> plans); (
// 还是使用上面script,如果提供对应的HelpPojo对象,mybatis会自动将字段的下划线转成驼峰,并去寻找HelpPojo相应的属性
// List<HelpPojo> findHelps(@Param("help") Help help, @Param("event") Event event, @Param("plans") List<Plan> plans);
// 此方法也可以再xml中实现(即部分可以通过 @Select 声明,部分可以在xml中实现)
List<HelpPojo> findHelps(@Param("help") HelpPojo helpPojo); // 此时xml中必须通过help对象获取属性.如果不写@Param("help")则可直接获取属性值
// 配合分页插件使用
public Object findHelps(Help help, Event event,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
// PageHelper要在目标查询的最近开启. 如果此处在查询一下其他数据则容易出现分页无效的情形
List users = helpMapper.findHelps(help, event);
PageInfo pageUser = new PageInfo(users);
return pageUser;
}用Provider去实现SQL拼接(适用于复杂sql)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class OrderProvider {
private final String TBL_ORDER = "tbl_order";
public String queryOrderByParam(OrderPara param) {
SQL sql = new SQL().SELECT("*").FROM(TBL_ORDER);
String room = param.getRoom();
if (StringUtils.hasText(room)) {
sql.WHERE("room LIKE #{room}");
}
Date myDate = param.getMyDate();
if (myDate != null) {
sql.WHERE("mydate LIKE #{mydate}");
}
return sql.toString();
}
}
public interface OrderDAO {
"queryOrderByParam") (type = OrderProvider.class, method =
List<Order> queryOrderByParam(OrderParam param);
}
xml版本(适合复杂操作)
xml版本(适合复杂操作)
Dao层:UserMapperXml.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public interface UserMapperXml {
List<UserInfo> findAll();
UserInfo getOne(Long id);
// 如果此处加了@Param别名,则xml中不能直接使用对象属性,而要使用别名.属性
int insert(UserInfo user); // 成功返回1
int update(UserInfo user); // jdbc url参数中需要加 &useAffectedRows=true
int delete(Long id);
// mybatis-plus:带有分页查询的mapper
// 再传入Map参数,如果直接通过 #{param.xxx} 取值会报错,需要加 @Param 注解。https://github.com/baomidou/mybatis-plus/issues/894
// 如果为其他引用类,则可直接使用参数名
IPage<Map> queryByProjectId(Page page, @Param("param") Map<String,Object> param);
}Dao实现(映射文件): UserMapper.xml(放在resources/mapper目录下)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238"1.0" encoding="UTF-8" xml version=
<!--http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html#-->
<!--sql映射文件:
namespace必须为实现接口名;每个sql是一个statement
使用include关键字调用其他xml文件的sql时,则需要在refid前加上该文件的命名空间
-->
<mapper namespace="cn.aezo.springboot.mybatis.mapperxml.UserMapperXml">
<!--
1.resultMap结果集映射定义(用来描述如何从数据库结果集中来加载对象).
2.resultType 与 resultMap 不能并用.
3.type也可以为java.util.HashMap,则返回结果中放的是Map
4.子标签有先后顺序。(constructor?,id*,result*,association*,collection*, discriminator?)
-->
<resultMap id="UserInfoResultMap" type="cn.aezo.springboot.mybatis.model.UserInfo">
<!--设置mybatis.configuration.map-underscore-to-camel-case=true则会自动对格式进行转换, 无需下面转换-->
<!--<result column="group_id" property="groupId" jdbcType="BIGINT"/>-->
<!--<result column="nick_name" property="nickName" jdbcType="VARCHAR"/>-->
<result column="desc" property="desc" jdbcType="BLOB"/><!-- BLOB/CLOB 类型必须转换 -->
</resultMap>
<!--sql:可被其他语句引用的可重用语句块. id:唯一的标识符,可被其它语句引用-->
<sql id="UserInfoColumns">id, group_id, nick_name, hobby</sql>
<sql id="userColumns">${alias}.id, ${alias}.username, ${alias}.password</sql><!-- alias不能通过bind在此sql内部设值 -->
<!--
id对应接口的方法名
resultType (类全称或别名, 如内置别名map) 与 resultMap(自定义数据库字段与实体字段转换关系map) 不能并用
statementType: STATEMENT(statement)、PREPARED(preparedstatement, 默认)、CALLABLE(callablestatement)
resultSetType: FORWARD_ONLY(游标向前滑动),SCROLL_SENSITIVE(滚动敏感),SCROLL_INSENSITIVE(不区分大小写的滚动)
-->
<select id="findAll" resultMap="UserInfoResultMap">
select
<!-- 如果引用在同一命名空间则可省略命名空间。但是 findAll 如果被其他命名空间引用则容易找到不 UserInfoColumns。因此建议一直加上命名空间 -->
<include refid="cn.aezo.springboot.mybatis.mapperxml.UserMapperXml.UserInfoColumns"/>,
<include refid="userColumns">
<property name="alias" value="t1"/>
</include>
from user_info
where 1=1
<!-- 注意:如误写成了 `test='name = "smalle"'` 则会把smalle赋值给name字段,可能会覆盖原始参数;常见的为 `test='name == "smalle"'` -->
<if test='name != null and name != ""'>
<!--
1.bind 相当于自定义变量。元素可以从 OGNL 表达式中创建一个变量并将其绑定到上下文;如果在foreach里面使用bind,#使用变量时值永远是最后一个,如果使用$则可动态让变量值改变(但是可能存在sql注入问题)
2.案例(还可以同foreach等一起使用)
<bind name="index" value="1+1" />
<bind name="index" value="index+2" />
select ${index} "结果index=4" from dual;
-->
<!--
_parameter 为传入的User对象。如果传入参数为Map,则为 _parameter.get('name')
_parameter 为 DynamicContext 中的属性,类似的还有 _databaseId=oracle|mysql
-->
<bind name="nameUpper" value="'%' + _parameter.getName().toUpperCase() + '%'" />
<!-- <bind name="nameUpper" value="'%' + _parameter.userInfo.get('name').toUpperCase() + '%'" /> -->
<!-- 定义了参数名 @Param("userInfo") -->
and upper(name) like #{nameUpper} <!-- 不能写成 #{nameUpper.toUpperCase()} -->
</if>
and hobby in
<!-- index默认从0开始;separator也可使用变量,但是只能使用#,不能使用$,也可以省略此属性 -->
<foreach item="item" index="index" collection="hobbyList" separator="," open="(" close=")">
#{item.hobby, jdbcType=VARCHAR}
</foreach>
<if test="birthdate != null">
<![CDATA[ and DATE_FORMAT(birthdate, '%Y/%m/%d') >= DATE_FORMAT(#{birthdate}, '%Y/%m/%d') ]]>
</if>
</select>
<update id="update">
<!-- 不使用bind直接把变量写到where里面mysql执行报错 -->
<bind name="id" value="check_item_sql.get(0).id" />
update sys_user_behavior set total = total + 1, create_time = now() where id = #{id}
</update>
<!-- property参数使用 -->
<sql id="sometable">
${prefix}Table where 1=1
<!-- 此时 #{username} 可以拿到selectMain的上下文 -->
<if test='username != null and username != ""'>
and username = #{username}
</if>
<!-- 此时 #{${field}} 可以拿到selectMain上下文中nickName的值 -->
<if test='${field} != null and ${field} != ""'>
and remark = #{${field}}
</if>
<!-- 零散片段. 此处在test语句中使用OGNL表达式`params.${item}`会报错,只能通过get方法动态获取属性值 -->
<if test="params.get(item) != null">
and ${item} = #{params.${item}}
</if>
<if test='strList != null and strList.size() > 0 and strList.contains("ban")'>
AND 'ban' = 'ban'
</if>
<bind usernameStr='"," + username + ","'>
<if test='str != null and str.contains(usernameStr)'></if>
</sql>
<!-- 只支持property参数值(不支持外部传入参数),且property的value属性值不支持EL表达式 -->
<sql id="someinclude">from <include refid="${include_target}"/></sql>
<!-- 返回 List<Map> 对象 -->
<select id="selectMain" resultType="map">
select *
<include refid="someinclude">
<property name="include_target" value="sometable"/>
<property name="prefix" value="Some"/>
<property name="field" value="nickName"/>
</include>
</select>
<!--
1.parameterType传入参数类型(可选,不填则可以通过 TypeHandler 推导出类型).
1.1 使用typeAliases进行类型别名映射后可写成resultType="userInfo"(自动扫描包mybatis.type-aliases-package, 默认该包下的类名首字母小写为别名).
1.2 传入parameterType="java.util.HashMap"(可省略),也可使用 #{myKey} 获取传入参数map中的值
2.如果返回结果使用resultType="cn.aezo.springboot.mybatis.model.UserInfo", 则nickName,groupId则为null(数据库中下划线对应实体驼峰转换失败,解决办法:设置mybatis.configuration.map-underscore-to-camel-case=true). 此处使用resultMap指明字段对应关系
-->
<select id="getOne" parameterType="java.lang.Long" resultType="userInfo">
select
<include refid="UserInfoColumns"/>
from user_info
where id = #{id}
</select>
<!-- 动态order by。此处传入orderBy参数即可,注意需要使用$(此时用#会报错) -->
<select id="findList">
select name from user_info order by ${orderBy}
</select>
<select id="collectionSplit">
<!-- foreach collection支持执行函数 -->
<foreach collection="listStr.trim().split('\n')" item="item">
<bind name="listVal" value="item.trim().split('@')" />
<!--
1.此处直接使用#{listVal[0]}、#{listVal[1]}的方式取值会导致上面的foreach无效,每次都是取的最后一条记录的值,改成foreach则不会存在此问题
2.部分情况也可以使用 ${itemVal};由于无法精确知道数据类型,像字符串需要手动加上引号,也不推荐;使用foreach的时候split可考虑使用'@@@###@@@'等字符串分割从而组装成数组
-->
<foreach collection="listVal" item="val">
#{val}
</foreach>
<!-- 支持 instanceof 判断数据类型(如是整数直接trim就回报错) -->
<bind name="itemVal" value="params.get(item)" />
<choose>
<when test="itemVal != null and itemVal instanceof String">
<bind name="itemVal" value="itemVal.trim().split('@@@###@@@')" />
<foreach collection="itemVal" item="val">
#{val}
</foreach>
</when>
<otherwise>
<!-- 此处不能写成 #{itemVal} 否则如何后面有值为null的时候就会把前面的字段全部覆盖成null -->
#{params.${item}}
</otherwise>
</choose>
<!-- 支持调用静态方法或常量(有说还可以直接调用bean名称的,如: myBean@test(itemVal)),取值时只能用$,可考虑bind一下再使用# -->
<if test='@cn.hutool.core.util.StrUtil@trim(itemVal) != ""'>
'${@cn.hutool.core.util.StrUtil@reverse(itemVal)}'
</if>
</foreach>
</select>
<!-- insert/update返回主键(默认返回修改的数据执行状态/影响行数)
1.定义方式
方式一:基于JDBC(Mysql/SqlServer都适用,Oracle不适用)
keyProperty(主键对应Model的属性名)和useGeneratedKeys(是否使用JDBC来获取内部自增主键,默认false)联合使用返回自增的主键(可用于insert和update语句)。
方式二:基于方言,每个数据库提供的内部函数(order表示执行selectKey和insert的先后顺序,默认AFTER)
1.Mysql: <selectKey keyProperty="id" resultType="long">select LAST_INSERT_ID()</selectKey>
2.SqlServer: <selectKey resultType="java.lang.Long" order="AFTER" keyProperty="id">SELECT IDENT_CURRENT('my_table')</selectKey>
3.Oracle: <selectKey keyProperty="id" order="BEFORE" resultType="java.lang.Long">select SEQ_MY_TABLE.nextval as id from dual</selectKey> 需要先创建好序列SEQ_MY_TABLE
2.获取方式:userMapper.insert(userInfo); userInfo.getUserId();
-->
<insert id="insert" parameterType="cn.aezo.springboot.mybatis.model.UserInfo" keyProperty="userId" useGeneratedKeys="true">
<!--
<selectKey keyProperty="id" order="BEFORE" resultType="java.lang.Long">select SEQ_MY_TABLE.nextval as id from dual</selectKey>
-->
insert into user_info
<!-- trim 可以在自己包含的内容前加上某些前缀,也可以在其后加上某些后缀 -->
<trim prefix="(" suffix=")">
nick_name, group_id, hobby
</trim>
values (#{nickName}, #{groupId}, #{hobby})
</insert>
<!--
Mybatis一次执行多条sql,一个标签多个Insert、update、delete操作
还需要在数据库连接配置中加入 allowMultiQueries=true
-->
<insert id="addPurchase" parameterType="com.zhao.vo.PurchaseVoOne">
insert into t_goods (goods_name) values (#{goodsName});
insert into t_supplier (supplier_name) values (#{supplierName});
</insert>
<update id="update" parameterType="cn.aezo.springboot.mybatis.model.UserInfo">
update user_info set
<!--动态sql, 标签:if、choose (when, otherwise)、trim (where, set)、foreach-->
<if test="nickName != null">nick_name = #{nickName},</if>
hobby = #{hobby}
where id = #{id}
</update>
<update id="update" parameterType="cn.aezo.springboot.mybatis.model.UserInfo">
update user_info
<!-- set 在更新操作的时候,在包含的语句前输出一个set。注意后面的单引号 -->
<set>
<if test="nickName != null">nick_name = #{nickName},</if>
<if test="hobby != null">hobby = #{hobby},</if>
</set>
where id = #{id}
</update>
<delete id="delete" parameterType="java.lang.Long">
delete from user_info where id = #{id}
</delete>
<!-- 此时也可执行成功,但是如果传入参数为空,容易报错(java.sql.SQLException: 无效的列类型: 1111) -->
<delete id="delete2">
delete from user_info where id = #{id}
</delete>
<!-- if else写法,和Boolean类型判断 -->
<choose>
<when test="boolField">
</when>
<otherwise>
</otherwise>
</choose>
<!-- null 认为是 false -->
<if test='boolField'></if>
<if test='!boolField'></if>
<if test='not boolField'></if>
<if test='boolField != null'></if><!-- 存在问题:true/false都满足 -->
<!-- 执行DDL语句可使用update标签 -->
<update id="changeTriggerStatus">
ALTER TRIGGER T_TEST ENABLE
</update>
</mapper>
xml联表查询举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47<select id="getClass" parameterType="int" resultMap="ClassResultMap">
select * from class c, teacher t, student s
where c.teacher_id = t.t_id and c.c_id = s.class_id
and c.c_id = #{id} and s.name = #{name}
</select>
<!--此处Classes类中仍然需要保存一个Teacher teacher的引用和一个List<Student> students的引用-->
<resultMap type="cn.aezo.demo.Classes" id="ClassResultMap">
<!--一个 ID 结果;标记结果作为 ID 可以帮助提高整体效能。association、collection中都最好加上 -->
<id property="id" column="c_id"/>
<!--注入到字段或 JavaBean 属性的普通结果-->
<result property="name" column="c_name"/>
<!--
association字面意思关联,这里只专门做一对一关联;
1.property表示是cn.aezo.demo.Classes中的属性(setter)名称;
2.javaType表示该属性是什么类型对象
3.columnPrefix="out_/in_" 字段前缀。如查询主表(Ycross_Storage)中关联某一张表(如Ycross_In_Out_Regist)关联了两次,但是表Ycross_In_Out_Regist的映射只有一个(property和column的对应关系只有一套)。可以再取出Ycross_In_Out_Regist中的字段的时候通过`as out_xxx`对某字段进行别名处理。此时映射的时候会将字段的名称去掉columnPrefix前缀去找对应的property
-->
<association
property="teacher"
javaType="cn.aezo.demo.Teacher">
<id property="id" column="t_id"/>
<result property="name" column="t_name"/>
</association>
<!--
1.ofType指定students集合中的对象类型。这样查询出来的集合条数和数据出来的一致(子表导致主表查询的条数增多)
2.javaType="ArrayList"可以省略
-->
<collection property="students" ofType="cn.aezo.demo.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
</collection>
<!--
此时返回的集合是主表的条数,然后基于每一条再重新查询数据获取子表数据并放入到Classes对象的students中。
1.select指查询Student的接口. 如果为当前mapper文件则可省略命名空间(namespace)直接写成 getStudent。(select和column只有在嵌套查询的时候才用得到)
2.column是传入到getStudent查询中的参数,id是传入参数名称,s_id获取获取字段值的字段名(就是先从主表查询的结果中获取s_id字段的值,传入到id中,发起getStudent子查询)。如果一个参数也可以直接写成column="s_id" (getStudent的接口中也声明接受一个此类型的参数即可)
3.columnPrefix="xx_"同上
4.会产生1+N问题。主表有多少此就会发起多少次查询,无法根据条件判断是否需要发起子查询。导出报表最好不要使用
-->
<collection
property="students"
ofType="cn.aezo.demo.Student"
column="{id = s_id, name = s_name}"
select="cn.aezo.demo.Student.getStudent">
</collection>
</resultMap>@MapKey使用
1
2
3
4
5
6
7
8
9
10
11// 如果name唯一则正常;如果同一name可能存在多个,则不会报错,该name对应的值为最后一条记录
"NAME") // 由于返回类型是Map,此处为Oracle的情况下返回字段类型为大写,所有为NAME;如果返回对象为实体则可以为name (
Map<String, Map<String, Object>> findUserMap();
// 正常返回,和没有@MapKey一样
"NAME") (
List<Map<String, Object>> findUserMap();
// 假设name不唯一,原意图是想返回所有name的值放到List中,但实际返回的对象类型为Map<String, Map<String, Object>>,相同name只会取最后一个
"NAME") (
Map<String, List<Map<String, Object>>> findUserMap();xml
1
2
3<select id="findUserMap" resultType="java.util.Map">
select u.id, u.name from user u
</select>
mybatis常见问题
#
和$
区别#
创建的是一个prepared statement语句,$
符创建的是一个inlined statement语句#{}
是实现的是PrepareStatement,${}
实现的是普通Statement。使用$
时,如字符串值就需要手动增加单引号,如果需要实现动态字段,则需要使用$
;#
则会自动给字符串值增加单引号- 字段使用变量代替时需要使用
$
;foreach.separator 参数如需使用变量,需用#
- 关于
<
、>
转义字符(在annotation中需要转义,在xml的sql语句中则不需要转义)<
转成<
,>=
转成>=
等- 使用CDATA
<![CDATA[ when min(starttime) <= '12:00' and max(endtime) <= '12:00' ]]>
- 双引号转义:
<if test='help.title != null and type = \"MY_TYPE\"'>
- select等xml语句后不要加
;
mybatis类型转换问题
mybatis会对Integer转换成字符串时,如果Integer类型值为0,则转换为空字符串。(js也是这样转换的)
1
2
3<!-- 此时Integer status = 0;时,下列语句返回false. 所有Integer类型的不要加status != '' -->
<!-- 前台尽量不要传递 '0' 的字符串,可能导致生成的sql为 status = '0',则存在隐式转换 -->
<if test="status != null and status != ''">and status = #{status}</if>多个字符则认为是字符串,单个字符则认为是Character字符.(如mybatis认为:test=”validStatus == ‘Y’”中的Y是字符,test=”validStatus == ‘YY’”中的YY则是字符串)
1
2
3
4
5<!-- 错误写法。传入参数validStatus='Y', 此时会报错NumberFormatException;mybatis认为传入参数是字符串对象Y,比较值是字符'Y',经过几个判断都不相等,再转成数值时则报错了 -->
<if test="validStatus == 'Y'">and validStatus = 1</if>
<!-- 正确写法 -->
<if test='validStatus == "Y"'>and validStatus = 1</if>
<if test="validStatus == 'Y'.toString()">and validStatus = 1</if>查询结果返回 1 和 0 的数值时可自动注入到
private Boolean hasChildren;
的属性中
处理数组
- dao中可以使用
submitTm[0]
获取值; xml中不行,其处理数组(如时间段)的方式如下 xml参数中可使用
${list.size}
获取长度,不能使用#(否则报错UnsupportOperationException)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<!-- (1)xml方式 -->
<!-- <if test='dataSourceList != null and dataSourceList.size() >= 1'> -->
<!-- (2)@Select方式 -->
<if test='submitTm != null and submitTm.length >= 1'>
<foreach collection="submitTm" index="i" item="item">
<if test='i == 0 and item != null'>and v.submit_tm >= #{item}</if>
<if test='i == 1 and item != null'>and v.submit_tm <= #{item}</if>
</foreach>
</if>
<!-- 或者 -->
<if test='planTmSection != null and planTmSection.size() > 0
and planTmSection.get(0) != null and planTmSection.get(0) != ""
and planTmSection.get(1) != null and planTmSection.get(1) != ""'>
and date_format(wl.plan_tm,'%Y-%m-%d') between
<foreach item="item" collection="planTmSection" separator="and">
#{item}
</foreach>
</if>
- dao中可以使用
日期说明
- mysql当前时间获取
now()
,oracle为sysdate - mysql数据库日期型可和前台时间字符串进行比较
- 当传入Date类型的日期参数时,虽然打印出来的sql仍然是 input_tm > ‘2000-01-01 00:00:00.0’,但是根据传入参数类型可知
2000-01-01 00:00:00.0(Timestamp)
此时会基于时间进行比较 - 如果是字符串参数则为
2000-01-01 00:00:00(String)
,且需要将字段日期值转成字符串
- mysql当前时间获取
- 数据库字段类型根据mybatis映射转换,
count(*)
转换成Long
<when>
/<if>
可进行嵌套使用,其子属性test可以使用双引号或单引号- 支持
choose (when, otherwise)
语句 - xml文件修改无需重新部署,立即生效?
Cause: java.sql.SQLException: 无法转换为内部表示
可能是由于类型转换导致,如强制将数据库中字符串类型字段映射某个对象的Long类型属性上- mybatis中用Map接收oracle的结果集,返回的数据key为大写。解决:sql可写成
select name as "name" from user
- xml中传入常量
- 格式
${@path$subClass@Attr.getValueMethod()}
,ognl表达式参考 - 如
AND type = ${@cn.aezo.test.Const@Type}
或者AND type = ${@cn.aezo.test.Const$TypeEnum@Test.getValue()}
(枚举)
- 格式
- 注入全局变量/动态解析全局变量: https://blog.csdn.net/mashangzhifu/article/details/122845644
- 参考下文拦截器(插件))
主键问题
- 控制主键自增
- mybatis使用
- mysql使用useGeneratedKeys属性
- oracle使用selectKey标签
- mybatis-plus使用
- mysql基于数据自增
@TableId(type = IdType.AUTO)
- oracle基于序列,
@KeySequence
结合内置自增策略
- mysql基于数据自增
- mybatis-plus时,主键如何兼容mysql和oracle生成策略
- 使用 id 雪花生成算法(长度为19位,因此为了方便之后兼容,不建议将主键设置成Integer)
- 基于spring 提供 DataFieldMaxValueIncrementer 接口实现主键生成: https://blog.csdn.net/huang007guo/article/details/104641660
- mybatis使用
- 获取自增主键(mysql为例,需要数据库设置主键自增) ^3
- 方式一:keyProperty(主键对应Model的属性名)和useGeneratedKeys(是否使用JDBC来获取内部自增主键,默认false)联合使用返回自增的主键(可用于insert和update语句)
- 方式二:
<selectKey keyProperty="id" resultType="long">select LAST_INSERT_ID()</selectKey>
- 获取方式:
userMapper.insert(userInfo); userInfo.getUserId();
批量执行语句
- 性能比较,同个表插入一万条数据时间近似值
- JDBC BATCH 1.1秒左右 > Mybatis BATCH 2.2秒左右 > Mybatis foreach 4.5秒左右
- 有测试说 Mybatis foreach > Mybatis BATCH
- jdbc batch
- 参考java-base.md#JDBC
- 采用PreparedStatement.addBatch()方式实现
- Mysql需要在jdbc连接url上追加
rewriteBatchedStatements=true
,否则不起作用;Oracle无需
- jdbcTemplate.batchUpdate
mybatis foreach
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<!--
mysql版本试用
1.mysql默认接受sql的大小是1048576(1M),即第三种方式若数据量超过1M会报`com.mysql.jdbc.PacketTooBigException: Packet for query is too large`异常:(可通过调整MySQL安装目录下的my.ini文件中[mysqld]段的"max_allowed_packet = 1M")
2.如果报错,需要在jdbc url上增加 allowMultiQueries=true
-->
<insert id="insertbatch">
insert into t_user(id, name) values
<foreach collection="list" item="user" separator=",">
(#{user.id}, #{user.name})
</foreach >
</insert>
<!-- **oracle版本适用**,或者使用 INSERT ALL INTO, 参考:https://www.cnblogs.com/nemowang1996/p/12519018.html -->
<!-- 序列不能直接和select union一起使用,如果把seq_user.nextval写到foreach循环里面会报错 -->
<insert id="insertbatch">
insert into t_user(id, name) (
select seq_user.nextval, a.name, a.sex from (
<foreach collection="list" item="user" separator ="UNION ALL">
select #{user.name} name, #{user.sex} sex from dual
</foreach >
) a
)
</insert>mybatis batch
- Mybatis内置的ExecutorType有3种,默认的是simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而batch模式重复使用已经预处理的语句,并且批量执行所有更新语句
- Mysql使用batch模式需要在jdbc连接url上追加
rewriteBatchedStatements=true
,否则不起作用;Oracle无需 案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
SqlSessionTemplate sqlSessionTemplate; // 或者注入SqlSessionFactory
public void testInsertBatch2() {
User user;
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
try {
UserDao mapper = sqlSession.getMapper(UserDao.class);
int count = 0;
int total = 500;
for (int i = 0; i < total; i++) {
user = new User();
user.setId(i+1);
user.setName("name" + i);
mapper.insert(user);
count++;
if (count % 100 == 0 || count == total) {
sqlSession.commit();
sqlSession.clearCache();
count = 0;
}
}
} catch (Exception e) {
e.printStackTrace();
sqlSession.rollback();
} finally {
sqlSession.close();
}
}
mybatis-plus 服务中的saveBatch(新增)/updateBatchById(更新)/removeByIds(删除)/saveOrUpdateBatch(新增或基于ID修改)访问,见下文。基于mybatis的
openSession(ExecutorType.BATCH)
,默认每1000条提交一次
Mybatis缓存
- 聊聊MyBatis缓存机制
- 一级缓存
- 配置
<setting name="localCacheScope" value="SESSION"/>
- 共有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话(同一个sqlSession对象)中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效(相当于关闭一级缓存)
- 现象
- 同一个sqlSession对象,执行多次查询,只有第一次会查询数据库,后续则返回缓存数据(不考虑缓存过期的情况)
- 在修改操作后执行的相同查询,一级缓存失效(如果只修改bean对象,没有提交sql则缓存仍然存在,返回的是被修改的bean)
- 一级缓存只在数据库会话内部共享。如果会话1先读取一次数据,然后会话2修改了该数据(只会让会话2的查询缓存失效),会话1重新读取此数据获取的任然是修改前数据
- 总结
- MyBatis一级缓存的生命周期和SqlSession一致
- MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺
- MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession(简单的Spring项目一般只有一个此对象)或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement
- 配置
- 二级缓存
- mybatis-plus默认开启了二级缓存,关闭可设置
mybatis-plus.configuration.cacheEnabled=false
- 配置
- 在MyBatis的配置文件中开启二级缓存
<setting name="cacheEnabled" value="true"/>
- 然后在MyBatis的映射XML中配置
<cache />
或者<cache-ref namespace="mapper.DemoMapper"/>
;其中cache标签支持属性- type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
- eviction: 定义回收的策略,常见的有FIFO,LRU。
- flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
- size: 最多缓存对象的个数。
- readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
- blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存
- 在MyBatis的配置文件中开启二级缓存
- 现象
- 当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库
- 执行更新等语句后,缓存失效
- 总结
- MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强
- MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻(可使用cache-ref让两个namespace使用同一个缓存)
- 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高
- mybatis-plus默认开启了二级缓存,关闭可设置
基于databaseId实现数据库兼容
- mybatis
- mybatis-config.xml增加上文所述配置
- mybatis-plus
1 |
|
- 使用(会根据数据源自动判断使用那种类型的sql)
1 | <select id="getNextSeqDemo" resultType="java.lang.Object" databaseId="mysql"> |
拦截器(插件)
- mybatis 拦截器 Interceptor 可拦截的目标对象有四个(前面是可被拦截的对象,后面括号中是对象中可被拦截的方法)
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- 拦截器执行顺序
- 不同拦截对象执行顺序,如下:
Executor
->StatementHandler
->ParameterHandler
-> `ResultSetHandler - 拦截相同对象执行顺序,取决于 mybatis-config.xml 中 plugin 配置顺序,越靠后,优先级越高
- 不同拦截对象执行顺序,如下:
- mybatis 拦截器使用
- 实现
Interceptor
接口;@Intercepts
注解用于配置需要拦截的对象和方法 - 配置mybatis拦截器: 基于xml或者注解成Bean
- 实现
- mybatis-plus 拦截器
- v3.4.0及以上: 其
InnerInterceptor
机制是基于 mybatis 的Interceptor
实现的,具体参考 mybatis-plus 类MybatisPlusInterceptor
(v3.4.0) - v3.4.0以下同 mybatis 拦截器使用,只需注册配置成Bean即可
- v3.4.0及以上: 其
- 案例
- Mybatis拦截器失效(引入了分页插件导致),参考下文案例:https://blog.csdn.net/weixin_44032384/article/details/116381837
- 结合 ThreadLocal 参考:https://blog.csdn.net/iteye_19045/article/details/100024506
- 基于查询:https://juejin.cn/post/6965441270277734430
- 基于修改:https://www.zhihu.com/question/375124631/answer/2357163072
- 主要代码
1 | // 可只拦截某个方法类型 |
- 注入全局动态参数
1 | // 插件逻辑(全局动态参数) |
数据类型对应关系
- JDBC数据类型转换
JDBC | Java(Mybatis) | Mysql | Oracle |
---|---|---|---|
Boolean | Boolean | bit | |
Integer | Integer | tinyint | |
Integer | Integer | smallint | |
Integer | Integer | mediumint | |
Integer | Integer | int | Number(11) |
Bigint | Long | bigint | Number(20) |
Numeric | Long | Number | |
Timestamp | Date | datetime | Date |
Date | Date | date | Date |
Decimal | BigDecimal | decimal | Number(12, 3) |
Char | char | Char | |
Blob | byte[]/Object | blob | Blob |
Clob | String | text | Clob |
- BLOB为二进制大对象,可存储图片(转成二进制,实际用的很少);CLOB文本大对象,可用来存储博客文章等;Mybatis对CLOB可直接解析成字符串,而BLOB则需要些对应关系
MyBatis Generator
- 使用
MyBatis Generator
自动生成model/dao/mapper - 官方文档:http://www.mybatis.org/generator/index.html
生成方式有多种(此处介绍maven插件的方式)
maven配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<build>
<plugins>
<!-- mybatis(mapper等)自动生成 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<!--maven可执行命令-->
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>resources目录添加文件:
generatorConfig.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90"1.0" encoding="UTF-8" xml version=
<generatorConfiguration>
<!--数据库驱动 -->
<!-- <classPathEntry location="C:\soft\oracle\product\11.2.0\dbhome_1\jdbc\lib\ojdbc6.jar"/> -->
<classPathEntry location="C:\Users\smalle\.m2\repository\mysql\mysql-connector-java\5.1.43\mysql-connector-java-5.1.43.jar" />
<context id="MySQLTables" targetRuntime="MyBatis3" defaultModelType="flat">
<plugin type="cn.aezo.mybatis.generator.plugins.MyPluginTableRename"/> <!--自定义插件,可继承 PluginAdapter-->
<!-- 为了防止生成的代码中有很多注释,比较难看 -->
<!-- 可自定义方法/字段注释,通过实现 CommentGenerator。参考 https://www.cnblogs.com/NieXiaoHui/p/6094144.html -->
<commentGenerator>
<property name="suppressDate" value="true" />
<property name="suppressAllComments" value="true" />
</commentGenerator>
<!--数据库链接地址账号密码 -->
<!-- driverClass="oracle.jdbc.driver.OracleDriver" connectionURL="jdbc:oracle:thin:@//localhost:1521/orcl" -->
<jdbcConnection
driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/springboot"
userId="root"
password="root">
</jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>
<!--生成Model类存放位置:targetPackage为model对应的包名;targetProject为项目根目录,此处相对当前项目,还可写成D:/mydemo/src/main/java -->
<javaModelGenerator
targetPackage="cn.aezo.springboot.mybatis.generator.model"
targetProject="src/main/java">
<!-- 是否对类CHAR类型的列的数据进行trim操作 -->
<property name="trimStrings" value="true" />
<!-- 开启enableSubPackages后,会基于targetPackage目录添加数据库表的Catalog和Schema配置(table标签属性) -->
<!-- <property name="enableSubPackages" value="true" /> -->
</javaModelGenerator>
<!--生成映射文件存放位置,会存放在src/main/resources/mapper目录下(自动创建mapper目录) -->
<sqlMapGenerator
targetPackage="mapper"
targetProject="src/main/resources">
<!-- <property name="enableSubPackages" value="true" /> -->
</sqlMapGenerator>
<!--生成Dao类存放位置 -->
<javaClientGenerator
type="XMLMAPPER"
targetPackage="cn.aezo.springboot.mybatis.generator.dao"
targetProject="src/main/java">
<!-- <property name="enableSubPackages" value="true" /> -->
</javaClientGenerator>
<!-- 读取所有的table标签,有几个table标签就解析几个 -->
<!-- %标识根据表名生成,tableName="t_%"表示只生成t_开头的表名 -->
<table tableName="%">
<!--
1、生成selectKey语句,为mybatis生成获取自增主键值语句(必须数据库字段设置成自增)
2、column表的字段名(不支持通配符,因此为了方便可将所有表的主键名设置为id)
3、sqlStatement="MySql/DB2/SqlServer等"
3-1、SqlServer会生成`SELECT SCOPE_IDENTITY()`,测试无法返回主键,可使用JDBC解决
4、identity:true表示column代表的是主键,会在插入记录之后获取自增值替换对应model的id值(自增需要由数据库提供),实际的insert语句将不含有主键字段; false表示非主键,会在插入记录获取自增值并替换model的id(如从序列中获取), 此时insert语句含有主键字段
5、最终生成的语句如
<selectKey keyProperty="userId" order="AFTER" resultType="java.lang.Long">
SELECT LAST_INSERT_ID()
</selectKey>
-->
<generatedKey column="id" sqlStatement="MySql" identity="true" />
<!--
sqlStatement="JDBC"会生成如下代码来获取主键,适用于MySql/SqlServer
<insert id="insert" parameterType="cn.aezo.demo.MyTable" keyColumn="id" keyProperty="id" useGeneratedKeys="true">
-->
<!-- <generatedKey column="id" sqlStatement="JDBC" identity="true" /> -->
</table>
<!-- 去掉表前缀:生成之后的文件名字User.java等。enableCountByExample标识是否使用Example -->
<!-- 如oracle此时的schema相当于用户名,如果不定义则会获取到多个t_user表,但是只会基于其中某个一个生成代码. 但是此时生成的mapper中表名带有前缀`smalle.` -->
<table schema="smalle" tableName="t_user" domainObjectName="User"
enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false">
<!-- 去掉字段前缀 `t_` -->
<property name="useActualColumnNames" value="false"/>
<columnRenamingRule searchString="^t_" replaceString=""/>
</table>
</context>
</generatorConfiguration>MyPluginTableRename 自定义插件。配置文件的table节点在自定义了schema后,生成的mapper表名会包含schema信息,此插件主要是为了去除此schema信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import org.mybatis.generator.api.PluginAdapter;
// ...
// 插件的生命周期:http://www.mybatis.org/generator/reference/pluggingIn.html
// 官方内置的插件:http://www.mybatis.org/generator/reference/plugins.html
public class MyPluginTableRename extends PluginAdapter {
// 验证此插件是否可以开启
public boolean validate(List<String> warnings) {
return true;
}
// 循环每个table时,执行对应初始化
public void initialized(IntrospectedTable introspectedTable) {
String schema = introspectedTable.getTableConfiguration().getSchema();
if(schema != null && !"".equals(schema)) {
String tableName = introspectedTable.getTableConfiguration().getTableName();
introspectedTable.setSqlMapFullyQualifiedRuntimeTableName(tableName);
introspectedTable.setSqlMapAliasedFullyQualifiedRuntimeTableName(tableName);
}
}
}进入到pom.xml目录,cmd执行命令生成文件:
mvn mybatis-generator:generate
生成Mapper中Example的使用:http://www.mybatis.org/generator/generatedobjects/exampleClassUsage.html
1
2
3
4
5
6
7
8
9
10UserExample userExample = new UserExample();
userExample.createCriteria().andUsernameEqualTo("smalle")
.andSexEqualTo(1);
userExample.setOrderByClause("username asc");
List<User> users = userMapper.selectByExample(userExample); // 未查询到数据时返回一个大小为0的数组
// generator生成的mapper
userMapper.insert(user); // 完全基于user创建(新增记录时不考虑数据库的默认值)
userMapper.insertSelective(user); // 不为 null 才会拼接此字段(新增记录时考虑数据库的默认值)
通过java代码调用mybatis-generator生成
引入依赖
1
2
3
4
5<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>关键代码
1
2
3
4
5
6
7
8
9
10
11
12List<String> warnings = new ArrayList<>();
boolean overwrite = true;
File configFile = new File("generatorConfig.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
try {
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
} catch (Exception e) {
e.printStackTrace();
}
使用oracle数据库
- 配置文件设置成
<generatedKey column="id" sqlStatement="MySql" identity="false" />
中identity=”false”为了生成id的insert语句(oracle需要通过序列来完成) - 修改生成的
<selectKey>
语句为根据序列获取主键 - 将oracle表转成mysql数据表,可参考《mysql-dba.md》
- 配置文件设置成
- plugin插件使用
- 内部通过PluginAggregator来保证plugin的生命周期
- 使用参考上述示例
MyPluginTableRename
源码解析
mybatis-plus
- mybatis-plus、github
- 返回结果集类型为List,当没有数据时返回大小为0的List,而不是null
springboot依赖及配置
1 | <!-- 包含 mybatis、mybatis-plus、generator --> |
- 配置类增加
@MapperScan({"cn.aezo.**.mapper"})
扫描Mapper(Java类)- 可能还需配置
mapper-locations
指定xml文件位置(默认值为classpath/mapper下的xml文件,如果在其子目录或其他目录则需配置)
- 可能还需配置
- application.yaml配置(可省略)
1 | # mybatis-plus 配置 |
使用
- 注解说明
- 如果Model主键名称不为
id
,则需要在对应主键字段上注解@TableId
- 如果Model名称不为表名,则可通过
@TableName
进行注解真实名称 - 如果Model的字段不为表字段,可通过
@TableField(exist = false)
注解
- 如果Model主键名称不为
- API使用
- 此处的
SubscribeService
继承了ServiceImpl
,对应接口继承了IService
- 使用mapper,如
SubscribeMapper
则会继承BaseMapper
- 此处的
1 | // ======== 基于 Mapper 进行访问 |
分页
1 | // 1.启用分页插件 |
- 取前100条
1 | // mysql |
Oracle序列
1 | // Entity增加注解 |
NULL空值处理
- mybatis-plus默认策略为
NOT_NULL
:在执行updateById(user),如果user对象的属性为NULL,则不会更新(空字符串会更新);也可以修改策略 - 如果需要进行NULL更新,则可通过设置字段策略或者通过UpdateWrapper进行(推荐)
1 | // https://baomidou.com/guide/faq.html#%E6%8F%92%E5%85%A5%E6%88%96%E6%9B%B4%E6%96%B0%E7%9A%84%E5%AD%97%E6%AE%B5%E6%9C%89-%E7%A9%BA%E5%AD%97%E7%AC%A6%E4%B8%B2-%E6%88%96%E8%80%85-null |
乐观锁插件
1 | // 1.启用乐观锁插件 |
代码生成
1 | GlobalConfig gc = new GlobalConfig(); |
常见问题
entity继承注意项
1
2
3public class Foo extend Bar {}
// 当执行下列语句时,生成的sql会包含 Bar 的字段;因此Bar中的字段必须是foo表中的字段,否则执行报错
fooMappler.selectOne(id);数据库兼容
- https://www.i4k.xyz/article/woyyazj/105111463
- 自定义sql语句兼容: 基于 databaseId (mybatis功能): https://blog.csdn.net/zhaizhisheng/article/details/105834300
- 主键如何兼容mysql和oracle生成策略: 参考上文主键问题
- 建议主键为字符串雪花ID
- 字段为关键字的处理
- 通过拦截器动态修改生成sql中字段名
- 建议字段名不能为关键字
- Mybatis-Plus整合多数据源: https://juejin.cn/post/7020066406012026893
- 字段为mysql关键字处理
- 如字段value在mysql中是关键字,不能直接使用,可在字段上加注解
@TableField(value = "
value")
- 如字段value在mysql中是关键字,不能直接使用,可在字段上加注解
参考文章