【JavaEE进阶】MyBatis 操作数据库(2)

欢迎关注个人主页:逸狼


创造不易,可以点点赞吗

如有错误,欢迎指出~


MyBatisXML配置⽂件

Mybatis的开发有两种⽅式: 1. 注解 2. XML 前⾯学习了注解的⽅式,接下来我们学习XML的⽅式 使⽤Mybatis的注解⽅式,主要是来完成⼀些简单的增删改查功能.如果需要实现复杂的SQL功能,建 议使⽤XML来配置映射语句,也就是将SQL语句写在XML配置⽂件中.

MyBatisXML的⽅式需要以下两步:

1. 配置数据库连接字符串和MyBatis

2. 写持久层代码

配置连接字符串和MyBatis

此步骤需要进⾏两项设置,数据库连接字符串设置和MyBatis的XML⽂件配置。

# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件 
mybatis.mapper-locations=classpath:mapper/**Mapper.xml

写持久层代码

持久层代码分两部分 1. ⽅法定义Interface 2. ⽅法实现:XXX.xml

添加mapper接⼝

数据持久层的接⼝定义: 

package com.example.demo;

import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserInfoXMLMapper {
    List<UserInfo> selectAll();
}

添加UserInfoMapper.xml

数据持久成的实现,MyBatis的固定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="selectAll" resultType="com.example.demo.model.UserInfo">
        select * from user_info
    </select>

</mapper>

以下是对以上标签的说明:

  • <mapper>标签:需要指定namespace 属性,表⽰命名空间,值为mapper接⼝的全限定 名,包括全包名.类名
  • <select>查询标签:是⽤来执⾏数据库的查询操作的:
    • id :是和Interface (接⼝)中定义的⽅法名称⼀样的,表⽰对接⼝的具体实现⽅法。
    •  resultType :是返回的数据类型,也就是开头我们定义的实体类 


 测试

package com.example.demo.mapper;

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 UserInfoXMLMapperTest {

    @Autowired
    private UserInfoXMLMapper userInfoXMLMapper;

    @Test
    void selectAll() {
        userInfoXMLMapper.selectAll().forEach(x -> System.out.println(x));
    }
}

使用xml的方式增删改查操作

接下来,我们来实现⼀下⽤⼾的增加、删除和修改的操作.

增(Insert)

UserInfoMapper接⼝:

    Integer insert(UserInfo userInfo);

UserInfoMapper.xml实现: 

    <insert id="insert">
        insert into user_info(username, `password`, age, gender, phone)
        values(#{username}, #{password}, #{age}, #{gender}, #{phone})
    </insert>

如果使⽤@Param设置参数名称的话,使⽤⽅法和注解类似

返回⾃增id

接⼝定义不变,Mapper.xml实现设置useGeneratedKeys和keyProperty属性

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into user_info(username, `password`, age, gender, phone)
        values(#{username}, #{password}, #{age}, #{gender}, #{phone})
    </insert>

删(Delete)

UserInfoMapper接⼝:

   Integer delete(Integer id);

UserInfoMapper.xml实现: 

    <delete id="delete">
        delete from user_info where id = #{id}
    </delete>

改(Update)

UserInfoMapper接⼝:

    Integer update(UserInfo userInfo);

UserInfoMapper.xml实现: 

    <update id="update">
        update user_info set username = #{username} where id = #{id}
    </update>

查(Select)

同样的,使⽤XML的⽅式进⾏查询,也存在数据封装的问题 我们把SQL语句进⾏简单修改,查询更多的字段内容

解决办法和注解类似: 1. 起别名 2. 结果映射 3. 开启驼峰命名

其中1,3的解决办法和注解⼀样

接下来看下xml如果来写结果映射

    <resultMap id="BeanMap" type="com.example.demo.model.UserInfo" >
        <result column="delete_flag" property="deleteFlag"></result>
        <result column="create_time" property="createTime"></result>
        <result column="update_time" property="updateTime"></result>
    </resultMap>
    
    <select id="selectAll" resultMap="BeanMap">
        select * from user_info
    </select>

一个xml可以定义多个resultMap,注解也是同样,id不能重复,建议把字段写全(否则可能会出现问题) 

select username, `password`, age, gender, phone from userinfo where username='' or 1='1'

多表查询

多表查询和单表查询类似,只是SQL不同⽽已 5.1.1 准备⼯作 上⾯建了⼀张⽤⼾表,我们再来建⼀张⽂章表,进⾏多表关联查询. ⽂章表的uid,对应⽤⼾表的id.

对应Model:

package com.example.demo.model;


import lombok.Data;

import java.util.Date;

@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;
}

查询的sql语句 

SELECT ta.id,ta.title,ta.content,ta.uid,tb.username,tb.age,tb.gender 
FROM article_info ta LEFT JOIN user_info tb ON ta.uid = tb.id WHERE ta.id = 1

 对应的接口

package com.example.demo.mapper;

import com.example.demo.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface ArticleMapper {

    @Select("SELECT ta.id,ta.title,ta.content,ta.uid,tb.username,tb.age,tb.gender " +
            "FROM article_info ta LEFT JOIN user_info tb ON ta.uid = tb.id WHERE ta.id = #{id}")
    ArticleInfo selectById(Integer id);
}

如果名称不⼀致的,采⽤ResultMap,或者别名的⽅式解决,和单表查询⼀样 Mybatis不分单表还是多表,主要就是三部分:SQL,映射关系和实体类 

测试

package com.example.demo.mapper;

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 selectById() {
        System.out.println(articleMapper.selectById(1).toString());
    }
}

 测试结果

这里where子句中的条件username='' or 1='1' ,由于1='1'恒成立,所以不管用户名是什么,该条件始终为真。这就导致原本应该根据特定用户名查询的操作,变成了查询表中的所有记录,结果返回了 7 条用户信息记录,展示了用户名、密码、年龄、性别和电话号码等字段。

#{}和${}

MyBatis参数赋值有两种⽅式,咱们前⾯使⽤了#{} 进⾏赋值,接下来我们看下⼆者的区别

#{}和${}使⽤对比

 Interger类型的参数

    @Select("select * from user_info where id = #{id};")
    List<UserInfo> selectById(@Param("id") Integer userId);

我们输⼊的参数并没有在后⾯拼接,id的值是使⽤? 进⾏占位.这种SQL我们称之为"预编译SQL" 

我们把 #{} 改成 ${} 再观察打印的⽇志:

    @Select("select * from user_info where id = ${id};")
    List<UserInfo> selectById(@Param("id") Integer userId);


这次的参数是直接拼接在SQL语句中了.

String类型的参数

    @Select("select * from user_info where username = #{name};")
    List<UserInfo> selectByName(String name);

我们把 #{} 改成 ${} 再观察打印的⽇志:

可以看到,这次的参数依然是直接拼接在SQL语句中了,但是字符串作为参数时,需要添加引号 '' ,使 ⽤ ${} 不会拼接引号 '' ,导致程序报错.

修改代码如下

    @Select("select * from user_info where username = '${name}';")
    List<UserInfo> selectByName(String name);

 结果测试成功

从上⾯两个例⼦可以看出:

  • #{} 使⽤的是预编译SQL,通过? 占位的⽅式,提前对SQL进⾏编译,然后把参数填充到SQL语句 中.#{} 会根据参数类型,⾃动拼接引号'' .
  • ${} 会直接进⾏字符替换,⼀起对SQL进⾏编译.如果参数为字符串,需要加上引号 '' . 参数为数字类型时,也可以加上,查询结果不变,但是可能会导致索引失效,性能下降.

#{}和${}区别

#{}和${}的区别就是预编译SQL和即时SQL的区别.

当用⼾发送⼀条SQL语句给服务器后,⼤致流程如下:

1. 解析语法和语义,校验SQL语句是否正确

2. 优化SQL语句,制定执⾏计划

3. 执⾏并返回结果 ⼀条SQL如果⾛上述流程处理,我们称之为ImmediateStatements(即时SQL)

绝⼤多数情况下,某⼀条SQL语句可能会被反复调⽤执⾏,或者每次执⾏的时候只有个别的值不同(⽐ 如select的where⼦句值不同,update的set⼦句值不同,insert的values值不同).如果每次都需要 经过上⾯的语法解析,SQL优化、SQL编译等,则效率就明显不⾏了.

预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来,后⾯再次执⾏这条语句时,不会再次编译 (只是输⼊的参数不同),省去了解析优化等过程,以此来提⾼效率

 更安全(防⽌SQL注⼊)

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

sql注⼊代码: ' or 1='1

可以看出来,查询的数据并不是⾃⼰想要的数据.所以⽤于查询的字段,尽量使⽤#{} 预查询的⽅式 SQL注⼊是⼀种⾮常常⻅的数据库攻击⼿段,SQL注⼊漏洞也是⽹络世界中最普遍的漏洞之⼀. 如果发⽣在⽤⼾登录的场景中,密码输⼊为 ' or 1='1 ,就可能完成登录(不是⼀定会发⽣的场景, 需要看登录代码如何写)

${}的使用场景

排序功能

使⽤ ${sort} 可以实现排序查询,⽽使⽤#{sort} 就不能实现排序查询了.

注意:此处sort参数为String类型,但是SQL语句中,排序规则是不需要加引号 '' 的,所以此时的 ${sort} 也不加引号

    @Select("select * from user_info order by id ${order}")
    List<UserInfo> selectByOrder(String order);

我们把${} 改成#{}

可以发现,当使⽤#{sort} 查询时,asc前后⾃动给加了引号,导致sql错误 #{} 会根据参数类型判断是否拼接引号 '' 如果参数类型为String,就会加上引号 

like查询

like使⽤#{}报错 

@Select("select id, username, age, gender, phone, delete_flag, create_time, 
update_time " +
 "from user_info where username like '%#{key}%' ")
List<UserInfo> queryAllUserByLike(String key);

把#{}改成${}可以正确查出来,但是${}存在SQL注⼊的问题,所以不能直接使⽤${}. 解决办法:使⽤mysql的内置函数concat()来处理,实现代码如下:

@Select("select id, username, age, gender, phone, delete_flag, create_time, update_time " 
+ "from user_info where username like concat('%',#{key},'%')")
List<UserInfo> queryAllUserByLike(String key);

数据库连接池

在上⾯Mybatis的讲解中,我们使⽤了数据库连接池技术,避免频繁的创建连接,销毁连接 

数据库连接池负责分配、管理和释放数据库连接,它允许应⽤程序重复使⽤⼀个现有的数据库连接, ⽽不是再重新建⽴⼀个.

  • 没有使⽤数据库连接池的情况:每次执⾏SQL语句,要先创建⼀个新的连接对象,然后执⾏SQL语句,SQL 语句执⾏完,再关闭连接对象释放资源.这种重复的创建连接,销毁连接⽐较消耗资源
  • 使⽤数据库连接池的情况:程序启动时,会在数据库连接池中创建⼀定数量的Connection对象,当客⼾ 请求数据库连接池,会从数据库连接池中获取Connection对象,然后执⾏SQL,SQL语句执⾏完,再把 Connection归还给连接池.

优点: 1. 减少了⽹络开销 2. 资源重⽤ 3. 提升了系统的性能

使⽤

常⻅的数据库连接池: • C3P0 • DBCP • Druid • Hikari

⽬前⽐较流⾏的是Hikari,Druid

Hikari:SpringBoot默认使⽤的数据库连接池

Druid: 如果我们想把默认的数据库连接池切换为Druid数据库连接池,只需要引⼊相关依赖即可

<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>druid-spring-boot-3-starter</artifactId>
 <version>1.2.21</version>
</dependency>
  1. Druid连接池是阿⾥巴巴开源的数据库连接池项⽬
  2. 功能强⼤,性能优秀,是Java语⾔最好的数据库连接池之⼀

MySQL开发企业规范总结

1. 表名,字段名使⽤⼩写字⺟或数字,单词之间以下划线分割.尽量避免出现数字开头或者两个下划线 中间只出现数字.数据库字段名的修改代价很⼤,所以字段名称需要慎重考虑。

MySQL在Windows下不区分⼤⼩写,但在Linux下默认是区分⼤⼩写.因此,数据库名,表名,字 段名都不允许出现任何⼤写字⺟,避免节外⽣枝

  • 正例:aliyun_admin,rdc_config,level3_name
  • 反例:AliyunAdmin,rdcConfig,level_3_name

2.表必备三字段:id,create_time,update_time

id必为主键,类型为bigintunsigned,单表时⾃增,步⻓为1 create_time,update_time的类型均为datetime类型,create_time表⽰创建时间, update_time表⽰更新时间 有同等含义的字段即可,字段名不做强制要求

3.在表查询中,避免使⽤*作为查询的字段列表,标明需要哪些字段.

  • 1. 增加查询分析器解析成本
  • 2. 增减字段容易与resultMap配置不⼀致
  • 3. ⽆⽤字段增加⽹络消耗,尤其是text类型的字段