对话存档:MySQL 时间字段最佳实践 一、添加时间字段的 ALTER 语句 在 MySQL 5.7.38 中,可以使用以下 ALTER 语句来添加创建时间和更新时间字段:
1 2 3 ALTER TABLE t_alarmADD COLUMN create_time_dt DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ,ADD COLUMN update_time_dt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ;
说明:
create_time_dt:创建时自动记录当前时间,之后不会自动变化
update_time_dt:创建时记录当前时间,每次更新记录时自动更新为当前时间
DATETIME 类型格式即为 2021-12-03 12:21:21
执行前建议:
二、时间参数前后端传递最佳实践 完整数据流架构 1 Vue (前端) → Controller (接收) → Service (处理) → MyBatis Mapper (持久化)
Vue 前端传参 传参格式:ISO 8601 标准字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import dayjs from 'dayjs' ;const params = { createTime : dayjs ().format ('YYYY-MM-DD HH:mm:ss' ), updateTime : new Date ().toISOString (), startTime : '2021-12-03 12:21:21' }; const params = { createTime : Date .now () };
推荐: 使用 YYYY-MM-DD HH:mm:ss 格式字符串,与 MySQL DATETIME 格式一致
Controller 层接收 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import org.springframework.format.annotation.DateTimeFormat;import java.util.Date;import java.time.LocalDateTime;import java.time.LocalDate;@RestController @RequestMapping("/alarm") public class AlarmController { @PostMapping("/create") public Result create ( @RequestParam("createTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") String createTime, @RequestParam("updateTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") String updateTime ) { return alarmService.create(createTime, updateTime); } @PostMapping("/create2") public Result create2 ( @RequestParam("createTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createTime, @RequestParam("updateTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime ) { return alarmService.create(createTime, updateTime); } @PostMapping("/create3") public Result create3 (@Valid @RequestBody AlarmDTO alarmDTO) { return alarmService.create(alarmDTO); } }
DTO 参数校验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import javax.validation.constraints.*;import org.springframework.format.annotation.DateTimeFormat;import java.time.LocalDateTime;public class AlarmDTO { @NotBlank(message = "告警名称不能为空") private String alarmName; @NotNull(message = "创建时间不能为空") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; @NotNull(message = "更新时间不能为空") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime startTime; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime endTime; }
应用层转换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Service public class AlarmService { @Autowired private AlarmMapper alarmMapper; public Result create (AlarmDTO dto) { String createTimeStr = formatDateTime(dto.getCreateTime()); String updateTimeStr = formatDateTime(dto.getUpdateTime()); Alarm alarm = new Alarm (); alarm.setCreateTimeDt(createTimeStr); alarm.setUpdateTimeDt(updateTimeStr); if (alarm.getCreateTimeDt() == null ) { alarm.setCreateTimeDt(formatDateTime(LocalDateTime.now())); } alarmMapper.insert(alarm); return Result.success(); } private String formatDateTime (LocalDateTime dateTime) { if (dateTime == null ) { return null ; } return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )); } private LocalDateTime parseDateTime (String dateTimeStr) { if (dateTimeStr == null ) { return null ; } return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )); } }
Entity 实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 public class Alarm { private Long id; private String alarmName; private String createTimeDt; private String updateTimeDt; }
MyBatis Mapper Mapper XML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?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.mapper.AlarmMapper" > <resultMap id ="BaseResultMap" type ="com.example.entity.Alarm" > <id column ="id" property ="id" /> <result column ="alarm_name" property ="alarmName" /> <result column ="create_time_dt" property ="createTimeDt" /> <result column ="update_time_dt" property ="updateTimeDt" /> </resultMap > <insert id ="insert" parameterType ="com.example.entity.Alarm" > INSERT INTO t_alarm (alarm_name, create_time_dt, update_time_dt) VALUES (#{alarmName}, #{createTimeDt}, #{updateTimeDt}) </insert > <select id ="selectByTimeRange" resultMap ="BaseResultMap" > SELECT * FROM t_alarm WHERE create_time_dt >= #{startTime} AND create_time_dt <= #{endTime} ORDER BY create_time_dt DESC </select > </mapper >
Mapper 接口
1 2 3 4 5 6 7 8 9 10 11 12 import org.apache.ibatis.annotations.Param;import java.util.List;public interface AlarmMapper { int insert (Alarm alarm) ; List<Alarm> selectByTimeRange ( @Param("startTime") String startTime, @Param("endTime") String endTime ) ;}
完整最佳实践总结
层级
推荐类型
说明
Vue 前端
String
格式:'2021-12-03 12:21:21'
Controller
LocalDateTime
使用 @DateTimeFormat 自动转换
DTO
LocalDateTime
类型安全,支持 JSR303 校验
Service
LocalDateTime
业务逻辑处理
Entity
String
与数据库 DATETIME 格式一致
MyBatis
String
无需类型处理器,直传直用
工具类推荐 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class DateTimeUtils { private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); public static String format (LocalDateTime dateTime) { return dateTime != null ? dateTime.format(DATETIME_FORMATTER) : null ; } public static LocalDateTime parse (String dateTimeStr) { return dateTimeStr != null ? LocalDateTime.parse(dateTimeStr, DATETIME_FORMATTER) : null ; } public static String now () { return LocalDateTime.now().format(DATETIME_FORMATTER); } }
注意事项
时区问题 :确保服务器和数据库时区一致
空值处理 :前后端都要做好 null 值处理
格式统一 :全项目统一使用 yyyy-MM-dd HH:mm:ss 格式
数据库自动更新 :如果配置了 ON UPDATE CURRENT_TIMESTAMP,应用层不需要手动更新 update_time_dt
三、时区检查与动态适配 检查 MySQL 时区 1 2 3 4 5 6 7 8 9 10 11 12 13 14 SELECT @@global .time_zone;SELECT @@session .time_zone;SELECT NOW(), CURRENT_TIMESTAMP (), UTC_TIMESTAMP();SHOW VARIABLES LIKE '%time_zone%' ;SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP());
结果示例:
SYSTEM - 表示使用系统时区
+08:00 - 东八区
UTC - 协调世界时
检查应用层(JVM)时区 1 2 3 4 5 6 System.out.println(TimeZone.getDefault()); System.out.println(System.getProperty("user.timezone" ));
四、应用层动态时区适配方案 方案一:JVM 启动参数指定时区(推荐) 1 2 3 4 5 6 7 8 9 10 java -Duser.timezone=Asia/Shanghai -jar your-app.jar export TZ='Asia/Shanghai' java -jar your-app.jar ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
方案二:Spring Boot 配置文件 application.yml
1 2 3 4 5 6 spring: jackson: time-zone: Asia/Shanghai date-format: yyyy-MM-dd HH:mm:ss datasource: url: jdbc:mysql://localhost:3306/your_db?serverTimezone=Asia/Shanghai
方案三:数据库连接 URL 时区参数(推荐) 1 2 3 4 5 6 7 8 9 10 11 12 13 spring: datasource: url: jdbc:mysql://localhost:3306/your_db?serverTimezone=Asia/Shanghai url: jdbc:mysql://localhost:3306/your_db?serverTimezone=UTC url: jdbc:mysql://localhost:3306/your_db?useTimezone=true&serverTimezone=Asia/Shanghai url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
方案四:自定义时区转换工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import java.time.*;import java.time.format.DateTimeFormatter;import java.util.TimeZone;public class TimeZoneUtils { private static final String DB_TIMEZONE = "UTC" ; private static final String APP_TIMEZONE = "Asia/Shanghai" ; private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); public static String toDbTime (LocalDateTime appTime) { if (appTime == null ) { return null ; } ZonedDateTime appZoned = appTime.atZone(ZoneId.of(APP_TIMEZONE)); ZonedDateTime dbZoned = appZoned.withZoneSameInstant(ZoneId.of(DB_TIMEZONE)); return dbZoned.format(DATETIME_FORMATTER); } public static LocalDateTime fromDbTime (String dbTimeStr) { if (dbTimeStr == null ) { return null ; } LocalDateTime dbTime = LocalDateTime.parse(dbTimeStr, DATETIME_FORMATTER); return dbTime.atZone(ZoneId.of(DB_TIMEZONE)) .withZoneSameInstant(ZoneId.of(APP_TIMEZONE)) .toLocalDateTime(); } public static String now () { return LocalDateTime.now(ZoneId.of(APP_TIMEZONE)) .format(DATETIME_FORMATTER); } public static String dbNow () { return ZonedDateTime.now(ZoneId.of(DB_TIMEZONE)) .format(DATETIME_FORMATTER); } }
方案五:MyBatis TypeHandler 自动转换(推荐) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import org.apache.ibatis.type.BaseTypeHandler;import org.apache.ibatis.type.JdbcType;import org.apache.ibatis.type.MappedTypes;import java.sql.*;import java.time.*;import java.time.format.DateTimeFormatter;@MappedTypes(LocalDateTime.class) public class LocalDateTimeTypeHandler extends BaseTypeHandler <LocalDateTime> { private static final String APP_TIMEZONE = "Asia/Shanghai" ; private static final String DB_TIMEZONE = "UTC" ; private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); @Override public void setNonNullParameter (PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType) throws SQLException { ZonedDateTime appZoned = parameter.atZone(ZoneId.of(APP_TIMEZONE)); ZonedDateTime dbZoned = appZoned.withZoneSameInstant(ZoneId.of(DB_TIMEZONE)); ps.setString(i, dbZoned.format(FORMATTER)); } @Override public LocalDateTime getNullableResult (ResultSet rs, String columnName) throws SQLException { String dbTimeStr = rs.getString(columnName); return convertToAppTime(dbTimeStr); } @Override public LocalDateTime getNullableResult (ResultSet rs, int columnIndex) throws SQLException { String dbTimeStr = rs.getString(columnIndex); return convertToAppTime(dbTimeStr); } @Override public LocalDateTime getNullableResult (CallableStatement cs, int columnIndex) throws SQLException { String dbTimeStr = cs.getString(columnIndex); return convertToAppTime(dbTimeStr); } private LocalDateTime convertToAppTime (String dbTimeStr) { if (dbTimeStr == null ) { return null ; } LocalDateTime dbTime = LocalDateTime.parse(dbTimeStr, FORMATTER); return dbTime.atZone(ZoneId.of(DB_TIMEZONE)) .withZoneSameInstant(ZoneId.of(APP_TIMEZONE)) .toLocalDateTime(); } }
配置 TypeHandler
1 2 3 mybatis: type-handlers-package: com.example.config.typehandler
或者在 Mapper XML 中指定:
1 2 3 4 5 6 7 <resultMap id ="BaseResultMap" type ="com.example.entity.Alarm" > <id column ="id" property ="id" /> <result column ="create_time_dt" property ="createTimeDt" typeHandler ="com.example.config.typehandler.LocalDateTimeTypeHandler" /> <result column ="update_time_dt" property ="updateTimeDt" typeHandler ="com.example.config.typehandler.LocalDateTimeTypeHandler" /> </resultMap >
方案六:Service 层统一处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @Service public class AlarmService { @Value("${app.timezone:Asia/Shanghai}") private String appTimezone; @Value("${db.timezone:UTC}") private String dbTimezone; public Result create (AlarmDTO dto) { Alarm alarm = new Alarm (); alarm.setCreateTimeDt(getCurrentDbTime()); if (dto.getUpdateTime() != null ) { alarm.setUpdateTimeDt(convertToDbTime(dto.getUpdateTime())); } else { alarm.setUpdateTimeDt(getCurrentDbTime()); } alarmMapper.insert(alarm); return Result.success(); } private String getCurrentDbTime () { return ZonedDateTime.now(ZoneId.of(dbTimezone)) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )); } private String convertToDbTime (LocalDateTime appTime) { ZonedDateTime appZoned = appTime.atZone(ZoneId.of(appTimezone)); ZonedDateTime dbZoned = appZoned.withZoneSameInstant(ZoneId.of(dbTimezone)); return dbZoned.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )); } private LocalDateTime convertFromDbTime (String dbTimeStr) { LocalDateTime dbTime = LocalDateTime.parse(dbTimeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )); return dbTime.atZone(ZoneId.of(dbTimezone)) .withZoneSameInstant(ZoneId.of(appTimezone)) .toLocalDateTime(); } }
五、完整配置示例 application.yml
1 2 3 4 5 6 7 8 9 10 11 12 app: timezone: Asia/Shanghai db-timezone: UTC spring: jackson: time-zone: ${app.timezone} date-format: yyyy-MM-dd HH:mm:ss datasource: url: jdbc:mysql://localhost:3306/your_db?serverTimezone=${app.db-timezone}
启动类添加时区
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @SpringBootApplication public class Application { @Value("${app.timezone:Asia/Shanghai}") private String timezone; @PostConstruct public void init () { TimeZone.setDefault(TimeZone.getTimeZone(timezone)); System.out.println("应用时区设置为: " + timezone); } public static void main (String[] args) { SpringApplication.run(Application.class, args); } }
六、最佳实践推荐
方案
适用场景
优先级
JVM 启动参数
统一时区,简单直接
⭐⭐⭐⭐⭐
数据库 URL 时区参数
数据库时区不一致
⭐⭐⭐⭐⭐
TypeHandler 自动转换
跨时区应用,自动处理
⭐⭐⭐⭐
Spring Boot 配置
项目级配置
⭐⭐⭐⭐
Service 层手动转换
特殊业务场景
⭐⭐⭐
推荐组合: JVM 参数 + 数据库 URL 时区参数 + Spring Boot 配置
七、通用 SQL 脚本模板 1 2 3 4 5 6 7 8 9 10 11 ALTER TABLE your_table_nameADD COLUMN create_time_dt DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ,ADD COLUMN update_time_dt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ;DESC your_table_name;
使用步骤:
替换表名
1 2 3 4 5 vim add_datetime_columns.sql sed 's/your_table_name/t_user/g' add_datetime_columns.sql > add_t_user.sql
执行脚本
1 mysql -u root -p your_database < add_datetime_columns.sql
验证结果