Mybatis

文章目录


前言

MyBatis是⼀款优秀的 持久层(Dao层) 框架,用于简化JDBC的开发

下面是一个实现 Mybatis 的例子,首先需要在 yml / properties 文件中配置数据库相关参数。

xml 复制代码
# 数据库连接配置
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

在项目中, 创建持久层接口UserInfoMapper

java 复制代码
@Mapper
public interface UserInfoMapper {
 //查询所有⽤⼾
 @Select("select * from userinfo")
 public List<UserInfo> queryAllUser();
}

可以在已经创建好的测试类下可进行测试(在src下的test目录下),或者可以使⽤Idea⾃动生成测试类(在Mapper中右键Generate Test),@SpringBootTest 注解加载 Spring 运行环境,测试结果打印表中的所有数据

java 复制代码
@SpringBootTest
class DemoApplicationTests {
	
	@Autowired
	private UserInfoMapper userInfoMapper;
	
	@Test
	void contextLoads() {
		List<UserInfo> userInfoList = userInfoMapper.queryAllUser();
		System.out.println(userInfoList);
	}
}

Mybatis的持久层接⼝规范⼀般都叫 XxxMapper,@Mapper注解表示是 Mybatis 中的 Mapper 接口,程序运行时,框架会自动⽣成接口的实现类对象(代理对象),并给交 Spring 的 IOC 容器管理,@Select注解代表的就是 select 查询,也就是注解对应方法的具体实现内容。

MyBatis基础操作

打印日志

在配置文件中配置下面的信息,

xml 复制代码
mybatis:
  configuration: # 配置打印 MyBatis⽇志
  log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
java 复制代码
@Slf4j
@SpringBootTest
class UserInfoMapperTest {
	
	@Autowired
	private UserInfoMapper userInfoMapper;
	
	@Test
	void queryAllUser() {
		log.info(userInfoMapper.queryAllUser().toString())
	}
}

日志中显示查询语句,传递的参数及类型、SQL的执行结果

参数传递

使用 #{ } 的方式获取方法中的参数,当mapper接口方法形参只有⼀个普通类型的参数,#{...} 里的属性名可以随便写,但是建议和参数名保持⼀致

java 复制代码
@Mapper
public interface UserInfoMapper {
 //通过id查询用户
 @Select("select * from userinfo where id = #{id}")
 public List<UserInfo> queryById(Integer userId);
}
java 复制代码
@Test
void queryById() {
	UserInfo userInfo = userInfoMapper.queryById(4);
	System.out.println(userInfo);
}

当参数是两个及以上时,若SQL 占位符与参数名称不匹配,此时 MyBatis 默认按参数名匹配,按位生成userId、deleteFlag、param1、param2。此时无法找到 id 对应的值 。

java 复制代码
@Mapper
public interface UserInfoMapper {
 @Select("select * from userinfo where id = #{id} and delete_flag = #{deleteFlag}")
 public List<UserInfo> queryByIdanddf(Integer userId, Integer deleteFlag);
}

param1、param2是给参数起的默认的名,对应的就是userId、deleteFlag。也可以通过param1、param2来进行传参,此时代码运行成功。

java 复制代码
@Mapper
public interface UserInfoMapper {
 @Select("select * from userinfo where id = #{param1} and delete_flag = #{deleteFlag}")
 public List<UserInfo> queryByIdanddf(Integer userId, Integer deleteFlag);
}

也可以通过 @Param,设置参数的别名,如果使用 @Param 设置别名,#{...} 里的属性名必须和@Param 设置的⼀样,此时把 userId 参数绑定给了 id,此时不能使用 #{ userid }。

java 复制代码
@Mapper
public interface UserInfoMapper {
 @Select("select * from userinfo where id = #{id} and delete_flag = #{deleteFlag}")
 public List<UserInfo> queryByParam(@Param("id")Integer userId, Integer deleteFlag);
}

参数传递时,默认使用的是方法中的参数名称,如果使用@Param进行了重命名,则使用重命名后的名字,总之必须保持前后对应

新增(Insert)

可以使用对象来进行参数传递,从而进行更新,此时直接使用属性名进行传递即可

java 复制代码
@Insert("insert into userinfo (username, password, age, gender, phone) values (#{username},#{password},#{age},#{gender},#{phone})")
Integer insert(UserInfo userInfo);

如果设置了 @Param 属性,#{...}中就需要用 "参数.属性" 的方式来获取

java 复制代码
@Insert("insert into userinfo (username, password, age, gender, phone) values (#{userInfo.username},#{userInfo.password},#{userInfo.age},#{userInfo.gender},#{userInfo.phone})")
Integer insert(@Param("userInfo")UserInfo userInfo);

Insert 语句默认返回的是受影响的行数,可以在Mapper接口的方法上添加⼀个Options的注解

  • useGeneratedKeys 会令 MyBatis 使用 JDBC 的getGeneratedKeys 方法来取出由数据库内部生成的主键
  • keyProperty 指定能够唯⼀识别对象的属性,此时插入自增的这个id会赋值到 keyProperty 指定的属性
java 复制代码
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into userinfo (username, password, age, gender, phone) values (#{userInfo.username},#{userInfo.password},#{userInfo.age},#{userInfo.gender},#{userInfo.phone})")
Integer insert(@Param("userInfo")UserInfo userInfo);

此时会输出添加到数据库中的对应数据的 id 值

java 复制代码
Integer count = userInfoMapper.insert(userInfo);
System.out.println("添加数据条数:" +count +", 数据ID:" + userInfo.getId());

删除(Delete)

java 复制代码
@Delete("delete from userinfo where id = #{id}")
void delete(Integer id);

修改(Update)

java 复制代码
@Update("update userinfo set username=#{username} where id=#{id}")
void update(UserInfo userInfo);

查询(Select)

java 复制代码
@Select("select * from userinfo where id = #{id}")
public List<UserInfo> queryById(Integer userId);

查询中的赋值映射

在使用MyBatis进行查询的时候,只有Java对象属性和数据库字段⼀模⼀样时, 才会进行赋值。

如下所示,当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性,此时deletFlag、creatTime、updateTime与数据库字段不一致,则不会进行赋值。

对于不一样的字段,可以通过以下方式进行对应赋值

  1. 改别名
java 复制代码
@Select("select id, username, password, age, gender, phone, delete_flag as " + 
"deleteFlag,create_time as createTime, update_time as updateTime from user_info")
public List<UserInfo> queryAllUser();
  1. 通过@Results注解:column对应数据库中的字段;property对应Java中的属性。
java 复制代码
@Results({
	@Result(column = "delete_flag",property = "deleteFlag"),
	@Result(column = "create_time",property = "createTime"),
	@Result(column = "update_time",property = "updateTime")
})
@Select("select id, username, `password`, age, gender, phone, delete_flag, create_time, update_time from user_info")
List<UserInfo> queryAllUser();

可以给@Results定义⼀个名称,后续可以通过@ResultMap进行复用

java 复制代码
@Results(id = "Baseresult",value = {
	@Result(column = "delete_flag",property = "deleteFlag"),
	@Result(column = "create_time",property = "createTime"),
	@Result(column = "update_time",property = "updateTime")
})
@Select("select id, username, password, age, gender, phone, delete_flag, create_time, update_time from userinfo")
List<UserInfo> queryAllUser();

@ResultMap(value = "Baseresult")
@Select("select id, username, password, age, gender, phone, delete_flag, create_time, update_time from userinfo where id= #{userid}")
UserInfo queryById(@Param("userid") Integer id);
  1. 自动驼峰转换
     驼峰命名规则为 abc_xyz => abcXyz,因此对于数据库表中的字段名 abc_xyz ,对应类中属性名abcXyz。此时的Java 代码不做任何处理。
     可在 yml 文件中进行配置
xml 复制代码
mybatis:
 configuration:
 map-underscore-to-camel-case: true #配置驼峰⾃动转换

MyBatis的xml配置规范

对于同一个方法来说,Mybatis的实现方式有两种:通过注解和通过xml的方式实现。对于同一个接口来说,其中的方法可以任选一种进行实现

基础实现流程

可以通过以下步骤实现

(1)在数据持久层添加 mapper 接口

java 复制代码
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface UserInfoXMlMapper {
	List<UserInfo> queryAllUser();
}

(2)添加 UserInfoXMLMapper.xml,MyBatis 的固定 xml 格式如下,在namespace中声明实现的是哪一个接口

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.UserInfoMapper">

</mapper>

(3)配置 mybatis xml 的文件路径,本例在 resources/mapper 文件夹下创建所有表的 xml 文件,因此在 yml 文件中配置如下,classpath表示当前的资源目录(即resources),mapper 表示在mapper文件夹下,**Mapper.xml 表示以 Mapper.xml 作为结束的文件

xml 复制代码
mybatis:
  mapper-locations: classpath:mapper/**Mapper.xml

(4)查询用户具体的实现,其中<select> 表示查询标签, id 中填写对应Interface (接口)中定义的方法名称,需要保持一致,表示对接空的具体实现\,resultType 表示返回的结果类型(路径写全)

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.UserInfoXMlMapper">
	<select id="queryAllUser" resultType="com.example.demo.model.UserInfo">
		select username,`password`, age, gender, phone from userinfo
	</select>
</mapper>

(5)进行单元测试

java 复制代码
@SpringBootTest
class UserInfoMapperTest {
	@Autowired
	private UserInfoMapper userInfoMapper;
	
	@Test
	void queryAllUser() {
		List<UserInfo> userInfoList = userInfoMapper.queryAllUser();
		System.out.println(userInfoList);
	}
}

新增(Insert)

xml 复制代码
<insert id="insertUser">
	insert into userinfo (username, password, age, gender, phone) values 
	(#{username}, #{password}, #{age},#{gender},#{phone})
</insert>

删除(Delete)

xml 复制代码
<delete id="deleteUser">
	delete from user_info where id = #{id}
</delete>

修改(Update)

xml 复制代码
<update id="updateUser">
	update user_info set username=#{username} where id=#{id}
</update>

查询(Select)

xml 复制代码
<select id="queryAllUser" resultType="com.example.demo.model.UserInfo">
	select id, username,password, age, gender, phone, 
	delete_flag, create_time, update_time from user_info
</select>

查询中的赋值映射

和在使用注解实现 MyBatis 时遇到的问题一样,参数名和字段名不一致,查询返回为 null ,其解决方案和注解类似

  1. 起别名
  2. 结果映射
  3. 开启驼峰命名

其中1,3的解决办法和注解⼀样,对于结果映射来说使用<resultMap>来进行实现,其中<id>表示主键。需要注意<select>中的resultMap属性需要修改为 BaseMap

xml 复制代码
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
	<id column="id" property="id"></id>
	<result column="delete_flag" property="deleteFlag"></result>
	<result column="create_time" property="createTime"></result>
	<result column="update_time" property="updateTime"></result>
</resultMap>

<select id="queryAllUser" resultMap="BaseMap">
select id, username,`password`, age, gender, phone, delete_flag, 
create_time, update_time from user_info
</select>

多表查询

需要补充实体类中的相关信息

java 复制代码
@Data
public class ArticleInfo {
	private Integer id;
	private String title;
	private String content;
	private Integer uid;
	private Integer deleteFlag;
	private Date createTime;
	private Date updateTime;
	//⽤⼾相关信息
	private String username;
	private Integer age;
	private Integer gender;
}

多表查询的接口定义如下,

java 复制代码
@Mapper
public interface ArticleInfoMapper {
	@Select("SELECT ta.id,ta.title,ta.content,ta.uid,tb.username,tb.age,tb.gender "+
	"FROM articleinfo ta LEFT JOIN user_info tb ON ta.uid = tb.id " +
	"WHERE ta.id = #{id}")
	ArticleInfo queryUserByUid(Integer id);
}

#{ } 和 ${ }

MyBatis 参数赋值有两种方式,包括使用 #{ } 和 ${ }。

对于 Interger 类型的参数

java 复制代码
@Select("select * from user_info where id= #{id}")
UserInfo queryById1(Integer id);

@Select("select * from user_info where id= ${id}")
UserInfo queryById2(Integer id);

打印得到的日志如下,可以发现,使用 #{ } 进行参数传递时,存在占位符 " ?" ,此时为预编译SQL语句;使用 ${ } 进行参数传递时,直接进行了拼接,此时为即时SLQ语句。

对于 String类型的参数

java 复制代码
@Select("select * from user_info where username= #{name} ")
UserInfo queryByName1(String name);

@Select("select * from user_info where username= ${name} ")
UserInfo queryByName2(String name);

此时${ } 进行参数传递时出现了错误,此时参数直接进行拼接,正确的SQL应为select * from userInfo where username = 'admin'

更改代码后,运行成功

java 复制代码
@Select("select * from user_info where username= '${name}' ")
UserInfo queryByName2(String name);
  • #{} 使⽤的是预编译SQL,通过 ? 占位的方式,提前对SQL进行编译,然后把参数填充到SQL语句中,#{} 会根据参数类型,自动拼接引号 ''
  • ${} 会直接进行字符替换,⼀起对SQL进行编译,如果参数为字符串,需要加上引号 '' .

SQL注入

SQL注入是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。

例如一个正常的SQL语句,

sql 复制代码
SELECT * FROM users WHERE username = 'user1' AND password = 'password1';

如果用户输入user1 为 admin' --,password1 为 anything,此时的SQL语句如下,-- 会把后面的校验注释掉,直接进行登录。

sql 复制代码
SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything';

又例如用户输入 user1 为 admin' OR '1'='1,password1 为 anything,此时的SQL语句如下,由于 '1'='1' 为 true,此时也会绕过验证。

sql 复制代码
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'anything';

排序功能

使用#{ }进行排序时,结果会出现报错

java 复制代码
@Select("select * from user_info order by id #{sort} ")
List<UserInfo> queryAllUserBySort(String sort);

此时的SQL语句如下所示,排序规则字段 desc 不需要加引号,此时使用${ }则更为合适。

sql 复制代码
select * from user_info order by id 'desc'

但是${ }存在注入风险,此时可以选择不接受用户输入的参数,参数由后台来进行处理,以上面为例对参数进行校验,仅接收 desc 或 asc 。

like查询

like 使用 #{} 报错

java 复制代码
@Select("select * from user_info where username like %#{key}%")
List<UserInfo> queryAllUserByLike(String key);

此时的 SQL 语句为

sql 复制代码
select * from user_info where username like %'admin'%

可以使用字符串拼接语句concat来进行处理

java 复制代码
@Select("select * from user_info where username like concat('%',#{key},'%')")
List<UserInfo> queryAllUserByLike(String key);

或修改为${ }后查询正确

java 复制代码
@Select("select * from user_info where username like '%${key}%'")
List<UserInfo> queryAllUserByLike(String key);

#{ } 和 ${ } 的区别

  • #{ }执行预编译SQL,{} 执行即时SQL,#{ }的性能比{ }优:#{ }执行的是预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来;${ }执行的是即时SLQ,需要每次进行编译。大多数情况下, 某⼀条 SQL 语句可能会被反复调用执行,因此使用#{ }的效率会更高

  • #{ }根据参数类型自动加引号,{} 直接进行字符串拼接,可能产生SQL注入问题,#{ }比{ }更安全:#{ }通过 ? 占位的方式提前对SQL进行编译,#{} 会根据参数类型,自动拼接引号;${ }则会直接进行字符替换。

  • 默写特殊情况下使用更为方便,例如排序或者模糊查询时:替换参数对于一些表名、字段名,不需要加引号的,使用${ }更为合适(例如排序功能的desc 或 asc)

数据库连接池

Mybatis 使用到了数据库连接池,数据库连接池负责分配、管理和释放数据库连接,它允许应⽤程序重复使同⼀个现有的数据库连接,而不是再重新建立一个。

没有使用连接池时,每次执行SQL语句,要先创建⼀个新的连接对象, 然后执行SQL语句,执行完再关闭连接对象释放资源;使用数据库连接池后,程序启动后会在数据库连接池中创建⼀定数量的Connection对象, 当请求时会从池中获取Connection对象,然后执行SQL,执行完再把Connection归还给连接池。

动态SQL操作

对于用户注册的情况,例如注册姓名、性别、年龄等数据,其中性别为非必填字段,即传入的数据为null,但此时数据库默认的gender字段设置为默认值为0,但是直接传入的时候会将该字段设置为null,此时就需要根据gender的输入做出动态的拼接操作

<if>标签

test 中写判断添加,其中的 gender,是传入对象中的属性,不是数据库字段

xml 复制代码
<insert id="insertUserByCondition">
insert into userinfo (username,password,age,
	<if test="gender != null">
	gender,
	</if>
	phone)
	values(#{username},#{password},#{age},
	<if test="gender != null">
	#{gender},
	</if>
	#{phone})
</insert>

对于所有字段都是非必填的情况来说,有下面的代码,但是如果传入的字段为username = admin ,password = 123,其余为空,此时的 SQL 语句会出现错误:insert into userinfo ( username,password ,) values ( admin,123,)。逗号更改在字段前面也会出现同样的错误

xml 复制代码
<insert id="insertUserByCondition">
insert into userinfo (
	<if test="username != null">username,</if>
	<if test="password!= null">password,</if>
	<if test="age!= null">age,</if>
	<if test="gender != null">gender,</if>
	<if test="phone!= null">phone</if>)
	values(
	<if test="username!= null">#{username},</if>
	<if test="password!= null">#{password},</if>
	<if test="age!= null">#{age},</if>
	<if test="gender!= null">#{gender},</if>
	<if test="phone!= null">#{phone}</if>
</insert>

<trim>标签

对于多个字段,⼀般考虑使用标签结合标签,对多个字段都采取动态生成的方式,<trim>有以下几个属性

  • prefix:表示整个语句块,以prefix的值作为前缀
  • suffix:表示整个语句块,以suffix的值作为后缀
  • prefixOverrides:表示整个语句块要去除掉的前缀
  • suffixOverrides:表示整个语句块要去除掉的后缀

此时代码如下所示,会去掉最后的逗号,同时给前后添加括号

xml 复制代码
<insert id="insertUserByCondition">
insert into userinfo
	<trim prefix="(" suffix=")" suffixOverrides=",">
		<if test="username != null">username,</if>
		<if test="password!= null">password,</if>
		<if test="age!= null">age,</if>
		<if test="gender != null">gender,</if>
		<if test="phone!= null">phone</if>
	</trim>
	values
	<trim prefix="(" suffix=")" suffixOverrides=",">
		<if test="username!= null">#{username},</if>
		<if test="password!= null">#{password},</if>
		<if test="age!= null">#{age},</if>
		<if test="gender!= null">#{gender},</if>
		<if test="phone!= null">#{phone}</if>
	</trim>
</insert>

<where>标签

对于多个条件查询情况,例如查询年龄为20、性别为男的所有用户,或仅查询年龄为20的用户,其中也需要用到字符串拼接操作

xml 复制代码
<select id="queryByCondition" resultType="com.example.demo.model.UserInfo">
select id, username, age, gender, phone, delete_flag, create_time, update_time from userinfo where
	<if test="age!= null">
	age = #{age}
	</if>
	<if test="gender!= null">
	and gender = #{gender}
	</if>
</select>

此时若输入 age = null,gender = " 男 ",此时的SQL语句为 select * from userinfo where and gender = " 男 "出现错误。此时可以使用 <where>标签如下所示,此时<where>会自动把 and 删掉。

xml 复制代码
<select id="queryByCondition" resultType="com.example.demo.model.UserInfo">
select * from userinfo 
	<where>
		<if test="age!= null">
		age = #{age}
		</if>
		<if test="gender!= null">
		and gender = #{gender}
		</if>
	</where>
</select>

若输入 age = null,gender = null,<where>标签会自动删除where,此时的SQL语句为select * from userinfo

<set>标签

<set>标签与 <trim>标签类似,;<set>标签用于去除更新操作中最后多余的逗号。

xml 复制代码
<update id="updateUserByCondition">
	update userinfo
	<set>
		<if test="username != null">username = #{username},</if>
		<if test="age != null">age = #{age},</if>
		<if test="deleteFlag != null">	delete_flag = #{deleteFlag},</if>
	</set>
	where id = #{id}
</update>

<foreach>标签

对集合进行遍历时可以使⽤该标签, <foreach>标签有以下属性,

  • collection:绑定方法参数中的集合,如 List,Set,Map或数组对象
  • item:遍历时的每⼀个对象
  • open:语句块开头的字符串
  • close:语句块结束的字符串
  • separator:每次遍历之间间隔的字符串

例如下面的批量删除操作

xml 复制代码
<delete id="deleteByIds">
	delete from userinfo where id in
	<foreach collection="ids" item="id" separator="," open="(" close=")">
	#{id}
	</foreach>
</delete>

测试用例的输入如下所示,

java 复制代码
void deleteByIds(List<Integer> ids);
java 复制代码
void deleteByIds(){
	userInfoMapper.deleteByIds(Arrays.asList(14,15,16,17));
}

<sql>和<include> 标签

  • <sql> :定义可重用的SQL片段
  • <include> :通过属性refid,指定包含的SQL片段
xml 复制代码
<sql id="allColumn">
	id, username, age, gender, phone, delete_flag, create_time, update_time
</sql>

通过 <include> 标签在原来抽取的地方进行引用

xml 复制代码
<select id="queryAllUser" resultMap="BaseMap">
	select
	<include refid="allColumn"></include>
	from userinfo
</select>

<select id="queryById" resultType="com.example.demo.model.UserInfo">
	select
	<include refid="allColumn"></include>
	from userinfo where id= #{id}
</select>
相关推荐
荒古前21 小时前
Spring Boot + MyBatis 启动报错:不允许有匹配 “[xX][mM][lL]“ 的处理指令目标
spring boot·后端·mybatis
w1225h1 天前
IDEA搭建SpringBoot,MyBatis,Mysql工程项目
spring boot·intellij-idea·mybatis
没有bug.的程序员1 天前
Spring Boot 与 MyBatis-Plus 批量插入的生死狙击
java·spring boot·后端·mybatis·plus·批量插入
小江的记录本1 天前
【泛型】泛型:泛型擦除、通配符、上下界限定
java·windows·spring boot·后端·spring·maven·mybatis
dreamxian2 天前
苍穹外卖day11
java·spring boot·后端·spring·mybatis
cjy0001112 天前
SpringBoot(整合MyBatis + MyBatis-Plus + MyBatisX插件使用)
spring boot·tomcat·mybatis
Zzxy2 天前
MyBatis-Plus入门
java·mybatis
givemeacar3 天前
Spring Boot中集成MyBatis操作数据库详细教程
数据库·spring boot·mybatis
小江的记录本3 天前
【Bean】JavaBean(原生规范)/ Spring Bean 【重点】/ 企业级Bean(EJB/Jakarta Bean)
java·数据库·spring boot·后端·spring·spring cloud·mybatis