MyBatis操作数据库
文章目录
- MyBatis操作数据库
- :one:什么是MyBatis
- :two:创建SSM项目
- 引入依赖
- 配置文件设置
- MyBatis底层逻辑
- :three:实现CRUD功能
- 查询
- 全列查询
- 带参数的查询
- 新增
- 获取自增主键
- 删除
- 更新
- :four:参数占位符:#{}和${}
- 不支持String参数问题
- ${}使用场景:
- sql注入问题
- :five:ResultMap的用处
- :six:多表查询
- :seven:动态sql
1️⃣什么是MyBatis
MyBatis是一个持久层框架(用于实现数据持久化的框架),它的底层也是基于JDBC的,不过用MyBatis比直接用JDBC操作数据库要方便很多。
MyBatis是一个ORM(Object Relational Mapping)框架,即对象关系映射框架,映射关系:
数据表–>>映射成一个类
一条记录–>>映射成一个对象
表中的字段–>>映射成类中的属性
由于MyBatis实现了这层映射关系,我们可以像操作对象一样来操作数据表。
2️⃣创建SSM项目
SSM项目:SpringBoot+SpringMVC+MyBatis
引入依赖
- MyBatis Framework 和 MySQL Driver这两个依赖是为了操作数据库
- SpringWeb即SpringMVC
- Lombok是一个工具,提供了很多好用的注解
当添加了MyBatis Framework 和 MySQL Driver 以及 SpringWeb这三个依赖后就构成了一个SSM项目
配置文件设置
配置文件需要设置两项内容:
- 数据库的连接信息
- MyBatis的xml文件的存放位置(xml文件中是sql语句)
# 数据库连接配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/myblog?characterEncoding=utf8&useSSL=false
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置myBatis的xml文件路径
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
MyBatis底层逻辑
当引入依赖,且配置好配置文件后,就可以使用MyBatis来做增删改查等功能了。在使用MyBatis之前,先了解MyBatis在整个后端中的定位:
数据持久层MyBatis是由Service层去调用的,然后我们知道MyBatis是基于JDBC的,MyBatis是对JDBC做了一个封装,不过封装后供Service层调用的接口不是纯的java类,而是接口和.xml文件的组合。
接口可以代表一张表,接口中是方法的声明,接口对应着有一个xml文件,xml文件中是方法的具体实现,方法的具体实现就是sql语句,比如查询,新增等语句。所以MyBatis相当于是把sql语句从类中分离出来了(JDBC时代sql语句是在类中的),分离到了.xml文件中。
接口搭配上接口的具体实现就可以去调用JDBC,进而操作数据库。所以MyBatis对于JDBC封装后,给开发者提供的不再是一些类,而是接口+xml文件。开发者使用接口+xml的组合就能实现相应的数据库操作。
3️⃣实现CRUD功能
查询
全列查询
使用MyBatis实现全列查询:
1️⃣先往数据库user表中添加两条记录,用于MyBatis查询:
2️⃣添加实体类:因为MyBatis是一个ORM框架,实现了数据库中的数据与对象之间的一个映射,所以当查询出了数据之后,可以自动创建出实体类对象,并给对象的属性赋值。但前提是先有一个实体类:
package com.example.demo.model;
import lombok.Data;
@Data
public class User {
private int id;
private String name;
private int age;
}
- 需要注意的是这里的类中的属性需要和数据表的字段名对应上,如果某一属性名和数据表中的字段名不对应,是没法赋值的。
- user表中的一条记录将来就会映射出一个User实例
3️⃣定义UserMapper接口:这个接口中有对于User表的增删改查的方法的定义
package com.example.demo.mapper;
import com.example.demo.model.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper {
//getAll()来实现全列查询
public List<User> getAll();
}
- 该接口需要添加@Mapper注解,该注解代表这个接口是用于MaBatis的,
4️⃣实现接口:即创建对应的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.example.demo.mapper.UserMapper" >
<select id="getAll" resultType="com.example.demo.model.User">
select * from user;
</select>
</mapper>
- namespace=“com.example.demo.mapper.UserMapper”:对应到某个上面的接口:包名+接口名
- select 标签代表查询,id="getAll"对应到接口中的getAll()方法,所以这个select标签即为接口中getAll()方法的具体实现。
- resultType=“com.example.demo.model.User”>:select操作的返回结果赋值给哪个类
至此MyBatis的代码就写完了,也实现了这个全列查询功能,那接下来就来验证一下该功能:
使用单元测试的方式来验证:
UserMapperTest测试类:
package com.example.demo.mapper;
import com.example.demo.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserMapperTest {
@Autowired
UserMapper userMapper;
@Test
void getAll() {
List<User> list = userMapper.getAll();
for (User user:list) {
System.out.println(user.toString());
}
}
}
- 类上加@SpringBootTest注解,代表当前测试的环境是SpringBoot
- 注入UserMapper
⭕️执行单元测试,最终就可以从数据库中拿到数据,并赋值给User实例
带参数的查询
UserMapper接口:
package com.example.demo.mapper;
import com.example.demo.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface UserMapper {
//getAll()来实现全列查询
public List<User> getAll();
//根据user表的id字段来查询
public User getById(@Param("id") int id);
}
- @Param注解中的"id"用来和对应的xml文件中的#{id}对应
- 这个@Param注解加上保证不出错,不加在linux平台可能会出错
UserMapper.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.example.demo.mapper.UserMapper" >
<select id="getAll" resultType="com.example.demo.model.User">
select * from user;
</select>
<select id="getById" resultType="com.example.demo.model.User">
select * from user where id=#{id};
</select>
</mapper>
- 使用#{id},来对应UserMapper接口中getById()方法中的@Param(“id”)
新增
给article表添加记录
1️⃣article表:
2️⃣实体类:article类:
package com.example.demo.model;
import lombok.Data;
@Data
public class Article {
private int id;
private String title;
private String content;
}
3️⃣ArticleMapper接口:
package com.example.demo.mapper;
import com.example.demo.model.Article;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ArticleMapper {
//添加一条记录到article表
public int add(Article article);
}
4️⃣ArticleMapper.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.example.demo.mapper.ArticleMapper" >
<insert id="add">
insert into article values(#{id},#{title},#{content})
</insert>
</mapper>
- insert标签不需要添加ResultType,因为insert操作返回的是影响的行数
- 对于参数是对象的情况:#{id},#{title},#{content} ,#{}的括号中直接写属性就可以了。
- 如果在接口的方法中加了@Param(“article”)这个注解,那在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.example.demo.mapper.ArticleMapper" >
<insert id="add">
insert into article values(#{article.id},#{article.title},# {article.content})
</insert>
</mapper>
5️⃣单元测试:
package com.example.demo.mapper;
import com.example.demo.model.Article;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ArticleMapperTest {
@Autowired
private ArticleMapper articleMapper;
@Test
void add() {
Article article = new Article();
// article.setId(10);
article.setTitle("第一条记录");
article.setContent("今天嘻嘻哈哈");
int result = articleMapper.add(article);
System.out.println(result);
}
}
- 因为article表的id字段是自增主键,所以是不应该给article对象的id属性赋值的,不赋值的时候id属性默认是0,在插入的时候id主键就会实现自增:从1开始,然后2,3,4,5,6…
获取自增主键
当添加一条记录的时候,我们是可以通过代码获取到该记录的自增主键的值的:
ArticleMapper接口:
package com.example.demo.mapper;
import com.example.demo.model.Article;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ArticleMapper {
//添加一条记录,并返回article表的自增id
public int addGetId(@Param("article") Article article);
}
ArticleMapper.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.example.demo.mapper.ArticleMapper" >
<insert id="addGetId" useGeneratedKeys="true" keyProperty="id">
insert into article values(#{article.id},#{article.title},#{article.content})
</insert>
</mapper>
在insert标签中,要添加两个属性:useGeneratedKeys=“true” keyProperty=“id”(id为自增主键的名称)。
单元测试:
package com.example.demo.mapper;
import com.example.demo.model.Article;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ArticleMapperTest {
@Autowired
private ArticleMapper articleMapper;
@Test
void addGetId() {
Article article = new Article();
article.setTitle("一条新的记录");
article.setContent("实现新增记录并返回该记录的自增id");
int result = articleMapper.addGetId(article);
System.out.println(result);
System.out.println(article.getId());
}
}
- 当成功插入一条记录后,就会将该记录的id返回给article对象的id属性,通过article.getId()就可以获取到新增记录的自增主键id的值
- 通过获取自增主键id的值,可以了解到article对象不仅可以作为待插入数据库的原始数据,还可以用来接收从数据库返回的数据。
删除
根据文章的id删除一篇文章
ArticleMapple接口:
package com.example.demo.mapper;
import com.example.demo.model.Article;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ArticleMapper {
//添加一条记录到article表
public int add( @Param("article") Article article);
//根据id删除一条记录
public int deleteById(@Param("id") int id);
}
ArticleMapper.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.example.demo.mapper.ArticleMapper" >
<insert id="add">
insert into article values(#{article.id},#{article.title},#{article.content})
</insert>
<delete id="deleteById">
delete from article where id=#{id}
</delete>
</mapper>
更新
根据文章id修改文章标题:
ArticleMapper接口:
package com.example.demo.mapper;
import com.example.demo.model.Article;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ArticleMapper {
//添加一条记录到article表
public int add( @Param("article") Article article);
//根据id删除一条记录
public int deleteById(@Param("id") int id);
//根据文章id修改文章标题
public int updateTitle(@Param("id") Integer id,@Param("title") String title);
}
ArticleMapper.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.example.demo.mapper.ArticleMapper" >
<insert id="add">
insert into article values(#{article.id},#{article.title},#{article.content})
</insert>
<delete id="deleteById">
delete from article where id=#{id}
</delete>
<update id="updateTitle">
update article set title=#{title} where id=#{id}
</update>
</mapper>
4️⃣参数占位符:#{}和${}
- #{}:预编译处理:会使用JDBC的那种方式,使用?作为占位符,然后调用setInt()方法或者setString()方法来替换掉占位符
- ${}:直接替换
不支持String参数问题
举个栗子:
1️⃣传递int类型的参数:
ArticleMapper接口:
package com.example.demo.mapper;
import com.example.demo.model.Article;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ArticleMapper {
//查询出id为3的这条记录
public Article selectById(@Param("id") int id);
}
ArticleMapper.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.example.demo.mapper.ArticleMapper" >
<select id="selectById" resultType="com.example.demo.model.Article">
select * from article where id=${id}
</select>
</mapper>
- 当传递的参数是int类型的时候,使用#{}和使用${}是都可以运行的
2️⃣传递String类型的参数:
ArticleMapper接口:
package com.example.demo.mapper;
import com.example.demo.model.Article;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ArticleMapper {
//查询出title为"你好,世界"的这条记录
public Article selectByTitle(@Param("title") String title);
}
ArticleMapper.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.example.demo.mapper.ArticleMapper" >
<select id="selectByTitle" resultType="com.example.demo.model.Article">
select * from article where title=#{title}
</select>
</mapper>
- 当传递的参数是String类型时,就必须使用#{},不能使用${}。
因为#{}使用的是?作为占位符,然后再调用preparedStatement.setString()方法替换掉占位符,最终执行的sql语句是:
select * from article where title=‘你好,世界’;(有单引号)
而使用${}这种方式,是直接替换的方式,直接替换的方式,最终执行的sql语句是:
select * from article where title=你好,世界;(无引号),无引号的方式会把:你好,世界作为字段名称去数据库查询,会报错:
报错提示:Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘你好,世界’ in ‘where clause’
那要想使用 ,就必须手动加单引号: s e l e c t ∗ f r o m a r t i c l e w h e r e t i t l e = ′ {},就必须手动加单引号:select * from article where title=' ,就必须手动加单引号:select∗fromarticlewheretitle=′{title}'。
${}使用场景:
通过上面的对比,我们发现,使用#{}比使用 的方式要好,所以 99.9 {}的方式要好,所以99.9%的场景使用#{}就OK了,但是有一种场景,必须使用 的方式要好,所以99.9{},就是传递的参数是一个系统参数,比如用于升序的 asc以及用于降序的desc
当传递的是个String类型的字段时是应该加引号的,但是如果传asc 以及desc,就会出错
ArticleMapper接口:
package com.example.demo.mapper;
import com.example.demo.model.Article;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ArticleMapper {
//按照id升序或者降序查询、
public List<Article> selectOrderById(@Param("order") String order);
}
ArticleMapper.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.example.demo.mapper.ArticleMapper" >
<select id="selectOrderById" resultType="com.example.demo.model.Article">
select * from article order by id ${order}
</select>
</mapper>
单元测试:
package com.example.demo.mapper;
import com.example.demo.model.Article;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ArticleMapperTest {
@Autowired
private ArticleMapper articleMapper;
@Test
void selectOrderById() {
List<Article> list = articleMapper.selectOrderById("desc");
for (Article article:list) {
System.out.println(article);
}
}
}
当使用#{}的时候,实际执行的sql语句为: select * from article order by id ‘desc’(有引号)
当使用${}的时候,实际执行的sql语句为:select * from article order by id desc(无引号)
当传递的参数是系统参数,asc,desc的时候,使用#{}就会出错,这种情况应该使用${}
这是${}的使用场景。
sql注入问题
#{}和 除了上面的使用场景上的差别外,还存在另一比较大的区别,就是 {}除了上面的使用场景上的差别外,还存在另一比较大的区别,就是 除了上面的使用场景上的差别外,还存在另一比较大的区别,就是{}可能引发sql注入问题,而#{}则不会引发sql注入问题。
UserMapper接口:
package com.example.demo.mapper;
import com.example.demo.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface UserMapper {
//根据name查询出用户
public User getUserByName(@Param("name") String name);
}
UserMapper.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.example.demo.mapper.UserMapper" >
<select id="getUserByName" resultType="com.example.demo.model.User">
select * from user where name = '${name}'
</select>
</mapper>
单元测试:
package com.example.demo.mapper;
import com.example.demo.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserMapperTest {
@Autowired
UserMapper userMapper;
@Test
void getUserByName() {
User user = userMapper.getUserByName("' or 1='1");
System.out.println(user);
}
}
- 当使用${}时:
单元测试中传递的参数是:’ or 1='1,xml文件中使用${}来接收参数,最终执行的sql语句为:
select * from user where name = ‘’ or 1=‘1’ (1='1’是恒成立的,因为Mysql会隐式类型转换),所以即使前端传递的参数在数据库中并没有,在这里也能查询出结果。这就是sql注入问题,要解决问题,可以在controller层验证参数的合法性,比如:如果传递的参数中有引号,就直接不往后执行,直接返回给前端一个结果:参数不合法。
- 当使用#{}时:
当使用#{}就不会出现上面的sql注入问题,因为使用#{},就不是${}的字符串直接替换的方式了,而是调用的JDBC中的:PreparedStatement的setString()来给参数赋值,如果参数中有单引号,会给单引号加一个转义字符:\ ,让单引号仅仅表示一个普通的字符,就能避免sql注入问题。
⭕️通过博客系统的登录功能来演示一下:
前端输入的用户名会传给这个方法:
打印一下待执行的sql语句:
- 可以发现通过setString的这种方式会给单引号加上转义字符,以此来避免了sql注入问题。
而#{}底层使用的就是使用JDBC的setString()这种方式,所以不会出现sql注入问题。
总结:
- #{}的方式更安全,大部分情况下应该使用#{},除了asc,desc那中情况
- 如果${},就要在controller层验证一下参数的合法性
5️⃣ResultMap的用处
一般来说,实体类的属性名要和数据表的字段名一样,如果不一样的话,那在做查询操作的时候,对象的属性就可能不会被赋值。但是有的时候,我们不希望有这种限制,因为可能数据表的字段名不是很合理,但是类的属性名又想合理起名,那这时候就需要给实体类的属性和数据表的字段设置一个映射关系了。这时候ResultMap就登场了,ResultMap就是用来实现这种对应关系的。
1️⃣Article实体类:
package com.example.demo.model;
import lombok.Data;
@Data
public class Article {
private int id;
private String titles;
private String content;
}
2️⃣Article数据表:
3️⃣ArticleMapper接口:
package com.example.demo.mapper;
import com.example.demo.model.Article;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ArticleMapper {
//查询出id为3的这条记录
public Article selectById(@Param("id") Integer id);
}
4️⃣ArticleMapper.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.example.demo.mapper.ArticleMapper" >
//类属性和表字段的映射关系
//id可以任意取,type为要映射的类
<resultMap id="baseMap" type="com.example.demo.model.Article">
//id标签用于主键映射,不过使用result映射主键也是可行的
<id column="id" property="id"></id>
//result标签用于普通字段映射
<result column="title" property="titles"></result>
<result column="content" property="content"></result>
</resultMap>
//resultMap为上面的resultMap标签中的id
<select id="selectById" resultMap="baseMap">
select * from article where id=#{id}
</select>
</mapper>
- 当按上面配置好xml文件后,就可以实现映射关系
- 如果类中有的属性名和数据报的字段名一样,当使用了ResultMap映射后,最好也在resultMap标签中映射一下
6️⃣多表查询
多表查询和单表查询类似,都是写接口,然后实现接口。
但是有一点区别,比如说,一个文章表,一个作者表,对这两张表进行联合查询,文章表作为主表,如果想要查询出文章表的所有列,以及每篇文章对应的作者姓名,就可以对这两张表进行笛卡尔积,然后筛选条件为文章表中的作者id=作者表中的id。由于我们多查询了一列:作者姓名,所以可以在文章表对应的实体类中加一个属性用来表示文章的作者姓名。最终将查询结果依次赋值给文章表实体类的每个属性。
唯一一点区别就是需要多查询其他表中的哪些列,就在当前主表中加上哪些与之对应的属性。