如何优雅地将每月新增上亿记录的大表在MySQL中进行分表操作,让解决方案实际落地?
引言
本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前面《分库分表的正确姿势》、《分库分表的后患问题》两篇中,对数据库的分库分表技术进行了全面阐述,但前两篇大多属于方法论,并不存在具体的实战实操,而只有理论没有实践的技术永远都属纸上谈兵,所以接下来会再开几个单章对分库分表各类方案进行落地。
分库分表实战内容基本上很少有人去分享,在网上能够搜出来的也大多属于一些方法论,但大部分技术开发真正缺少的恰恰是这些实操经验,所以后续的内容多以实践为主,携手诸位真正彻底悟透分库分表相关的技术。
尤其是对于库内分表这块的分享,当你去搜索单表数据增长过快该如何处理时,一般都会推荐你做分表处理,但你几乎找不到较为全面的实操教学,网上讲述分表技术更多是停留在表面的理论概念层次做阐述,而本章中则会结合自身之前接触的一个项目业务,再对库内分表技术进行全面阐述~
PS:虽然当时负责的项目并未达到月增上亿条数据的规模,但处理这种单表数据过大的方案都是一致的,将本文看完最后,无论单月数据增长多少,几百万条、几千万条、甚至几亿条....,相信诸位都能具备处理这类业务的能力!
一、源自于软硬结合的特殊业务
在讲本次主题之前,先来聊聊之前碰到的这个业务,这个业务比较特殊,相信很多小伙伴从未碰到过,这种业务本身用户量不大,甚至可以说用户量非常非常少,因为业务的起源来自于一款硬件设备,但具体的设备类型由于某些缘故就不透露了,可以理解成是下面这个东东:
虽然当时的硬件设备并不是这个,但也和它很类似,相信大家但凡在超市购过物都认识它,也就是超市收银台的收银机,当时我们是对外提供了一千台设备,这种设备通常一台只有一个用户,所以当时整个系统上线后所有的用户加起来,涵盖后台管理员、超级管理员账号在内,也不过1200
个用户,这个用户规模相较于常见业务而言属实不多。
而当时我们需要负责的就是:为这些设备开发一个操作系统,这里不是指
Windows、Linux、Mac
这类嵌入式的底层系统,而是给机器的操作员开发一个操作界面,就类似于诸位在超市购物时,超市收银员用手操作的那个界面。
因为这些机器本身会安装一个带UI
的系统,里面也支持安装一些软件,我们的软件会以GUI
的形式嵌入这些设备,当时我要干的就是直接开发API
接口,然后提供给GUI
界面界面调用。本质上就属一个前后端分离的项目,只不过前端从原本的Web
界面变成了GUI
界面。
大家听起来这个项目是不是特别容易完成,用户量又少代表不需要考虑并发,也不会存在太大的流量冲击,性能要求也不会太高,似乎就是一个简简单单的单体增删改查项目呀?但事情远没有表面这么简单,诸位请接着往下看。
1.1、项目的难点
起初当我收到通知要负责这个需求时,从表面浅显的想了一下,似乎发现也不是太难,就是一个单体项目的CRUD
工作,以我这手出神入化的CV
大法,Hlod
住它简直轻轻松松,因此当时也没想太多就直接接手了,项目初期由于团队每位成员经验都很丰富,各自凭借着个人的Copy
神功,项目的开发进度可谓是一骑千里,但慢慢的问题来了,而且这个问题还不小!
当时大概对外预计分发
1000
台机器,每台机器正式投入运营后,预估单日会产生500~600
条数据的产出,套到前面的举例中,也就是大概会向几百个超市投放共计1000
台收银机,每个收银台平均下来之后,大概单日内会有500~600
个顾客结账!
这里咱们做个数学题:现在有1000
台机器,每台机器单日就算产生500
条数据:1000 * 500 = 500000
,这也就意味着单日的账单表中会新增50W
条流水数据,单月整个账单表的数据增长量为:50W * 30 = 1500W
!
单月数据增长
1500W
的概念不言而喻,这也就代表着一年的数据增长量为1500W * 12 = 1.8E
,这批机器投入后预估最少会运行三年起步,甚至十年乃至更久,同时第一批次就要投入1000
台,后面可能还会有第二批次、第三批次.....的投入。
50W
只是最低的账单流水数据量,后续正式运营后可能数据量更大,此时架构的设计就成了难题!
1.2、方案的探讨
基本上当时团队的成员中,没人在此之前碰过这类需求,因此开了一个研讨会,去决定该如何将具体的方案落地,这里有人也许会说,数据量这么大,快上分布式/微服务啊!但实则解决不了这个问题,Why
?因为项目整体的用户量并不大,最多同一时刻也才1000
并发请求,就算这个并发量再增大几个级别,这里用单体架构优化好了也能够抗住,所以问题并不在业务系统的架构上面,而是在数据落库这方面。
这里直接用分库可以吗?答案是也不行,
Why
?因为整个项目中只有账单表才有这么大的数据量,其他的用户表、系统表、功能菜单表、后台表......,基本上不会有太大的数据量,所以直接做分库也没必要,属实有些浪费资源。
有小伙伴可能想到了!在之前的文章中好像聊过《MySQL的表分区技术》,这里可以按月份对流水表做分区呀!乍一听似乎像那么一回事,但依旧不行,因为第一批机器投入后,单月预计就会产生1500W
条数据,后续可能会增加机器数量,因此单月的数据量达到2000W、3000W.....
都有可能,如果按月做表分区,每个分区里面都有几千万条数据,一张账单表的流水随着时间推移,数据量甚至会达到几十亿!
一张表中存储几十亿条数据,这基本上不现实,虽然
InnoDB
在数据页为16KB
尺寸下,单表最多能存储64TB
数据,有可能这几十亿条数据真的能存下去,但查询时的性能简直令人头大,并且最关键的是不方便后续对数据做维护、管理、备份和迁移工作。
因此经过一番探讨后,最后决定选择了表分区技术的进阶版实现,即单库内做水平分表,按月份对数据做分表,也就是将账单表分为month_bills_202210、month_bills_202211、month_bills_202212.......
以月份结尾的多张表,每个月的账单流水数据最终都会插入到各自的月份表中。
最终架构定型为:业务系统使用单体架构 + 数据库使用单库 + 流水表按月份做水平分表。
二、按月分表方案的落地实践
在上一阶段中已经决定好了具体的方案,但又该如何将方案落地呢?首先咱们先把方案落地的思路捋清楚:
- ①能够自动按月创建一张月份账单表,从而将每月的流水数据写入进去。
- ②写入数据时,能够根据当前的日期,选择对应的月份账单表并插入数据。
实现了上面两个需求后,整个方案近乎落地了一半,但接下来该如何去实现相应功能呢?咱们一点点来动手实现。
2.1、利用存储过程实现按月动态创建表
创建表的SQL
语句大家都不陌生,按月份创建表之前,自然也需要一份原生创建表的DDL
语句,如下:
CREATE TABLE `month_bills_202211` (
`month_bills_id` int(8) NOT NULL AUTO_INCREMENT COMMENT '账单ID',
`serial_number` varchar(50) NOT NULL COMMENT '流水号',
`bills_info` text NOT NULL COMMENT '账单详情',
`pay_money` decimal(10,3) NOT NULL COMMENT '支付金额',
`machine_serial_no` varchar(20) NOT NULL COMMENT '收银机器',
`bill_date` timestamp NOT NULL COMMENT '账单日期',
`bill_comment` varchar(100) NULL DEFAULT '无' COMMENT '账单备注',
PRIMARY KEY (`month_bills_id`) USING BTREE,
UNIQUE `serial_number` (`serial_number`),
KEY `bill_date` (`bill_date`)
)
ENGINE = InnoDB
CHARACTER SET = utf8
COLLATE = utf8_general_ci
ROW_FORMAT = Compact;
上述的语句会创建一张月份账单表,这张表主要包含七个字段,如下:
字段 | 简介 | 描述 |
---|---|---|
month_bills_id |
月份账单ID | 主要作为月份账单表的主键字段 |
serial_number |
流水号 | 所有账单流水数据的唯一流水号 |
bills_info |
账单详情 | 顾客本次订单中,购买的所有商品详情数据 |
pay_money |
支付金额 | 本次顾客共计消费的总金额 |
machine_serial_no |
收银机器 | 负责结算顾客订单的收银机器 |
bill_date |
账单日期 | 本次账单的结算日期 |
bill_comment |
账单备注 | 账单的额外备注 |
其中注意的几个小细节:
- ①日期字段使用的是
timestamp
类型,而并非datetime
,因为前者更省空间。 - ②账单详情字段用的是
text
类型,因为这个字段可能会出现很多的信息。 - ③定义了一个和表没有关系的自增字段作为主键,用于维护聚簇索引树的结构。
除开有上述七个字段外,还有三个索引:
索引字段 | 索引类型 | 索引作用 |
---|---|---|
month_bills_id |
主键索引 | 主要作用就是用来维护聚簇索引树 |
serial_number |
唯一索引 | 当需要根据流水号查询数据时使用 |
bill_date |
唯一联合索引 | 当需要根据日期查询数据时使用 |
到这里就有了最基本的建表语句,主要是用来创建第一张月份账单表,如果想要实现动态按照每月建表的话,还需要用到存储过程来实现,接着来写一个存储过程,但如若对于存储过程语法还不了解的各位小伙伴,这里就不再做基础讲解,可自行阅读之前的《全解MySQL存储过程》。
最终撰写出的存储过程如下:
DELIMITER //
DROP PROCEDURE IF EXISTS create_table_by_month //
CREATE PROCEDURE `create_table_by_month`()
BEGIN
-- 用于记录下一个月份是多久
DECLARE nextMonth varchar(20);
-- 用于记录创建表的SQL语句
DECLARE createTableSQL varchar(5210);
-- 执行创建表的SQL语句后,获取表的数量
DECLARE tableCount int;
-- 用于记录要生成的表名
DECLARE tableName varchar(20);
-- 用于记录表的前缀
DECLARE table_prefix varchar(20);
-- 获取下个月的日期并赋值给nextMonth变量
SELECT SUBSTR(
replace(
DATE_ADD(CURDATE(), INTERVAL 1 MONTH),
'-', ''),
1, 6) INTO @nextMonth;
-- 设置表前缀变量值为td_user_banks_log_
set @table_prefix = 'month_bills_';
-- 定义表的名称=表前缀+月份,即 month_bills_2022112 这个格式
SET @tableName = CONCAT(@table_prefix, @nextMonth);
-- 定义创建表的SQL语句
set @createTableSQL=concat("create table if not exists ",@tableName,"(
`month_bills_id` int(8) NOT NULL AUTO_INCREMENT COMMENT '账单ID',
`serial_number` varchar(50) NOT NULL COMMENT '流水号',
`bills_info` text NOT NULL COMMENT '账单详情',
`pay_money` decimal(10,3) NOT NULL COMMENT '支付金额',
`machine_serial_no` varchar(20) NOT NULL COMMENT '收银机器',
`bill_date` timestamp NOT NULL DEFAULT now() COMMENT '账单日期',
`bill_comment` varchar(100) NULL DEFAULT '无' COMMENT '账单备注',
PRIMARY KEY (`month_bills_id`) USING BTREE,
UNIQUE `serial_number` (`serial_number`),
KEY `bill_date` (`bill_date`)
) ENGINE = InnoDB
CHARACTER SET = utf8
COLLATE = utf8_general_ci
ROW_FORMAT = Compact;");
-- 使用 PREPARE 关键字来创建一个预备执行的SQL体
PREPARE create_stmt from @createTableSQL;
-- 使用 EXECUTE 关键字来执行上面的预备SQL体:create_stmt
EXECUTE create_stmt;
-- 释放掉前面创建的SQL体(减少内存占用)
DEALLOCATE PREPARE create_stmt;
-- 执行完建表语句后,查询表数量并保存再 tableCount 变量中
SELECT
COUNT(1) INTO @tableCount
FROM
information_schema.`TABLES`
WHERE TABLE_NAME = @tableName;
-- 查询一下对应的表是否已存在
SELECT @tableCount 'tableCount';
END //
delimiter ;
上述这个存储过程比较长,但基本上都写好了注释,所以阅读起来应该还是比较轻松的,也包括该存储过程在MySQL5.1、8.0
版本中都测试过,所以大家也可以直接用,主要拆解一下里面较为难理解的一句SQL
,如下:
SELECT SUBSTR(
replace(
DATE_ADD(CURDATE(), INTERVAL 1 MONTH),
'-', ''),
1, 6) INTO @nextMonth;
这条语句执行之后会生成一个202212
这样的月份数字,主要用来作为表名的后缀,以此来区分不同的表,但里面用了几个函数组合出了该效果,下面做一下拆解,如下:
-- 在当前日期的基础上增加一个月,如2022-11-12 23:46:11,会得到2022-12-12 23:46:11
select DATE_ADD(CURDATE(), INTERVAL 1 MONTH);
-- 使用空字符代替日期中的 - 符号,得到 20221212 23:46:11 这样的效果
select replace('2022-12-12 23:46:11', '-', '');
-- 对字符串做截取,获取第一位到第六位,得到 202212 这样的效果
select SUBSTR("20221212 23:46:11",1,6);
经过上述拆解之后大家应该能看的很清楚,最终每次调用该存储过程时,都会基于当前数据库的时间,然后向后增加一个月,同时将格式转化为YYYYMM
格式,接下来调用该存储过程,如下:
call create_table_by_month();
+------------+
| tableCount |
+------------+
| 1 |
+------------+
当返回的值为1
而并非0
时,就表示已经在数据库中查到了前面通过存储过程创建的表,即表示动态创建表的存储过程可以生效!接着为了能够每月定时触发,可以在MySQL
中注册一个每月执行一次的定时事件,如下:
create EVENT
`create_table_by_month_event` -- 创建一个定时器
ON SCHEDULE EVERY
1 MONTH -- 每间隔一个月执行一次
STARTS
'2022-11-28 00:00:00' -- 从2022-11-28 00:00:00后开始
ON COMPLETION
PRESERVE ENABLE -- 执行完成之后不删除定时器
DO
call create_table_by_month(); -- 每次触发定时器时执行的语句
MySQL5.1
版本中除开引入了存储过程/函数、触发器的支持外,还引入了定时器的技术,也就是支持定时执行一条SQL
,此时咱们可借助MySQL
自带的定时器来定时调用之前的存储过程,最终实现按月定时创建表的需求!
但定时器在使用之前,需要先查看定时器是否开启,如下:
show variables like 'event_scheduler';
如果是OFF
关闭状态,需要通过set global event_scheduler = 1 | on;
命令开启。
如果想要永久生效,MySQL8.0
以下的版本可找到my.ini/my.conf
文件,然后找到[mysqld]
的区域,再里面多加入一行event_scheduler = ON
的配置即可。
这里再附上一些管理定时器的命令:
-- 查看创建的定时器
show events;
select * from mysql.event;
select * from information_schema.EVENTS;
-- 删除一个定时器
drop event 定时器名称;
-- 关闭一个定时器任务
alter event 定时器名称 on COMPLETION PRESERVE DISABLE;
-- 开启一个定时器任务
alter event 定时器名称 on COMPLETION PRESERVE ENABLE;
经过上述几步后,就能够让MySQL
自己按月创建表了,但为啥我会将定时器的时间设置为2022-11-28 00:00:00
这个时间后开始呢?因为202211
这张表我已经手动建立了,不将建立表的工作放在月初一号执行,这是因为前面的存储过程是创建下月表,而不是创建当月表,同时月底提前创建下月表,还能提高容错率,在MySQL
定时器故障的情况下,能预留人工介入的时间。
2.2、写入数据时能够根据月份插入对应表
作为一个后端项目,必然还需要搭建客户端,这里用SpringBoot+MyBatis
来快速构建一个单体项目(最后会给出完整源码),这里需要注意,月份账单表对应的实体类中要多出一个targetTable
字段,如下:
public class MonthBills {
// 月份账单表ID
private Integer monthBillsId;
// 账单流水号
private String serialNumber;
// 支付金额
private BigDecimal payMoney;
// 收银机器
private String machineSerialNo;
// 账单日期
private Date billDate;
// 账单详情
private String billsInfo;
// 账单备注
private String billComment;
// 要操作的目标表
private String targetTable;
// 省略构造方法和Get/Set方法.....
}
上述的实体类与之前的表字段结构几乎完全相同,但会多出一个targetTable
属性,后续会用来记录要操作的目标表,接着再撰写一个工具类,如下:
public class TableTimeUtils {
/*
* 使用ThreadLocal来确保线程安全,或者可以使用Java8新引入的DateTimeFormatter类:
* monthTL:负责将一个日期处理成 YYYYMM 格式
*/
private static ThreadLocal<SimpleDateFormat> monthTL =
ThreadLocal.withInitial(() ->
new SimpleDateFormat("YYYYMM"));
// 表的前缀
private static String tablePrefix = "month_bills_";
// 将一个日期格式化为YYYYMM格式
public static String getYearMonth(Date date) {
return monthTL.get().format(date);
}
// 获取目标数据的表名(操作单条数据公用的方法:增删改查)
public static void getDataByTable(MonthBills monthBills){
// 获取传入对象的时间
Date billDate = monthBills.getBillDate();
// 根据该对象中的时间,计算出要操作的表名后缀
String yearMonth = getYearMonth(billDate);
// 将表前缀和后缀拼接,得到完整的表名,如:month_bills_202211
monthBills.setTargetTable(tablePrefix + yearMonth);
}
}
这个工具类主要负责处理日期的时间格式,以及用来定位要操作的目标表名,对于日期格式化类:SimpleDateFormat
由于是线程不安全的,所以使用ThreadLocal
来确保线程安全!上述工具类中主要提供了两个基础方法:
-
getYearMonth()
:将一个日期格式化成YYYYMM
格式。 -
getDataByTable()
:获取单条数据操作时的表名。
有了工具类后,接着来撰写Dao、Mapper
层的代码,如下:
@Mapper
@Repository
public interface MonthBillsMapper {
int deleteByPrimaryKey(Integer monthBillsId);
int insertSelective(MonthBills record);
MonthBills selectByPrimaryKey(Integer monthBillsId);
int updateByPrimaryKeySelective(MonthBills record);
}
上述是月份账单表对应的Dao/Mapper
接口,因为我这里是通过MyBatis
的逆向工程文件自动生成的,所以名字就是上面那样,我这边未成更改,接着来看看对应的xml
文件,如下:
<insert id="insertSelective" parameterType="com.zhuzi.dbMachineSubmeter.entity.MonthBills">
insert into ${targetTable}
<trim prefix="(" suffix=")" suffixOverrides="," >
<if test="monthBillsId != null" >
month_bills_id,
</if>
<if test="serialNumber != null" >
serial_number,
</if>
<if test="payMoney != null" >
pay_money,
</if>
<if test="machineSerialNo != null" >
machine_serial_no,
</if>
<if test="billDate != null" >
bill_date,
</if>
<if test="billComment != null" >
bill_comment,
</if>
<if test="billsInfo != null" >
bills_info,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides="," >
<if test="monthBillsId != null" >
#{monthBillsId,jdbcType=INTEGER},
</if>
<if test="serialNumber != null" >
#{serialNumber,jdbcType=VARCHAR},
</if>
<if test="payMoney != null" >
#{payMoney,jdbcType=DECIMAL},
</if>
<if test="machineSerialNo != null" >
#{machineSerialNo,jdbcType=VARCHAR},
</if>
<if test="billDate != null" >
#{billDate,jdbcType=TIMESTAMP},
</if>
<if test="billComment != null" >
#{billComment,jdbcType=VARCHAR},
</if>
<if test="billsInfo != null" >
#{billsInfo,jdbcType=LONGVARCHAR},
</if>
</trim>
</insert>
上述这么大一长串,其实也不是俺手敲的,依旧是MyBatis
逆向工程生成的代码,但我对其中的一处稍微做了改动,如下:
-- 原本生成的代码是:
insert into month_bills_202211
-- 然后被我改成了:
insert into ${targetTable}
还记得最开始的实体类中,咱们多添加的那个targetTable
属性嘛?在这里会根据该字段的值动态的去操作不同月份的表,接着来写一下Service
层的接口和实现类,如下:
// Service接口(目前里面只有一个方法)
public interface IMonthBillsService {
int insert(MonthBills monthBills);
}
// Service实现类
@Service
public class MonthBillsServiceImpl implements IMonthBillsService {
@Autowired
private MonthBillsMapper billsMapper;
@Override
public int insert(MonthBills monthBills) {
// 获取要插入数据的表名
TableTimeUtils.getDataByTable(monthBills);
// 返回插入数据的状态
return billsMapper.insertSelective(monthBills);
}
}
在service
层目前仅实现了一个插入数据的方法,其中的逻辑也非常简单,仅仅在调用Dao
层的插入方法之前,获取了一下当前这条数据要插入的表名,最后来看看Controller/API
层,如下:
@RestController
@RequestMapping("/bills")
public class MonthBillsAPI {
@Autowired
private IMonthBillsService billsService;
// 账单结算的API
@RequestMapping("/settleUp")
public String settleUp(MonthBills monthBills){
// 设置账单交易时间为当前时间
monthBills.setBillDate(new Date(System.currentTimeMillis()));
// 使用UUID随机生成一个流水号
monthBills.setSerialNumber(monthBills.getMachineSerialNo()
+ System.currentTimeMillis());
// 调用新增账单数据的service方法
if (billsService.insert(monthBills) > 0){
return ">>>>账单结算成功<<<<";
}
return ">>>>账单结算失败<<<<";
}
}
在API
层主要对外提供了一个账单结算的接口,这里为了方便测试,所以对于请求方式的处理就没那么严谨了,在调用该接口后,会先获取一下当前系统时间作为账单时间,接着会随机生成一个UUID
作为流水号,最后就会调用service
层的insert()
方法。
到这里为止就搭建出了一个最简单的
WEB
接口,接着来做一个小小的测试,这里为了方便就不用专门的PostMan
工具了,就通过浏览器简单的调试一下,接口如下:http://localhost:8080/bills/settleUp?billsInfo=白玉竹子*3:9999.999&payMoney=9999.999&machineSerialNo=NF-002-X
最终测试效果图如下:
效果很明显,确实做到了咱们需要的效果,接着来看看控制台输出的SQL
日志,如下:
主要可以观察到,原本xml
中的动态表名,最终会根据月份被替换为具体的表名,最后再来看看数据库中的表是否真正插入了数据,如下:
因为之前测试过一次,因此表中早有了一条数据,主要观察第二条,的确是咱们刚刚测试时插入的数据,这也就意味着咱们按月动态插入的需求已经实现。
但看到这里估计绝大部分小伙伴略微有些懵,毕竟一通代码下来看起来,尤其是不在
IDEA
工具里面,没那么方便调试,因此最后画一个执行流程图,提供给诸位来梳理整体思路!
- ①客户端调用结算接口,传入相关的账单数据,即账单详情、账单金额、收银机器。
- ②
API
层会先获取当前系统时间作为账单交易的时间,然后调用Service
层的插入方法。 - ③
Service
层会先根据账单交易时间,获取到数据具体要插入的表名,接着调用Dao
层接口。 - ④
Dao
层会根据上层传递过来的表名,生成具体的SQL
语句,然后执行插入数据的操作。
三、按月分表后要解决的问题
上述已经将最基础的需求做了简单实现,那么接着再分析一下这些月份账单表还会有哪些需求呢?
- ①除去最基本的新增操作外,还会有删除、修改、查询账单的需求。
- ②一般账单表中的流水数据,都会支持按时间进行范围查询操作。
上述这两个需求会是账单表中还会存在的操作,对于第一点也比较容易实现,就是要求客户端在修改、删除、查询数据时,都必须携带上对应的时间,一般客户端的修改、删除操作都是基于先查询出数据的基础之上的,而一般查询数据都会按照月份进行查询,或者根据流水号进行查询。
3.1、根据流水号查询数据
还记得前面对于流水号的设计嘛?前面没有太过说明,这里咱们单独拧出来聊一聊:
setSerialNumber(monthBills.getMachineSerialNo()+System.currentTimeMillis());
这里使用了收银机器序列号+时间戳作为账单流水号,因为同一台机器在同一时间内,绝对只能对一个账单进行结算,所以再结合递增的时间戳,就能够得到一个全局唯一的流水号。System.currentTimeMillis()
获取到的时间戳是13
位数字,会放在机器序列号的后面,那接下来如果客户端要根据流水号查询账单数据,又该如何定位具体的表呢?首先需要在工具类中撰写一个新的方法:
// 根据流水号得到表名
public static void getTableBySerialNumber(MonthBills monthBills){
// 获取流水号的后13位(时间戳)
String timeMillis = monthBills.getSerialNumber().
substring(monthBills.getSerialNumber().length() - 13);
// 将字符串类型的时间戳转换为long类型
long millis = Long.parseLong(timeMillis);
// 调用getYearMonth()方法获取时间戳中的年月
String yearMonth = getYearMonth(new Date(millis));
// 用表的前缀名拼接年月,得到最终要操作的表名
monthBills.setTargetTable(tablePrefix + yearMonth);
}
上面这个方法实际上很简单,就是先解析流水号中的时间戳,然后根据时间戳得到具体的年月,最后拼接表的前缀名,得到最终需要操作的表名,接着来写一下Dao
层代码,如下:
<!-- 在MonthBillsMapper中多定义一个接口: -->
<!-- MonthBills selectBySerialNumber(MonthBills record); -->
<!-- 定义返回的结果集 -->
<resultMap id="ResultMapMonthBills" type="com.zhuzi.dbMachineSubmeter.entity.MonthBills" >
<constructor >
<idArg column="month_bills_id" jdbcType="INTEGER" javaType="java.lang.Integer" />
<arg column="serial_number" jdbcType="VARCHAR" javaType="java.lang.String" />
<arg column="pay_money" jdbcType="DECIMAL" javaType="java.math.BigDecimal" />
<arg column="machine_serial_no" jdbcType="VARCHAR" javaType="java.lang.String" />
<arg column="bill_date" jdbcType="TIMESTAMP" javaType="java.util.Date" />
<arg column="bill_comment" jdbcType="VARCHAR" javaType="java.lang.String" />
<arg column="bills_info" jdbcType="LONGVARCHAR" javaType="java.lang.String" />
</constructor>
</resultMap>
<!-- 定义字段列表 -->
<sql id="Base_Column_List" >
month_bills_id, serial_number, bills_info, pay_money, machine_serial_no,
bill_date, bill_comment
</sql>
<!-- 编写对应的查询语句,这里依旧是通过 ${targetTable} 动态表名做查询 -->
<select id="selectBySerialNumber" resultMap="ResultMapMonthBills"
parameterType="com.zhuzi.dbMachineSubmeter.entity.MonthBills" >
select
<include refid="Base_Column_List" />
from ${targetTable}
where serial_number = #{serial_number,jdbcType=VARCHAR}
</select>
接着来写一下Service
层的代码,如下:
// 在IMonthBillsService接口中多定义一个方法
MonthBills selectBySerialNumber(MonthBills monthBills);
// 在MonthBillsServiceImpl实现类中撰写具体的实现
@Override
public MonthBills selectBySerialNumber(MonthBills monthBills) {
// 根据流水号获取要查询数据的具体表名
TableTimeUtils.getTableBySerialNumber(monthBills);
// 调用Dao层根据流水号查询数据的方法
return billsMapper.selectBySerialNumber(monthBills);
}
这里的实现尤为简单,仅调用了一下前面写的工具类方法,获取了一下要查询数据的动态表名,接着再来写一下API
层的接口,如下:
// 根据流水号查询数据的API
@RequestMapping("/selectBySerialNumber")
public String selectBySerialNumber(MonthBills monthBills){
// 调用Service层根据流水号查询数据的方法
MonthBills result = billsService.selectBySerialNumber(monthBills);
if (result != null){
return result.toString();
}
return ">>>>未查询到流水号对应的数据<<<<";
}
接着来做一下测试,调用地址如下:
http://localhost:8080/bills/selectBySerialNumber?serialNumber=NF-002-X1668494222684
测试效果图如下:
此时会发现,根据流水号查询数据的效果就实现啦,这里主要是得设计好流水号的组成,其中一定要包含一个时间戳在内,这样就能够通过解析流水号的方式,得到具体要查询数据的表名,否则根据流水号查询数据的动作将异乎寻常的困难,因为需要把全部表扫描一次才能得到数据。
设计好根据流水号查询数据后,对于修改和删除的操作则不再重复撰写啦!因为过程也大致相同,就是在修改、删除时,同样先根据流水号定位到具体要操作的表,接着再去对应表中做相应操作即可。
3.2、按时间范围查询数据
按时间范围查询账单的流水数据,这是所有后台管理系统中都支持的功能,在这个项目中也不例外,但想要实现这个功能,则必须要有先实现两个功能:
- ①能够根据用户输入的两个时间范围,得到两个日期之间的所有表名。
- ②能够根据第①步中得到的表名,生成对应的查询语句,能够在单张表、多张表中通用。
上述这两个需求实际上实现起来也并不难,接着来一起做一下!
3.2.1、得到两个日期之间的所有表名
想要实现这个功能,那必然需要再在工具类中撰写一个方法,如下:
// 获取按时间范围查询时,两个日期之间,所有月份账单表的表名
public static List<String> getRangeQueryByTables(String startTime, String endTime){
// 声明一个日期格式化类
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM");
// 声明保存表名的集合
List<String> tables = new ArrayList<>();
try {
// 将两个传入的字符日期转换成日期类型
Date startDate = sdf.parse(startTime);
Date endDate = sdf.parse(endTime);
//用 Calendar 进行日期比较判断
Calendar calendar = Calendar.getInstance();
while (startDate.getTime() <= endDate.getTime()){
// 把生成的月份拼接表前缀名,加入到集合中
tables.add(tablePrefix + monthTL.get().format(startDate));
// 设置日期,并把比对器的日期增加一月
calendar.setTime(startDate);
calendar.add(Calendar.MONTH, 1);
// 获取增加后的日期
startDate = calendar.getTime();
}
} catch (ParseException e) {
e.printStackTrace();
}
// 返回两个日期之间的所有表名
return tables;
}
该方法需要传入两个参数,即两个字符串类型的时间,接着会通过Calendar
工具类,对两个日期的大小做判断,当开始日期小于结束日期时,则会直接将表前缀名与年月拼接,得到一张月份账单表的表名,接着会对开始日期加一个月,然后继续重复上一步......,直至得到两日期之间的所有表名。
3.2.2、根据表名集合生成对应的SQL语句
想要实现这个功能其实也非常简单,只需要做一堆判断即可,再在工具类中写一个方法:
// 根据日期生成SQL语句的方法
public static String getRangeQuerySQL(String startTime, String endTime){
// 先获取两个日期之间的所有表名
List<String> tables = getRangeQueryByTables(startTime, endTime);
// 提前创建一个字符串对象保存SQL语句
StringBuffer sql = new StringBuffer();
// 如果查询的两个日期是同一张表,则直接生成 BETWEEN AND 的SQL语句
if (tables.size() == 1){
sql.append("select * from ")
.append(tables.get(0))
.append(" where bill_date BETWEEN '")
.append(startTime)
.append("' AND '")
.append(endTime)
.append("';");
// 如果本次范围查询的两个日期之间有多张表
}else {
// 则用for循环遍历所有表名
for (String table : tables) {
// 对于第一张表则只需要查询开始日期之后的数据
if (table.equals(tables.get(0))){
sql.append("select * from ")
.append(table)
.append(" where bill_date > '")
.append(startTime)
.append("' union all ");
}
// 对于最后一张表只需要查询结束日期之前的数据
else if (table.equals(tables.get(tables.size()-1))){
sql.append("select * from ")
.append(table)
.append(" where bill_date < '")
.append(endTime)
.append("';");
// 对于其他表则获取所有数据
} else {
sql.append("select * from ")
.append(table)
.append("' union all ");
}
}
}
// 返回最终生成的SQL语句
return sql.toString();
}
这个方法看起来似乎有些长,但其实功能也非常简单,如下:
- ①如果两个日期在一个月内,则生成
BETWEEN AND
的查询语句。 - 如果两个日期间隔了多月,则用
for
循环遍历前面得到的表名:- 如果是第一张表,则只需要查询开始日期之后的数据,再用
union all
拼接后面的语句。 - 如果是最后一张表,则只需要查询结束日期之前的数据,以
;
分号结尾即可。 - 如果是中间的表,则查询对应的所有数据,接着继续用
union all
拼接其他语句。
- 如果是第一张表,则只需要查询开始日期之后的数据,再用
接着做个简单的小测试,效果如下:
很明显,通过这两个方法,可以实现最初咱们提出的两个需求,实现这两个基础功能后,接着套入到前面的项目中~
3.2.3、实现按时间做范围查询的API接口
依旧按照之前的步骤,先定义Dao
层的接口和.xml
文件,如下:
// 定义一个返回多条数据的接口
List<MonthBills> rangeQueryByDate(@Param("sql") String sql);
<select id="rangeQueryByDate" resultMap="ResultMapMonthBills"
parameterType="java.lang.String" >
${sql}
</select>
主要观察xml
文件中的代码,因为这里需要实现自定义SQL
的执行,所以将SQL
语句的生成工作放在了外部完成,在xml
中仅需将对应的SQL
语句发给MySQL
执行,并接收返回结果即
上一篇: 机电安装中的吊装与起重作业
推荐阅读
-
SSM三大框架基础面试题-一、Spring篇 什么是Spring框架? Spring是一种轻量级框架,提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 列举一些重要的Spring模块? Spring Core:核心,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 谈谈自己对于Spring IOC和AOP的理解 IOC(Inversion Of Controll,控制反转)是一种设计思想: 在程序中手动创建对象的控制权,交由给Spring框架来管理。IOC在其他语言中也有应用,并非Spring特有。IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。 将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。 Spring中的bean的作用域有哪些? 1.singleton:该bean实例为单例 2.prototype:每次请求都会创建一个新的bean实例(多例)。 3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。 Spring中的单例bean的线程安全问题了解吗? 概念用于理解:大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例bean存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 有两种常见的解决方案(用于回答的点): 1.在bean对象中尽量避免定义可变的成员变量(不太现实)。 2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal(线程本地化对象)中(推荐的一种方式)。 ThreadLocal解决多线程变量共享问题(参考博客):https://segmentfault.com/a/1190000009236777 Spring中Bean的生命周期: 1.Bean容器找到配置文件中Spring Bean的定义。 2.Bean容器利用Java Reflection API创建一个Bean的实例。 3.如果涉及到一些属性值,利用set方法设置一些属性值。 4.如果Bean实现了BeanNameAware接口,调用setBeanName方法,传入Bean的名字。 5.如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader方法,传入ClassLoader对象的实例。 6.如果Bean实现了BeanFactoryAware接口,调用setBeanClassFacotory方法,传入ClassLoader对象的实例。 7.与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 8.如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执postProcessBeforeInitialization方法。 9.如果Bean实现了InitializingBean接口,执行afeterPropertiesSet方法。 10.如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 11.如果有和加载这个Bean的Spring容器相关的BeanPostProcess对象,执行postProcessAfterInitialization方法。 12.当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy方法。 13.当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 Spring框架中用到了哪些设计模式? 1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。 2.代理设计模式:Spring AOP功能的实现。 3.单例设计模式:Spring中的bean默认都是单例的。 4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。 5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。 7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。 还有很多。。。。。。。 @Component和@Bean的区别是什么 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。 @Configuration public class AppConfig { @Bean public TransferService transferService { return new TransferServiceImpl; } } <beans> <bean id="transferService" class="com.kk.TransferServiceImpl"/> </beans> @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1; when 2: return new serviceImpl2; when 3: return new serviceImpl3; } } 将一个类声明为Spring的bean的注解有哪些? 声明bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明 注入bean的注解: @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 *扩:JSR 是 java 规范标准 Spring事务管理的方式有几种? 1.编程式事务:在代码中硬编码(不推荐使用)。 2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。 Spring事务中的隔离级别有哪几种? 在TransactionDefinition接口中定义了五个表示隔离级别的常量:ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 Spring事务中有哪几种事务传播行为? 在TransactionDefinition接口中定义了八个表示事务传播行为的常量。 支持当前事务的情况:PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。 不支持当前事务的情况:PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 其他情况:PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 二、SpringMVC篇 什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。 Spring MVC的工作原理了解嘛? image.png Springmvc的优点: (1)可以支持各种视图技术,而不仅仅局限于JSP; (2)与Spring框架集成(如IoC容器、AOP等); (3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。 (4) 支持各种请求资源的映射策略。 Spring MVC的主要组件? (1)前端控制器 DispatcherServlet(不需要程序员开发) 作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。 (2)处理器映射器HandlerMapping(不需要程序员开发) 作用:根据请求的URL来查找Handler (3)处理器适配器HandlerAdapter 注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。 (4)处理器Handler(需要程序员开发) (5)视图解析器 ViewResolver(不需要程序员开发) 作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view) (6)视图View(需要程序员开发jsp) View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等) springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。 SpringMVC怎么样设定重定向和转发的? (1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" (2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" SpringMvc怎么和AJAX相互调用的? 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。 如何解决POST请求中文乱码问题,GET的又如何处理呢? (1)解决post请求乱码问题: 在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8; <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> (2)get请求中文参数出现乱码解决方法有两个: ①修改tomcat配置文件添加编码与工程编码一致,如下: <ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> ②另外一种方法对参数进行重新编码: String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8") ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。 Spring MVC的异常处理 ? 统一异常处理: Spring MVC处理异常有3种方式: (1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; (2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; (3)使用@ExceptionHandler注解实现异常处理; 统一异常处理的博客:https://blog.csdn.net/ctwy291314/article/details/81983103 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。(此题目类似于上面Spring 中 第5题 有两种解决方案) SpringMVC常用的注解有哪些? @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 SpingMvc中的控制器的注解一般用那个,有没有别的注解可以替代? 一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置? 可以在@RequestMapping注解里面加上method=RequestMethod.GET。 怎样在方法里面得到Request,或者Session? 直接在方法的形参中声明request,SpringMVC就自动把request对象传入。 如果想在拦截的方法里面得到从前台传入的参数,怎么得到? 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象? 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面。 SpringMVC中函数的返回值是什么? 返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的。 SpringMVC用什么对象从后台向前台传递数据的? 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以拿到数据。 怎么样把ModelMap里面的数据放入Session里面? 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 SpringMvc里面拦截器是怎么写的: 有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: <!-- 配置SpringMvc的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.zwp.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors> 注解原理: 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池 三、Mybatis篇 什么是MyBatis? MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 讲下MyBatis的缓存 MyBatis的缓存分为一级缓存和二级缓存,一级缓存放在session里面,默认就有, 二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口, 可在它的映射文件中配置<cache/> Mybatis是如何进行分页的?分页插件的原理是什么? 1)Mybatis使用RowBounds对象进行分页,也可以直接编写sql实现分页,也可以使用Mybatis的分页插件。 2)分页插件的原理:实现Mybatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql。 举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10 简述Mybatis的插件运行原理,以及如何编写一个插件? 1)Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、 Executor这4种接口的插件,Mybatis通过动态代理, 为需要拦截的接口生成代理对象以实现接口方法拦截功能, 每当执行这4种接口对象的方法时,就会进入拦截方法, 具体就是InvocationHandler的invoke方法,当然, 只会拦截那些你指定需要拦截的方法。 2)实现Mybatis的Interceptor接口并复写intercept方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 记住,别忘了在配置文件中配置你编写的插件。 Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不? 1)Mybatis动态sql可以让我们在Xml映射文件内, 以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。 2)Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。 3)其执行原理为,使用OGNL从sql参数对象中计算表达式的值, 根据表达式的值动态拼接sql,以此来完成动态sql的功能。 #{}和${}的区别是什么? 1)#{}是预编译处理,${}是字符串替换。 2)Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值(有效的防止SQL注入); 3)Mybatis在处理${}时,就是把${}替换成变量的值。 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? Hibernate属于全自动ORM映射工具, 使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis在查询关联对象或关联集合对象时, 需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? 1)Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一,collection指的就是一对多查询。 在Mybatis配置文件中, 可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 2)它的原理是,使用CGLIB创建目标对象的代理对象, 当调用目标方法时,进入拦截器方法, 比如调用a.getB.getName, 拦截器invoke方法发现a.getB是null值, 那么就会单独发送事先保存好的查询关联B对象的sql, 把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了, 接着完成a.getB.getName方法的调用。 这就是延迟加载的基本原理。 MyBatis与Hibernate有哪些不同? 1)Mybatis和hibernate不同,它不完全是一个ORM框架, 因为MyBatis需要程序员自己编写Sql语句, 不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句, 并将java对象和sql语句映射生成最终执行的sql, 最后将sql执行的结果再映射生成java对象。 2)Mybatis学习门槛低,简单易学,程序员直接编写原生态sql, 可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发, 例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁, 一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性, 如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。 3)Hibernate对象/关系映射能力强,数据库无关性好, 对于关系模型要求高的软件(例如需求固定的定制化软件) 如果用hibernate开发可以节省很多代码,提高效率。 但是Hibernate的缺点是学习门槛高,要精通门槛更高, 而且怎么设计O/R映射,在性能和对象模型之间如何权衡, 以及怎样用好Hibernate需要具有很强的经验和能力才行。 总之,按照用户的需求在有限的资源环境下只要能做出维护性、 扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。 MyBatis的好处是什么? 1)MyBatis把sql语句从Java源程序中独立出来,放在单独的XML文件中编写, 给程序的维护带来了很大便利。 2)MyBatis封装了底层JDBC API的调用细节,并能自动将结果集转换成Java Bean对象, 大大简化了Java数据库编程的重复工作。 3)因为MyBatis需要程序员自己去编写sql语句, 程序员可以结合数据库自身的特点灵活控制sql语句, 因此能够实现比Hibernate等全自动orm框架更高的查询效率,能够完成复杂查询。 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象, 其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象, 其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete> 标签均会被解析为MappedStatement对象, 标签内的sql会被解析为BoundSql对象。 什么是MyBatis的接口绑定,有什么好处? 接口映射就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置. 接口绑定有几种实现方式,分别是怎么实现的? 接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加 上@Select@Update等注解里面包含Sql语句来绑定, 另外一种就是通过xml里面写SQL来绑定,在这种情况下, 要指定xml映射文件里面的namespace必须为接口的全路径名. 什么情况下用注解绑定,什么情况下用xml绑定? 当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多 MyBatis实现一对一有几种方式?具体怎么操作的? 有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id, 去再另外一个表里面查询数据,也是通过association配置, 但另外一个表的查询通过select属性配置。 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别? 能,Mybatis不仅可以执行一对一、一对多的关联查询, 还可以执行多对一,多对多的关联查询,多对一查询, 其实就是一对一查询,只需要把selectOne修改为selectList即可; 多对多查询,其实就是一对多查询,只需要把selectOne修改为selectList即可。 关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象, 赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询, 一部分列是A对象的属性值,另外一部分列是关联对象B的属性值, 好处是只发一个sql查询,就可以把主对象和其关联对象查出来。 MyBatis里面的动态Sql是怎么设定的?用什么语法? MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现, 但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有 内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and 或or 开始,那么会自动把这个and或者or取掉。 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。 第二种是使用sql列的别名功能,将列别名书写为对象属性名, 比如T_NAME AS NAME,对象属性名一般是name,小写, 但是列名不区分大小写,Mybatis会忽略列名大小写,
-
如何优雅地将每月新增上亿记录的大表在MySQL中进行分表操作,让解决方案实际落地?