下来我们进入数据持久化的部分,对于一个真实的业务系统,能够正常的运转离不开数据的持久化。在数据持久化这块,目前主流的还是关系型数据库(RDBMS),NoSQL(NewSQL)也有了长足发展,特别在大数据领域。
JDBC
数据持久化这块,javaEE推出了JDBC规范,基于JDBC规范,只要不同的数据库支持对应协议,业务系统就能和数据库服务器(MySQL、Oracle、DB2等)交互
JPA
JPA(Java Persistence API)用于对象持久化的 API,是 Java EE 5.0 平台标准的 ORM 规范,使得应用程序以统一的方式访问持久层。
Hibernate 是符合 JPA 规范的,而 MyBatis 却不符合,因为 MyBatis 还是需要写 SQL 的
MyBatis
MyBatis是一个不屏蔽SQL且提供动态SQL、接口式编程和简易SQL绑定POJO的半自动化框架,它的使用十分简单,而且能非常容易定制SQL,以提高网站性能,因此在移动互联网兴起的时代,它占据了强势的地位。
MyBatis的基本概念:数据库表对应的java对象(POJO)、Mapper(映射器,一些绑定映射语句的接口)、映射语句(XML和注解两种方式,过去XML方式更为流行)、Mybatis配置
- 数据表结构
CREATE TABLE `tz_user` (
`user_id` varchar(36) NOT NULL DEFAULT '' COMMENT 'ID',
`nick_name` varchar(50) DEFAULT NULL COMMENT '用户昵称',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`user_mail` varchar(100) DEFAULT NULL COMMENT '用户邮箱',
`login_password` varchar(255) DEFAULT NULL COMMENT '登录密码',
`pay_password` varchar(50) DEFAULT NULL COMMENT '支付密码',
`user_mobile` varchar(50) DEFAULT NULL COMMENT '手机号码',
`modify_time` datetime NOT NULL COMMENT '修改时间',
`user_regtime` datetime NOT NULL COMMENT '注册时间',
`user_regip` varchar(50) DEFAULT NULL COMMENT '注册IP',
`user_lasttime` datetime DEFAULT NULL COMMENT '最后登录时间',
`user_lastip` varchar(50) DEFAULT NULL COMMENT '最后登录IP',
`user_memo` varchar(500) DEFAULT NULL COMMENT '备注',
`sex` char(1) DEFAULT 'M' COMMENT 'M(男) or F(女)',
`birth_date` char(10) DEFAULT NULL COMMENT '例如:2009-11-27',
`pic` varchar(255) DEFAULT NULL COMMENT '头像图片路径',
`status` int(1) NOT NULL DEFAULT '1' COMMENT '状态 1 正常 0 无效',
`score` int(11) DEFAULT NULL COMMENT '用户积分',
PRIMARY KEY (`user_id`),
UNIQUE KEY `ud_user_mail` (`user_mail`),
UNIQUE KEY `ud_user_unique_mobile` (`user_mobile`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
- 实体类(示例)
@Data
public class User implements Serializable {
private String userId;//id
private String realName;//用户名
private String loginPassword;//密码
}
这是实体对象,和数据表字段一一对应(并且属性名称和数据表字段名称存在规则映射,例如:user_id 为 userId)
这里也体现了”约定“的威力,基于约定很多事情处理都变得异常简单
- Mapper XML
这是Mybatis中的核心:
因为SQL命名规范(多以”_“连接)和java(多以驼峰)命名规范不一致,所以通过resultMap标签来做java属性和数据表字段的对应关系;
关于映射,Mybatis提供了三种处理方式:
1、如上代码所示,手动指定(麻烦);
2、SQL语句设置别名(稍显麻烦),例如:SELECT user_id as userId, real_name as realName
3、设置开启驼峰命名
map-underscore-to-camel-case: true(基于“约定”的处理);
select标签中的resultMap属性,描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素,它和resultMap标签中的id属性保持一致;
sql标签,可被其它语句引用的可重用语句块; insert标签,映射插入语句; update标签,映射更新语句; delete标签,映射删除语句; select标签,映射查询语句;
对应SQL的CRUD
- Mapper接口
@Mapper
public interface UserMapper {
public List findAll();
}
- MyBatis配置
- 使用Spring Boot,Mybatis配置非常简单
- #mybatis的相关配置
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.easycloud.daomall.show.model
#开启驼峰命名
configuration:
map-underscore-to-camel-case: true - 其中type-aliases-package指定实体类所在的包,这样在设置”type、parameterType、resultType“属性值时可只用类名。
- api暴露
然后按照前面讲的Spring RESTful service暴露,就可以查看访问数据库的效果了
MyBatis-Plus
https://baomidou.com
在实际的业务编写中,你会发现要写大量的CRUD Mapper XML和对应的接口定义,但其实对于数据表的操作基本可以抽象为CRUD(包括分页和高级检索)这几种类型,MyBatis-Plus就应运而生。
使用MyBatis-Plus后,我们看看可以省略哪些代码:
- application.yml中的MyBatis配置
- Mapper XML文件
- Mapper java接口中的方法定义,然后继承BaseMapper
- public interface UserMapper extends BaseMapper<User> {
- 内置方法说明参见:https://baomidou.com/pages/49cc81/
- 调用Mapper的地方改为selectList(null)
- 实体类名和数据表名不存在规则一致性的话,需要加上TableName注解(例如我们的代码示例)
- @Data
@TableName("tz_user")
public class User implements Serializable { - MyBatis-Plus处处体现了”约定优于配置“的原则
分页实现
分页插件,在实际的业务中,分页是一个常见的场景,使用MyBatis-Plus分页,需要启用分分页插件
@Configuration
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
分页代码
IPage page = new Page(start, rows);
return userMapper.selectPage(page,null).getRecords();
其中Page对象,第一个参数表示是第几页,第二个参数表示每页有几条数据
条件构造
https://baomidou.com/pages/10c804/
MyBatis-Plus中使用Wrapper实现,支持查询、更新的where条件。使用Lambda 表达式更加的简洁。
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(User::getUserId, "111");
上面就是查询userId为111的用户
其它特性
作为一个可扩展的设计,数据表ID不应该使用数据库自增ID(例如:分库分表就会出现ID重复问题),所以MyBatis-Plus提供了ID生成的增强
- 注解使用ID生成策略
@TableId()
private String userId;
不做任何设置,使用其默认内置的雪花算法
它还支持其他几种模式,非必要使用默认方式足矣
ASSIGN_ID(雪花算法) 如果不设置类型值,默认则使用IdType.ASSIGN_ID策略(自3.3.0起)。该策略会使用雪花算法自动生成主键ID,主键类型为长或字符串(分别对应的MySQL的表字段为BIGINT和VARCHAR)
ASSIGN_UUID(排除中划线的UUID) 如果使用IdType.ASSIGN_UUID策略,并重新自动生成排除中划线的UUID作为主键。主键类型为String,对应MySQL的表分段为VARCHAR(32)
AUTO(数据库ID自增)
INPUT(插入前自行设置主键值)
无(无状态) 如果使用IdType.NONE策略,表示未设置主键类型(注解里等于跟随上下,左右里约等于INPUT)
策略全局设置
mybatis-plus.global-config.db-config.id-type=值
- 调用
User user = new User();
user.setUserRegtime(LocalDateTime.now());
user.setModifyTime(LocalDateTime.now());
userService.save(user);
其他前置知识
Java
Lambda
我们知道java8是一个大改动的版本,也让java的能力有了更进一步的提升,并且很多新特性应用广泛,Lambda表达式就是其中之一,在MyBatis章节我们就有使用Lambda表达式,第一感觉就是书写更加的方便,非常的强大。下来我们就对Lambda表达式做一定的了解。
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。
Lambda 表达式的语法格式如下:
(parameters) -> expression
或
(parameters) ->{ statements; }
Lambda 表达式的简单例子:
// 1. 不需要参数,返回值为 5
() -> 5
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y
// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)
在 LambdaTester.java 文件输入以下代码:
package com.easycloud.javacase.lambda;
public class LambdaShow {
public static void main(String args[]){
LambdaShow tester = new LambdaShow();
// 类型声明
MathOperation addition = (int a, int b) -> a + b;
// 不用类型声明
MathOperation subtraction = (a, b) -> a - b;
// 大括号中的返回语句
MathOperation multiplication = (int a, int b) -> { return a * b; };
// 没有大括号及返回语句
MathOperation division = (int a, int b) -> a / b;
System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
System.out.println("10 / 5 = " + tester.operate(10, 5, division));
// 不用括号
GreetingService greetService1 = message ->
System.out.println("Hello " + message);
// 用括号
GreetingService greetService2 = (message) ->
System.out.println("Hello " + message);
greetService1.sayMessage("Runoob");
greetService2.sayMessage("Google");
}
interface MathOperation {
int operation(int a, int b);
}
interface GreetingService {
void sayMessage(String message);
}
private int operate(int a, int b, MathOperation mathOperation){
return mathOperation.operation(a, b);
}
}
使用 Lambda 表达式需要注意以下两点:
- Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。在上面例子中,我们使用各种类型的Lambda表达式来定义MathOperation接口的方法。然后我们定义了sayMessage的执行。
- Lambda 表达式免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。
我们来看一下,引入Lambda表达式对以前以前繁琐写法的改进,
例如常见的排序算法:
java8以前:
// 使用 java 7 排序
private void sortUsingJava7(List names){
Collections.sort(names, new Comparator() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
}
java8的Lambda:
// 使用 java 8 排序
private void sortUsingJava8(List names){
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
}
再例如(多线程编程):
java8以前:
Runnable r2 = new Runnable(){
public void run(){
System.out.println("Hello World 2");
}
};
//执行Runnable方法
public static void process(Runnable r){
r.run();
}
//打印 "Hello World 1"
process(r1);
//打印 "Hello World 2"
process(r2);
java8的Lambda:
Runnable r1 = () -> System.out.println("Hello World 1");
Runnable r2 = () -> System.out.println("Hello World 2");
另外我们在Mybatis的QueryWrapper中使用了”方法引用“,用一对冒号::代表方法引用,使语言调用更加紧凑简洁,减少冗余代码。
queryWrapper.lambda().eq(User::getUserId, "111")
和Lambda配合使用的场景很多,上面演示的接口sayMessage()和Runnable接口,都是函数式接口(Functional Interface)
函数式接口
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为 lambda 表达式。
Lambda 表达式和方法引用(实际上也可认为是Lambda表达式)上。
JDK 1.8 之前已有的函数式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
JDK 1.8 新增加的函数接口:
- java.util.function
我们再来看一个具体的示例:
Predicate
该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)。
该接口用于测试对象是 true 或 false。
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class Java8Tester {
public static void main(String args[]){
List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
// Predicate predicate = n -> true
// n 是一个参数传递到 Predicate 接口的 test 方法
// n 如果存在则 test 方法返回 true
System.out.println("输出所有数据:");
// 传递参数 n
eval(list, n->true);
// Predicate predicate1 = n -> n%2 == 0
// n 是一个参数传递到 Predicate 接口的 test 方法
// 如果 n%2 为 0 test 方法返回 true
System.out.println("输出所有偶数:");
eval(list, n-> n%2 == 0 );
// Predicate predicate2 = n -> n > 3
// n 是一个参数传递到 Predicate 接口的 test 方法
// 如果 n 大于 3 test 方法返回 true
System.out.println("输出大于 3 的所有数字:");
eval(list, n-> n > 3 );
}
public static void eval(List list, Predicate predicate) {
for(Integer n: list) {
if(predicate.test(n)) {
System.out.println(n + " ");
}
}
}
}