MyBatis学习笔记
MyBatis是一个ORM(对象关系映射框架),它是为了实现更加简单的操作数据库的框架。底层是基于JDBC的。
ORM(Object Relational Mapping),即对象关系映射。在⾯向 对象编程语⾔中,将关系型数据库中的数据与对象建⽴起映射关系,进⽽⾃动的完成数据与对象 的互相转换:
1.将输⼊数据(即传⼊对象)+SQL 映射成原⽣ SQL
2.将结果集映射为返回对象,即输出对象 ORM 把数据库映射为对象:
数据库表(table)--> 类(class)
记录(record,⾏数据)--> 对象(object)
字段(field) --> 对象的属性(attribute)
⼀般的 ORM 框架,会将数据库模型的每张表都映射为⼀个 Java 类。 也就是说使⽤ MyBatis 可以像操作对象⼀样来操作数据库中的表,可以实现对象和数据库表之间 的转换
一、配置MyBatis开发环境
1.MyBatis框架的搭建
(1)添加MyBatis框架
添加MyBatis环境依赖
创建好后我们会发现会报错,这是正常的,因为我们还没有设置连接数据库MySQL的详细信息。
(2)设置MyBatis配置(application.properties)
确保自己的mysql里面有数据
a.数据库的相关连接信息。b.xml保存路径和命名格式
#设置数据的相关连接信息
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#设置 MyBatis XML 存放路径和命名格式
mybatis.mapper-locations=classpath:mybatis/*Mapper.xml
#配置 MyBatis 执行时打印SQL(可选配置)
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
logging.level.com.example.demo=debug
二、使用MyBatis模式和语法操作数据库
常规的写法,包含了两个文件:
1.接口:方法的声明(给其他层(Service)调用)
根据数据库表定义实体类
import lombok.Data;
import java.time.LocalDateTime;
import java.time.LocalTime;
@Data
public class UserInfo {
private int id;
private String username;
private String password;
private String photo;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int state;
}
定义数据持久层接口(mapper)
package com.example.demo.mapper;
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper //数据持久层标志
public interface UserMapper {
List<UserInfo> getAll();
}
2.XML:实现接口
查询操作演示
3.单元测试查看(在我们需要测试的类里面右击Generate->test)
4.添加Service、Controller
package com.example.demo.service;
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public List<UserInfo> getAll() {
return userMapper.getAll();
}
}
package com.example.demo.controller;
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/u")
public class UserController {
@Resource
private UserService userService;
@RequestMapping("/getall")
public List<UserInfo> getAll() {
return userService.getAll();
}
}
三、安装插件MyBatisX插件
为了方便开发MyBatis,实现XML和对应的接口之间的快速跳转,我们可以安装MyBatisX的插件。
1.MyBatis参数化查询
对于需要传参的查询,我们需要使用@Param,去动态传递,同时该参数的类型建议使用包装类,因为包装类允许有null值。
(1)根据id查询userinfo的信息
// 根据id查询对应的信息记录
UserInfo getUserById(@Param("id") Integer uid);
<!-- 根据id查找-->
<select id="getUserById" resultType="com.example.demo.model.UserInfo">
select * from userinfo where id = ${id}
</select>
@Test
void getUserById() {
UserInfo userInfo = userMapper.getUserById(3);
System.out.println(userInfo);
}
2.查询语句中使用#和$区别
重点:$和#有什么区别?
(1)${} 直接替换;#{} 预执行。
(2)${} 是不安全的,存在SQL注入;而#{} 是安全的,不存在SQL注入。
演示SQL注入
以登录(username,password)为例
当我们增加了' '
后就正确显示了
然后我们不用加' '
,换成#会发现直接就正确显示了
我们在换种写法
当我们使用$
时候会发现密码错误的仍然可以登陆进去,这就是$
存在的sql注入问题。
而使用#
的话就不会出现这种情况。
从上面的例子可以看出${}
可以实现的功能#{}
都能实现,并且${}
还存在SQL注入的问题,那么为什么${}
还存在?
为啥${}
仍然存在
比如升序(asc)降序(desc)问题
使用${}
// 为啥${}写法还存在
List<UserInfo> getAllOrder(("myorder") String myorder);
<!-- 为啥${}还存在-->
<select id="getAllOrder" resultType="com.example.demo.model.UserInfo">
select * from userinfo order by id ${myorder}
</select>
void getAllOrder() {
List<UserInfo> list = userMapper.getAllOrder("desc");
System.out.println(list);
}
使用#{}
会发现报错了
可以看到desc解析过来的话就直接是字符串了。所以这种情况的话就需要使用${}才可以。
总结:${}
${} 适用场景:当业务需要传递SQL命令时,只能适用 ${} ,不能使用#{}。
${} 注意事项:如果要使用 ${},那么传递的参数一定要能够被穷举,否则是不能使用的。
能用#{}就不用${}
3.MyBatis删除操作
对于删除操作没有resultType,它返回的就是受影响的行数。删除、添加、修改默认返回是受影响的行数。
// 删除操作
int delById(("id") Integer id);
<!-- 删除操作-->
<delete id="delById">
delete from userinfo where id = #{id}
</delete>
void delById() {
int result = userMapper.delById(3);
// 返回受影响的行数
System.out.println(result);
}
在单元测试里面执行的数据库操作,我们可以使用 @Transactional
注解,去模拟可以不用实际的删除数据库中的数据,在进行业务场景后,测试完成后,就会回滚。
4.类中的属性和数据库表中的字段名不一致时候,那么查询结果为null,解决方案如下。
(1)将类中的属性和表中的字段名保持一致(最简单的解决方案)。
(2)使用sql语句中的as进行列名(字段名)重命名,让列名(字段名)等于属性名。
(3)定义一个resultMap,将属性名和字段名进行手动映射。
5.MyBatis修改操作
指定只能修改的参数:
整个对象都可修改的:
同样的如果不想实际的修改到数据库,那么就是用@Transactional
。
@Transactional
@Test
void updById() {
String username = "王二";
int result = userMapper.updById(5,username);
System.out.println(result);
}
6.MyBatis添加操作
(1)返回受影响的行数
// 所以我们要修改的是全部的那么就是用对象作为参数传递
int insert2(UserInfo userInfo);
<insert id="insert2">
insert into userinfo(username,password,photo) values(#{username},#{password},#{photo})
</insert>
@Test
void insert2() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("铁蛋");
userInfo.setPassword("789");
userInfo.setPhoto("images/11.png");
int result = userMapper.insert2(userInfo);
}
// 这种写法只是添加指定的参数
int insert1(("username") String username,("password") String password);
<insert id="insert1">
insert into userinfo(username,password) values(#{username},#{password})
</insert>
void insert1() {
String username = "大锤";
String password = "456";
int result = userMapper.insert1(username,password);
System.out.println(result);
}
(2)返回自增id
// 返回自增的主键id
int insert3(UserInfo userInfo);
<!-- 返回自增主键id-->
<insert id="insert3" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
insert into userinfo(username,password,photo) values(#{username},#{password},#{photo})
</insert>
@Test
void insert3() {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("二毛");
userInfo.setPassword("123");
userInfo.setPhoto("images/33.png");
int result = userMapper.insert3(userInfo);
System.out.println("受影响的行数:" + result + "\n返回的自增id主键:" + userInfo.getId());
}
7.like查询
Like
查询可以查询以什么开头、什么结尾、包含什么,同时也可以搭配通配符使用(_
——》匹配单个字符),这里我以包含什么举例实现。
// like查询
List<UserInfo> getLike(("username") String username);
<!-- like查询-->
<select id="getLike" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like '%#{username}%'
</select>
void getLike() {
String username = "四";
List<UserInfo> list = userMapper.getLike(username);
System.out.println(list);
}
会发现报错了,原因是因为使用了#{}
,预执行的,那么就会变成('四'
)
如果换成${}
符是可以的,但是不能这样使用,因为上面有了${}
使用的场景,所以不符合规范使用。
所以我们可以考虑在测试代码中拼接的方式。但是这种写法的话就不建议使用。
所以我们这里要使用的是MySQL里的内置函数(concat
),直接拼接字符,可以无限拼接。
四、MyBatis注解的方式实现多表查询
1.返回类型:resultType
很多时候的查询场景都可以使用resultType进行返回的,但对于增、删、改来讲返回的是受影响的行数,可以不用设置返回类型。
使用restultType的好处就是使用方便,直接定义到某个实体类即可。
2.返回字典映射:resultMap
和resultType一样的都是属于结果映射属性。
resultMap使用场景:
(1)字段名称和程序中的属性名不同的情况。
(2)一对一和一对多关系可以使用resultType映射并查询数据。
3.多表查询
实体关系:一对一、一对多、多对多
(1)一对一的表映射
一对一映射要使用<association>
标签,例如一篇文章只对应一个作者:
我们要查找某一篇文章的作者,之前写MyBatis操作数据库,我们需要写两个类,这里的话我们就使用注解,就不使用之前的方式(接口声明,xml实现),我们只需要在接口声明中使用注解即可(将sql语句放在注解中)
MyBatis使用注解的方式操作数据库,查询 、添加 、删除 、修改
(2)一对多的表映射
我们在使用多表查询的时候可以使用单线程和多线程的方式去实现查找对应信息。(查询某个作者写的所有文章)
单线程方式:先把作者信息查询出来,然后在查询对应文章信息,最后把文章信息添加到作者信息中,就可以啦,所以我们需要在用户对象中添加一个属性List,用来存放查询的作者及文章信息。
// 文章列表
private List<ArticleInfo> articleInfoList;
// 根据用户表id查询作者信息
@Select("select * from userinfo where id = #{id}")
UserInfo getUserById1(@Param("id") Integer id);
@Select("select * from articleinfo where uid = #{uid}")
List<ArticleInfo> getListByuid(@Param("uid") Integer uid);
// 查询一个作者他的所有文章
@Autowired
private ArticleMapper articleMapper;
@Test
void getUserList() {
int uid = 1;
// 1.根据 uid查询 userinfo
UserInfo userInfo = userMapper.getUserById1(uid);
// System.out.println(userInfo);
// 2.根据 uid查询文章列表
List<ArticleInfo> articleInfoList = articleMapper.getListByuid(uid);
// System.out.println(articleInfoList);
// 组装数据
userInfo.setArticleInfoList(articleInfoList);
System.out.println(userInfo);
}
多线程:创建多线程方式、线程池
void getUserList() {
int uid = 1;
// 多线程方式(线程池)
// 1.定义线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,10,100,
TimeUnit.SECONDS,new LinkedBlockingDeque<>(100));
final Object[] resultArray = new Object[2];
threadPoolExecutor.submit(new Runnable() {
public void run() {
// 1.根据 uid查询 userinfo
resultArray[0] = userMapper.getUserById1(uid);
}
});
threadPoolExecutor.submit(new Runnable() {
public void run() {
// 2.根据 uid查询文章列表
resultArray[1] = articleMapper.getListByuid(uid);
}
});
// 等线程池执行完后在进行组装(如何判断线程池里面的线程执行完毕)
while (threadPoolExecutor.getTaskCount() != threadPoolExecutor.getCompletedTaskCount()){
}
// 组装数据
UserInfo userInfo = (UserInfo) resultArray[0];
userInfo.setArticleInfoList((List<ArticleInfo>) resultArray[1]);
System.out.println(userInfo);
}
五、动态SQL使用
1.if
我们在填写一些表单信息的时候,会发现有的是必填项,有的则是非必填项,那么对于这种情况的处理我们就需要使用if去判断了。
<!-- 动态sql,if(插入语句)-->
<insert id="insert4" parameterType="com.example.demo.model.UserInfo" useGeneratedKeys="true" keyProperty="id">
insert into userinfo(username,password
<if test="photo != null">
,photo
</if>
)values(#{username},#{password}
<if test="photo != null">
,#{photo}
</if>
)
</insert>
其中的photo是非必填字段,那么我们在插入语句中设置if
动态sql,然后我们可以发现不给photo设置值也可以存进去。
2.trim
如果不是一个非必填项,假设所有字段都是非必填项,那么我们就要考虑使用<trim>
标签结合<if>
标签,对多个字段动态生成sql。
trim标签属性:
prefix:表示整个语句块,以prefix的值作为前缀
suffix:表示整个语句块,以suffix的值作为后缀
prefixOverrides:表示整个语句块要去除掉的前缀
suffixOverrides:表示整个语句块要去除掉的后缀
<!-- 动态sql,if(插入语句)-->
<insert id="insert5" parameterType="com.example.demo.model.UserInfo" useGeneratedKeys="true" keyProperty="id">
insert into userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username != null">
username,
</if>
<if test="password != null">
password,
</if>
<if test="username != null">
photo,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="username != null">
#{username},
</if>
<if test="password != null">
#{password},
</if>
<if test="username != null">
#{photo},
</if>
</trim>
</insert>
在上面的sql动态解析时候,会将第一个<trim>部分做如下处理:
(1)基于prefix配置,开始部分加上(
(2)基于suffix配置,结束部分加上)
(3)多个<if>组织的语句都已`,`结尾,在最后拼接好的字符串还会以`,`结尾,会基于suffixOverrides配置去掉最后一个`,`
(4)注意<if test=""></if>,test的属性是传入对象的属性,不是数据库表中的字段名
3.where
用户传入过来的对象属性,有时候会根据属性做where
条件查询,用户对象中属性不为null
的,都为查询条件。查询用户名为xx的。
<!-- 动态sql,if(插入语句)-->
<select id="getWhere" resultType="com.example.demo.model.UserInfo">
select * from userinfo
<where>
<if test="username != null">
username = #{username}
</if>
</where>
</select>
where标签作用:
(1)根据where标签中的内容决定是否生成”where"关键字
(2)去除最前面的“and”关键字
4.set
根据传入的用户对象属性来更新用户数据,可以使用set
标签指定动态内容(根据用户id,修改其它属性)
// 动态sql,set(修改操作)
int sets(UserInfo userInfo);
<!-- 动态sql,set(修改语句)-->
<update id="sets">
update userinfo
<set>
<if test="username != null">
username=#{username}
</if>
<if test="password != null">
password=#{password}
</if>
<if test="photo != null">
photo=#{photo}
</if>
</set>
where id=#{id}
</update>
5.delete、foreach
如果遇到我们要对一个集合进行操作,可以使用foreach
。
foreach属性:
collection:绑定方法参数中的集合,如List,Set,Map或数组对象
item:遍历时的每一个对象
open:语句块开头的字符串
close:语句块结束的字符串
separator:每次遍历之间间隔的字符串。
示例:根据多个文章id来删除文章数据
// foreache、id,根据多个文章id来删除文章数据
int delarticle(List<Integer> id);
<!-- // foreache、id,根据多个文章id来删除文章数据-->
<delete id="delarticle">
delete from articleinfo where id in
<foreach collection="list" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>