目录
快速入门Mybatis(Springboot集成Mybatis)
前言
带着问题学java系列博文之java基础篇。从问题出发,学习java知识。
Hibernate/Mybatis
前面无框架开发后台服务时(《Java EE--无框架开发后台服务》),数据库操作这块,没有借助任何框架,直接就是JDBC编程,整个需求实现下来可以说是痛苦万分。
1.每次操作数据库都要创建数据库连接,执行操作后又要释放连接,存在频繁的数据库连接创建、销毁;造成资源消耗,效率低下;
2.所有的操作都需要编写sql语句,并完成参数和sql语句的拼接,没有与实体类对象关联起来,操作复杂;
3.查询之后,需要遍历ResultSet结果集,完成结果集与对象实体的封装;
4.对于查询结果集、重复查询等没有任何缓存,每一次都需要从数据库查询数据;
5.不支持自动建表、更新表。
上面总结的痛点相信经历过纯手工JDBC编程的都深有体会,为了解决这些问题,所以推出了ORM(Object relationship mapping 对象关系映射)框架,旨在采用池技术统一管理数据库连接session、connection;自动封装对象实体类与数据表之间的关联(实体类与数据表关联,查询结果自动封装为具体对象);对查询结果可配置缓存(一级、二级缓存),对于重复查询可以从缓存中获取,提高效率;全自动的ORM框架还支持自动建表、更新表等。简单的说,有了ORM框架,将会发现数据库编程这块会彻底解放,甚至都不需要程序员去编写sql,一切都由框架准备好了。
下面就带领大家快速上手这两大框架,也经典的后台架构SSM/SSH中的第三个框架。(本博文都是基于SpringBoot(SpringBoot框架可以看成是Spring SpringMVC集合,后续博文会单独讲解),以全注解举例。如果想要详细学习框架,建议还是从配置阶段开始,逐渐过渡到全注解阶段。当然如果只是为了使用它,那直接学习全注解,能上手开发即可)
快速入门Mybatis(Springboot集成Mybatis)
建表语句:
CREATE TABLE `school`.`student` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '学生编号',
`name` VARCHAR(45) NOT NULL COMMENT '姓名',
`age` INT(11) NOT NULL COMMENT '年龄',
`address` VARCHAR(100) NULL COMMENT '居住地址',
PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COMMENT = '学生表';
CREATE TABLE `school`.`grades` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`student_id` INT NOT NULL COMMENT '外键关联学生表',
`math` INT NOT NULL COMMENT '数学成绩',
`chinese` INT NOT NULL COMMENT '语文成绩',
`english` INT NOT NULL COMMENT '英语成绩',
PRIMARY KEY (`id`),
INDEX `student_id_idx` (`student_id` ASC) VISIBLE,
CONSTRAINT `studentId`
FOREIGN KEY (`student_id`)
REFERENCES `school`.`student` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COMMENT = '成绩表';
集成对接:
项目结构图:
1.实体类
@Data
@Table(name = "grades")
public class Grades implements Serializable {
@Id
@KeySql(useGeneratedKeys = true)
private Integer id;
@Column(name = "student_id")
private Integer studentId;
private Integer math;
private Integer chinese;
private Integer english;
}
@Data
@Table(name = "student")
public class Student {
@Id
@KeySql(useGeneratedKeys = true) //mybatis通用mapper的注解,表明自增属性
private Integer id;
private String name;
private Integer age;
private String address;
}
2.mapper接口(依赖通用mapper实现)
public interface StudentMapper extends Mapper<Student> {
}
public interface GradesMapper extends Mapper<Grades> {
}
3.启动类添加注解扫描mapper
@SpringBootApplication
@MapperScan("com.zst.learnmybatis.mapper") //配置要扫描的mapper包名
public class LearnmybatisApplication {
public static void main(String[] args) {
SpringApplication.run(LearnmybatisApplication.class, args);
}
}
4.配置信息
##日志中打印具体的sql语句
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
##指定扫描的实体类包名
mybatis.type-aliases-package=com.zst.learnmybatis.domain
##使用druid数据库连接池
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://127.0.0.1:3306/school?useSSL=true&characterEncoding=utf-8&serverTimezone=GMT
spring.datasource.druid.username=root
spring.datasource.druid.password=***
5.所需依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
至此,springboot集成Mybatis(依赖通用mapper启动器)完毕,这里用了druid连接池,tk.mybatis.mapper。用通用mapper的目的,是为了简化编码,因为通用mapper基本实现了单表操作的所有方法,可以直接调用。
单表增删改查
@SpringBootTest
@RunWith(SpringRunner.class)
class StudentMapperTest {
@Autowired
StudentMapper studentMapper;
//单表增删改查
@Test
public void testInsert(){
Student student = new Student();
student.setName("王五");
student.setAge(18);
student.setAddress("合肥");
studentMapper.insert(student);
}
}
单表增删改查等基本操作,可以直接使用通用mapper的基础方法,是不是非常简便。我们什么都没做,但是框架已经为我们实现所有!
下面梳理一下mapper中基本的增删改查方法:
添加方法:
mapper给我们默认实现了两个方法:insert()和insertSelective();它们分别的意思是:
insert():表示向表中插入一条数据,表列值就是该对象的属性值。如果有自增主键,且对象实例的主键属性也有值,则直接插入,如果对象主键属性没有值,则由数据库根据表数据自动生成。
insertSelective():区别insert(),这个方法仅给属性值不为null的列设定值,如果对象实例属性为null或者未设定值,则不做任何操作,表数据该列值由数据库默认填充。
删除方法:
mapper给我们实现了三个删除方法:deleteByPrimaryKey()、delete()、deleteByExample()
deleteByPrimaryKey():这个方法就如其名,根据主键删除;
delete():这个方法相当于条件删除,即删除表记录中列值符合对象实例属性值的所有记录(对象实例的属性值相当于and拼接的条件);
deleteByExample():这个方法完全就是条件删除,其中传入的example就是mybatis定义的专门用于条件拼接的类。
修改方法:
mapper给我们实现了四个修改方法:updateByPrimaryKey()、updateByExample()、updateByExampleSelective()、updateByPrimaryKeySelective()
updateByPrimaryKey():根据主键更新表记录,实体属性空值也会设置到数据表对应列;
updateByPrimaryKeySelective():根据主键更新表记录,实体属性空值也不会设置到数据表对应列,对应列的值由数据库已有值或者默认填充;
updateByExample():根据条件更新,相当于更新所有符合条件的表记录,实体属性值对应表的列,空值也会设置;
updateByExampleSelective():根据条件更新,相当于更新所有符合条件的表记录,实体属性值对应表的列,空值不会设置,由数据库已有值或者默认填充;
查询方法:
查询方法内置有很多个,分别解释一下意思:
select():将传入的实体类属性值作为条件,进行查询;
selectAll():查询表中所有记录;
selectByExample():根据传入的Example进行条件查询;
selectByRowBounds():将传入的实体类属性值作为条件,拼接上RowBounds的取结果限制(分页),进行查询;这个方法就是为了分页准备的,RowBounds支持两个参数offset和limit;
selectByExampleAndRowBounds():将传入的Example作为条件,拼接上RowBounds的取结果限制(分页),进行查询;(分页方法)
selectByPrimaryKey():根据主键查询;
selectCount():查询记录总数;
selectCountByExample():根据example条件查询,查询记录总数;
selectOne():将传入的实体类属性值作为条件,进行查询;必须确保符合条件的记录仅有一条,否则将会报错;
selectOneByExample():将传入的Example作为条件,进行查询,必须确保符合条件的记录仅有一条,否则将会报错;
单表条件查询
@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
//单表条件查询
@Test
public void testExample(){
Example example = new Example(Student.class);
example.createCriteria().andLike("name","%二%")
.orLike("address","%肥%");
List<Student> students = studentMapper.selectByExample(example);
for (Student student : students) {
System.out.println(student);
}
}
}
如上,使用Example对象,拼接条件,然后进行条件查询。上例演示了查询姓名中含有“二”或者地址中含有“肥”的学生。
调用Example对象的createCriteria()方法,创建一个条件对象后,就可以调用相关方法,进行条件拼接。Example.Criteria对象具体方法如下:
addCriterion()、addOrCriterion() :添加自定义的条件(or 等同于 sql语句的or:或运算连接)
andIsNull()、andIsNotNull():值是空/非空(and 等同于 sql语句的and:与运算连接;类似方法,or连接符)
andEuqalTo()、andNotEqualTo():相等/不相等(类似方法,or连接符)
andLessThan()、andLessThanOrEqualTo():小于/小于等于(类似方法,or连接符)
andIn()、andNotIn():在取值范围内/不在范围内(类似方法,or连接符)
andBetween()、andNotBetween():在取值范围内/不在范围内(类似方法,or连接符)
andLike()、andNotLike():含有/不含有条件值(类似方法,or连接符)
也可以直接用Example的方法进行条件拼接:
@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
//单表条件查询
@Test
public void testExample(){
Example example = Example.builder(Student.class).andWhere(Sqls.custom().andLike("name","%二%")
.orLike("address","%肥%")).orderByDesc("id").build();
studentMapper.selectByExample(example);
}
}
如上还加上了排序,演示了查询姓名中含有“二”或者地址中含有“肥”的学生,并按照id降序输出。
单表条件查询Example中的方法涵盖了所有情形吗?如果有些没有涵盖怎么办呢?
其实单表查询,使用Example基本可以满足所有场景了。如果真的遇到Example或者Example.Criteria中没有的,也可以通过手写sql来实现,范例如下:
public interface StudentMapper extends Mapper<Student> {
@Select("select * from student where name like #{name} or address like #{address} order by id desc")
List<Student> likeNameOrAddressOrderByIdDesc(String name,String address);
}
即只需要在Mapper接口中增加方法,编写sql语句就好。上例的方法和使用Example查询效果完全一致。
***:#{param}和${param}的区别是?
上例编写sql语句时,参数设定这块用了一个符号表达式#{name},它的意思是这里使用参数name。而mybatis还有另外一种符号表达式:${param}
如果上例使用这个表达式的话,具体如下:
select * from student where name like '%${name}%' or address like '%${address}%' order by id desc
可以看到${param}表达式,相当于字符串占位符,仅仅是将参数作为字符串填充进sql语句。
从这两个sql语句可以看出来,很明显使用#{param}更好一点,原因很简单:使用#{param}相当于使用了PrepareStatement,向其中添加参数,执行效率更高,而且没有sql注入漏洞;而使用${param}相当于使用了statement,只是一个字符串填充,执行效率低,且存在sql注入漏洞。因此推荐使用#{param}表达式,不过要注意,像本例中like查询的时候,需要对传参name手动加上“%%”,然后再作为方法传参。
***:查询结果是自定义类型,怎么办?
mapper中自带的方法返回类型都是实体类或者实体类集合等,如果我们要求返回自定义类型,该怎么办呢?显然此时就无法使用mapper自带的方法了,需要我们在mapper接口类中编写sql,自定义返回类型,如下例:
public interface StudentMapper extends Mapper<Student> {
@Select("select name,age,address from student where id = #{id}")
@ResultType(StudentDto.class) //可以使用注解表明返回类型,也可以省略不写
StudentDto queryByPrimaryKey(Integer id);
}
/**
* 自定义返回类型实体类
* 注意属性名要和查询语句返回的列名一一对应,否则将无法转化
* 本质:框架替我们封装了返回结果而已
* ResultSet.getString("name")->name属性值
* ResultSet.getString("address")->address属性值
* ResultSet.getInt("age")->age属性值
*/
@Data
@ToString
public class StudentDto {
private String name;
private Integer age;
private String address;
}
@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
//自定义类型
@Test
public void testDefine(){
StudentDto studentDto = studentMapper.queryByPrimaryKey(1);
System.out.println(studentDto.toString());
}
}
多表联合查询
通用mapper只有当前类对应的单表操作系列方法,如果要实现多表联合查询,则必须在mapper接口类中增加相应方法,编写sql语句。如下例:
@Data
@ToString
public class StudentScoreDto {
private String name;
private Integer math;
private Integer chinese;
private Integer english;
}
public interface StudentMapper extends Mapper<Student> {
@Select("select s.name,g.math,g.chinese,g.english from student s left join grades g on s.id = g.student_id where s.id = #{id}")
StudentScoreDto queryStudentScoreById(Integer studentId);
}
@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
//多表查询
@Test
public void testMulti(){
StudentScoreDto studentScoreDto = studentMapper.queryStudentScoreById(2);
System.out.println(studentScoreDto.toString());
}
}
本例演示了查询id为2的学生的姓名以及各科的分数,用到了自定义类型,以及编写sql语句(左连查询)。
分页查询
上面单表查询时分析过,mapper给我们写好了方法,支持分页查询,如下例:
@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
//分页查询
@Test
public void testPageQuery(){
List<Student> students = studentMapper.selectByRowBounds(null, new RowBounds(3, 5));
for (Student student : students) {
System.out.println(student);
}
}
}
从上图可以看到其实还是查询了所有数据,只是在返回数据时,框架给我们做了数据筛除。其它的借助RowBounds对象实现分页查询方法也类似。另外使用这个方法有个缺陷,那就是需要我们每次计算出offset(偏移量),然后作为参数出入方法。显然,这种查询效率低,还需要我们每次都计算偏移量的分页方式很不友好。为此,业内出了各种帮助分页查询的小插件框架,比较著名的有PageHelper,下面以PageHelper举例:
@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
//分页查询
@Test
public void testPageQueryByHelper(){
//开启分页
PageHelper.startPage(2,5);
List<Student> students = studentMapper.selectAll();
PageInfo<Student> pageInfo = new PageInfo<>(students);
for (Student student : students) {
System.out.println(student);
}
System.out.format("total:%s,pageSize:%s,pageNum:%s",pageInfo.getTotal(),pageInfo.getPageSize(),pageInfo.getPageNum());
System.out.println();
for (Student student : pageInfo.getList()) {
System.out.println(student);
}
}
}
可以看到,使用PageHelper进行分页查询,虽然看起来还是调用mapper的selectAll()查询所有,但其实是由PageHelper接管了sql语句,分别执行了两次sql查询,首先查询出记录总数,然后PageHelper会根据传入的参数(PageNum 页码,PageSize 每页记录数)计算出偏移量,作为sql查询 limit 语句后的参数填入。进行第二次查询,获得分页数据。PageHelper虽然分为两次查询,但是两次都是小数据量返回,对于表数据非常多的情形,很明显比一次查询所有记录效率要高得多,且占用内存也更小。因此推荐使用PageHelper进行分页查询,不建议使用mapper自带的方法。
以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!