文章目录
MyBatis-Plus进阶和使用实践
一、前言
- 环境说明
- 操作系统:Windows 10 专业版
- Mybatis-Plus 版本:3.4.2
- JDK 版本:1.8
- 开发工具:IntelliJ IDEA 2020.2.2
- 简介
-
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
特性:
- 无侵入、消耗小、强大的 CRUD 操作
- 支持 Lambda 形式调用,支持多种数据库
- 支持主键自动生成,支持 ActiveRecord 模式
- 支持自定义全局通用操作,支持关键词自动转义
- 内置代码生成器,内置分页插件,内置性能分析插件
- 内置全局拦截插件,内置 SQL 注入剥离器
MyBatis-Plus 官网:https://mp.baomidou.com/
MyBatis-Plus 示例工程:https://gitee.com/baomidou/mybatis-plus-samples/tree/master
MyBatis 官网:https://mybatis.org/mybatis-3/zh/index.html
Maven 仓库:https://mvnrepository.com/
MyBatis-Plus入门和使用实践:https://blog.csdn.net/u011424614/article/details/113791443
二、正文
1.创建项目
1)引入依赖
pom.xml
引入依赖 mybatis-plus、lombok、spring-boot
<!-- spring-boot-starter 父工程 -->
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.3.RELEASE</version>
</parent>
<dependencies>
<!-- spring-boot 启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- spring-boot 测试启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- mybatis-plus spring-boot 启动器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.4.2</version>
</dependency>
<!-- mysql 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
2)SpringBoot配置
- resources 文件夹创建
application.yml
# mysql 连接配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mp-advanced-db?useSSL=false&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: 123456
# 输入日志,查看 sql 语句
logging:
level:
root: warn
org.example.dao: trace
pattern:
console: '%p%m%n'
App.java
启动 spring-boot
package org.example;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* spring-boot 启动入口
*
*/
@SpringBootApplication
@MapperScan({"org.example.dao"})
public class App
{
public static void main( String[] args )
{
SpringApplication.run(App.class, args);
}
}
3)测试数据
- sql 脚本
#创建用户表
CREATE TABLE user (
id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主键',
name VARCHAR(30) DEFAULT NULL COMMENT '姓名',
age INT(11) DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
manager_id BIGINT(20) DEFAULT NULL COMMENT '上级领导id',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
update_time DATETIME DEFAULT NULL COMMENT '修改时间',
version INT(11) DEFAULT '1' COMMENT '版本',
deleted INT(1) DEFAULT '0' COMMENT '逻辑删除标识(0.未删除,1.已删除)',
CONSTRAINT manager_fk FOREIGN KEY (manager_id)
REFERENCES user (id)
) ENGINE=INNODB CHARSET=UTF8;
#初始化数据:
INSERT INTO user (id, name, age, email, manager_id
, create_time)
VALUES (1087982257332887553, '大boss', 40, 'boss@baomidou.com', NULL
, '2019-01-11 14:20:20'),
(1088248166370832385, '王天风', 25, 'wtf@baomidou.com', 1087982257332887553
, '2019-02-05 11:12:22'),
(1088250446457389058, '李艺伟', 28, 'lyw@baomidou.com', 1088248166370832385
, '2019-02-14 08:31:16'),
(1094590409767661570, '张雨琪', 31, 'zjq@baomidou.com', 1088248166370832385
, '2019-01-14 09:15:15'),
(1094592041087729666, '刘红雨', 32, 'lhm@baomidou.com', 1088248166370832385
, '2019-01-14 09:48:16');
- 创建
entity/User.java
package org.example.entity;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import java.time.LocalDateTime;
/***
* 用户实体类
* */
@Data
public class User extends Model {
// 用户 ID
private Long id;
// 姓名
private String name;
// 年龄
private Integer age;
// 邮箱
private String email;
// 上级领导 ID
private Long managerId;
// 创建时间
private LocalDateTime createTime;
// 更新时间
private LocalDateTime updateTime;
// 版本
private Integer version;
// 逻辑删除标识(0.未删除,1.已删除)
private Integer deleted;
}
- 创建
dao/UserMapper.java
(接口)
package org.example.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.entity.User;
public interface UserMapper extends BaseMapper<User> {
}
4)测试例子
- 测试类:
AppTest.java
package org.example;
import org.example.dao.UserMapper;
import org.example.entity.User;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class AppTest
{
@Autowired
private UserMapper userMapper;
/**
* 查询列表
* */
@Test
public void select()
{
List<User> users = userMapper.selectList(null);
// 判断预期值与实际值是否相同
// Assert.assertEquals(5, users.size());
// 循环输出
users.forEach(System.err::println);
}
}
2.逻辑删除
使用场景: 1.保留业务数据; 2.主外键关联,不方便删除
- application.yml 配置逻辑删除默认值
# mybatis-plus
mybatis-plus:
# 全局主键配置
global-config:
db-config:
# 逻辑删除:标记为正常的值
logic-not-delete-value: 0
# 逻辑删除:标记为删除的值
logic-delete-value: 1
- entity/User.java
@TableLogic
:标记逻辑删除属性
@TableField(select = false)
:查询时排除掉当前属性
public class User {
......
// 逻辑删除标识(0.未删除,1.已删除)
@TableLogic
@TableField(select = false)
private Integer deleted;
}
- 测试类:AppTest.java
@Test
public void deleteById()
{
Long userid = 1088250446457389058L;
// 根据 ID 删除数据
Integer num = userMapper.deleteById(userid);
System.out.println("受影响行数:"+num);
// 查询删除结果
List<User> users = userMapper.selectList(null);
users.forEach(System.err::println);
}
结果:
1.删除方法,最终执行的是 update 语句
2.查询方法,最终执行的查询 SQL 中添加了逻辑删除的属性,只查询未删除的数据
注意事项:
- 执行修改时,会自动添加逻辑删除属性,作为其中的一个条件
- 自定义 SQL 不会自动添加逻辑删除属性(解决:1.删除标识加到 Wrapper 中;2.删除标识加到 SQL 中)
3.自动填充
自动填充-MP官网:https://mp.baomidou.com/guide/auto-fill-metainfo.html
使用场景: 自动填充创建时间和修改时间
- entity/User.java
@TableField(fill = FieldFill.INSERT)
:执行新增操作时,进行填充
@TableField(fill = FieldFill.UPDATE)
:执行修改操作时,进行填充
public class User {
......
// 创建时间
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
// 更新时间
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
}
- 创建 componect/MyMetaObjectHandler.java
实现:
com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
接口
package org.example.componect;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 当属性存在时,在执行自动填充操作
boolean hasSetter = metaObject.hasSetter("createTime");
// 当属性未设置值时,在执行自动填充操作
Object val = getFieldValByName("createTime", metaObject);
if(hasSetter && val == null){ // 属性存在,且未设置值
setFieldValByName("createTime", LocalDateTime.now(), metaObject);
}
}
@Override
public void updateFill(MetaObject metaObject) {
// 当属性存在时,在执行自动填充操作
boolean hasSetter = metaObject.hasSetter("updateTime");
// 当属性未设置值时,在执行自动填充操作
Object val = getFieldValByName("updateTime", metaObject);
if(hasSetter && val == null){ // 属性存在,且未设置值
setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
}
}
- 测试类:AppTest.java
@Test
public void insert()
{
User user = new User();
user.setName("李小龙");
user.setAge(22);
user.setEmail("lxl@163.com");
user.setManagerId(1088248166370832385L);
// 插入数据
int num = userMapper.insert(user);
System.err.println("影响记录数据:"+num);
System.err.println("主键:"+user.getId());
}
@Test
public void updateById()
{
User user = new User();
user.setId(1360237954211033089L);
user.setAge(23);
// 更新数据
int num = userMapper.updateById(user);
System.err.println("影响记录数据:"+num);
}
4.乐观锁
乐观锁-维基百科:https://zh.wikipedia.org/wiki/%E4%B9%90%E8%A7%82%E5%B9%B6%E5%8F%91%E6%8E%A7%E5%88%B6
乐观锁-MP官网:https://mp.baomidou.com/guide/interceptor-optimistic-locker.html#optimisticlockerinnerinterceptor
使用场景: 防止更新冲突
- config/MybatisPlusConfig.java
package org.example.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 乐观锁插件
return interceptor;
}
}
- entity/User.java
public class User {
......
// 版本
@Version
private Integer version;
}
- 测试类:AppTest.java
@Test
public void updateById()
{
// 查询数据,获取当前版本
Long userid = 1360237954211033089L;
User user = userMapper.selectById(userid);
// 更新数据
user.setAge(23);
int num = userMapper.updateById(user);
System.err.println("影响记录数据:"+num);
}
5.性能分析
执行 SQL 分析-MP官网:https://mp.baomidou.com/guide/p6spy.html
p6spy-GitHub:https://github.com/p6spy/p6spy
使用场景: 输出每条 SQL 语句执行时间
注意: 该插件有性能损耗,不建议生产环境使用。
建议: 开发环境和测试环境配置 p6spy 驱动;生产环境配置正常的数据库连接驱动
- pom.xml
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.9.1</version>
</dependency>
- resources/application.yml
数据源配置修改:driver-class-name 和 url
spring:
datasource:
# driver-class-name: com.mysql.cj.jdbc.Driver
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# url: jdbc:mysql://localhost:3306 ......
url: jdbc:p6spy:mysql://localhost:3306 ......
......
- 创建 resources/spy.properties
#3.2.1以上使用
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
#3.2.1以下使用或者不配置
#modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
# 输出到日志文件
# logfile=spylog.log
- 测试类:AppTest.java
@Test
public void select()
{
List<User> users = userMapper.selectList(null);
// 循环输出
users.forEach(System.err::println);
}
6.多租户
多租户技术-维基百科:https://zh.wikipedia.org/wiki/%E5%A4%9A%E7%A7%9F%E6%88%B6%E6%8A%80%E8%A1%93
多租户-MP官网:https://mp.baomidou.com/guide/interceptor-tenant-line.html#tenantlineinnerinterceptor
使用场景: 多用户的环境下共享相同的数据库,例如:当前用户只能查询自己创建的数据
- config/MybatisPlusConfig.java
package org.example.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 多租户 sql 解析器
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public String getTenantIdColumn() {
return "manager_id"; // 实现多租户的数据库表字段名
}
@Override
public Expression getTenantId() {
// 一般从 session 或 配置文件中获取
Long managerId = 1088248166370832385L;
return new LongValue(managerId); // 传入的多租户值
}
@Override
public boolean ignoreTable(String tableName) {
List<String> list = Arrays.asList("rols", "power");
return list.contains(tableName);
// return false;// 表示当前表需要添加多租户过滤;如果不需要,判断表名后,返回 true 即可
}
}));
// 分页插件
// 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
// 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
// PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
- 测试类:AppTest.java
实际情况:CURD 都会生效
方法级取消多租户信息:Mapper 方法上添加注解
@InterceptorIgnore(tenantLine = "true")
@Test
public void select()
{
List<User> users = userMapper.selectList(null);
// 循环输出
users.forEach(System.err::println);
}
7.动态表
动态表-MP官网:https://mp.baomidou.com/guide/interceptor-dynamic-table-name.html
-
使用场景: 不同的表,表结构相同(数据量大);例如:日志表,根据月份进行拆分
-
config/MybatisPlusConfig.java
package org.example.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.HashMap;
import java.util.Random;
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 动态表(这里需要先创建 user_2018 和 user_2019 两张表)
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor
= new DynamicTableNameInnerInterceptor();
HashMap<String, TableNameHandler> map = new HashMap<String, TableNameHandler>(2) {{
put("user", (sql, tableName) -> {
String year = "_2018";
int random = new Random().nextInt(10);
if (random % 2 == 1) {
year = "_2019";
}
return tableName + year;
});
}};
dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map);
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
return interceptor;
}
}
- 测试类:AppTest.java
方法级取消动态表名替换:Mapper 方法上添加注解
@InterceptorIgnore(dynamicTableName= "true")
@Test
public void select()
{
List<User> users = userMapper.selectList(null);
// 循环输出
users.forEach(System.err::println);
}
8.SQL注入器
SQL 注入器-MP官网:https://mp.baomidou.com/guide/sql-injector.html
1)自定义
-
使用场景: 自定义通用方法;例如:BaseMapper 的方法
步骤:
- 创建自定义方法的类
- 创建注入器
- 将自定义方法添加到 Mapper 中
- 创建 method/DeleteAllMethod.java
继承
com.baomidou.mybatisplus.core.injector.AbstractMethod
package org.example.method;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
public class DeleteAllMethod extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 定义 sql 语句
String sql = "delete from " + tableInfo.getTableName();
// 定义方法名
String method = "deleteAll";
// sql 源
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// delete 方法
return addDeleteMappedStatement(mapperClass, method, sqlSource);
}
}
- 创建 injector/MySqlInjector.java
package org.example.injector;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import org.example.method.DeleteAllMethod;
import java.util.List;
@Component
public class MySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methods = super.getMethodList(mapperClass); // 获取之前已添加的方法类
methods.add(new DeleteAllMethod()); // 添加自定义的方法类
return methods;
}
}
- 创建 dao/MyMapper.java
继承
com.baomidou.mybatisplus.core.mapper.BaseMapper
package org.example.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface MyMapper<T> extends BaseMapper<T> {
/**
* 删除所有数据
* @return 受影响行数
* */
public int deleteAll();
}
- dao/UserMapper.java
继承
MyMapper
package org.example.dao;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.example.entity.User;
import java.util.List;
public interface UserMapper extends MyMapper<User> {
......
}
- 测试类:AppTest.java
方法级取消动态表名替换:Mapper 方法上添加注解
@InterceptorIgnore(dynamicTableName= "true")
@Test
public void deleteAll()
{
Integer num = userMapper.deleteAll();
System.out.println("受影响行数:"+num);
}
2)MP选装件
选装件-MP官网:https://baomidou.com/guide/crud-interface.html#mapper-%E5%B1%82-%E9%80%89%E8%A3%85%E4%BB%B6
- MP 写好的方法,但是并没有添加到 BaseMapper 中;需要手动在
injector/MySqlInjector.java
中添加
选装件目录:com.baomidou.mybatisplus.extension.injector.methods
类名 | 说明 |
---|---|
InsertBatchSomeColumn | 批量插入,可排除某些字段 |
LogicDeleteByIdWithFill | 根据 id 逻辑删除数据,并带字段填充功能;例如:逻辑删除时,修改操作人字段 |
AlwaysUpdateSomeColumnById | 根据 id 更新固定的某些字段 |
InsertBatchSomeColumn 为例:
- injector/MySqlInjector.java
package org.example.injector;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import org.example.method.DeleteAllMethod;
import java.util.List;
@Component
public class MySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methods = super.getMethodList(mapperClass); // 获取之前已添加的方法类
methods.add(new DeleteAllMethod()); // 添加自定义的方法类
methods.add(new InsertBatchSomeColumn(t->!t.isLogicDelete())); // 不是逻辑删除的字段都包含在内
return methods;
}
}
- dao/MyMapper.java
继承
com.baomidou.mybatisplus.core.mapper.BaseMapper
package org.example.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
public interface MyMapper<T> extends BaseMapper<T> {
/**
* 删除所有数据
* @return 受影响行数
* */
public int deleteAll();
/**
* 批量插入
* @return 受影响行数
* */
public int insertBatchSomeColumn(List<T> list);
}
- dao/UserMapper.java
继承
MyMapper
- 测试类:AppTest.java
方法级取消动态表名替换:Mapper 方法上添加注解
@InterceptorIgnore(dynamicTableName= "true")
@Test
public void insertBatchSomeColumn()
{
User user = new User();
user.setName("李小龙");
user.setAge(22);
user.setEmail("lxl@163.com");
user.setManagerId(1088248166370832385L);
User user2 = new User();
user2.setName("李二龙");
user2.setAge(25);
user2.setEmail("lel@163.com");
user2.setManagerId(1088248166370832385L);
List<User> users = Arrays.asList(user, user2);
// 插入数据
int num = userMapper.insertBatchSomeColumn(users);
System.err.println("影响记录数据:"+num);
System.err.println("主键:"+user.getId());
}