Java导入Excel到数据库工具——EXCEL-UTIL4J使用示例

2017.9.3 更新v2.1.0: 进度监控调整为整个导入过程的进度监控,并且更加精确

前些时日开发了一款用Java将Excel导入数据库的工具:EXCEL-UTIL4J(源码和文档在这里),觉得还是写一个使用示例的好,主要想用来记录使用方法。

因为时间有限,工具只实现了一对一关系数据导入,以及只能使用hibernate作为ORM框架进行导入,所以本示例将只做一对一关系数据导入,并且最大可能的利用到工具的所有功能,使用hibernate进行导入。

示例中只会简单的提示代码作用,建议先大概浏览一下工具文档,这样有利于理解示例的代码,本示例代码源码见 excel-util4j-sample


需求

1. Excel表如下:
Excel表头
2. 数据表结构如下:
Java导入Excel到数据库工具——EXCEL-UTIL4J使用示例_第1张图片
表结构
3. 分析
  1. 字段对应
    Excel中的账号密码在数据表excel_util4j_user(下简称user)中,分别对应usernamepassword
    Excel中的姓名性别生日电话地址excel_util4j_user_info(下简称info)中,分别对应namegenderbirthdayphoneaddress

  2. 字段约束(下面有建表语句,展示更清晰)

    1. 唯一性
      user表中username以及info表中phone字段为唯一的
    2. 非空
      usernamepasswordname
    3. 其他
      username最大20字、password6~20字、name最大20字、phone最大20,并且需要验证是否为电话号码、address最大50字
  3. 常量值字段
    user表中除了excel中的字段外,还有enable字段,该字段表示是否启用账号,值为01,分别表示禁用启用,本例中所有账户设置为1

  4. 其他字段

  • 每个用户还有角色,对应的表为excel_util4j_user_role(下简称user_role)表,由于excel-util4j只能导入一对一关系的数据,所以这里仅仅设置角色为一个,假设role_id3

  • 创建时间字段create_time,该字段不由excel导入,也不是常量字段,而是系统当前时间

  1. 密码处理
    密码解析后,需要验证是否为6~20个字符,通过验证后需要转换为MD5+Base64加密的格式再存表

下面是建表语句,表结构和约束更清晰的展现:

  • user表:
CREATE TABLE excel_util4j_user (
  
  id bigint(20) NOT NULL AUTO_INCREMENT,
  username varchar(20) NOT NULL,
  password varchar(20) NOT NULL,
  create_time datetime DEFAULT NULL,
  enable int(1) NOT NULL,
  
  PRIMARY KEY (id),
  UNIQUE KEY username_unique (username) USING BTREE
);
  • info表:
CREATE TABLE excel_util4j_user_info (

  id bigint(20) NOT NULL AUTO_INCREMENT,
  user_id bigint(20) NOT NULL,
  name varchar(20) NOT NULL,
  gender int(1) DEFAULT NULL,
  birthday date DEFAULT NULL,
  phone varchar(20) DEFAULT NULL,
  address varchar(50) DEFAULT NULL,

  PRIMARY KEY (id),
  UNIQUE KEY phone_unique (phone) USING BTREE
);
  • user_role表:
CREATE TABLE excel_util4j_user_role (

  id bigint(20) NOT NULL AUTO_INCREMENT,
  user_id bigint(20) NOT NULL,
  role_id int(11) NOT NULL,

  PRIMARY KEY (id)
);

编码

  1. 源码下载下来,然后mvn install -Dmaven.test.skip=true安装相应jar包,共4个

  2. 配置POM文件,将包添加到依赖:

    
    
        online.dinghuiye
        poi-kit
        1.0.1
    
    
    
    
        online.dinghuiye
        excelutil-api
        2.1.0
    
    
    
    
        online.dinghuiye
        excelutil
        2.1.0
    
    
    
    
        online.dinghuiye
        persistence-hibernate-impl
        2.1.0
    
    
    
    
    
        mysql
        mysql-connector-java
        5.1.42
    
    
    
        com.alibaba
        druid
        1.0.31
    
    
    
        org.hibernate
        hibernate-core
        4.3.11.Final
    
    
    
    
    
        org.apache.commons
        commons-lang3
        3.6
    
    
    
        javax.validation
        validation-api
        1.1.0.Final
    
    
    
        org.hibernate
        hibernate-validator
        5.4.1.Final
    
    
        org.glassfish
        javax.el
        3.0.1-b08
    
    
    
        com.alibaba
        fastjson
        1.2.33
    
    
  3. 创建实体类,用Intellij Idea自动生成,下面列出3个pojo代码,关键部分都有注释。属性注解参看文档。

    值得注意的是:

    • UniqueValidator.class(v2.0.0版本的示例)或UsernameUniqueValidator.classPhoneUniqueValidator.class(v2.1.0版本示例)是自定义的判重验证器,并不是使用的hibernate validator的自定义验证,是excel-util4j中实现的验证器,使用前需要编写类并实现online.dinghuiye.api.validation.Validator,代码见下文UniqueValidator验证器,另外两个验证器相似

    • password字段从excel导入后,需要先验证长度在6 ~ 20个字符,然后通过PasswordRepairer进行修正,即将POJO对象的password属性值设置为MD5+Base64加密的字符串。加密后的字符串长度不一定再满足6 ~ 20个字符了,hibernate存表时还会再次按照POJO注解进行验证,此时就可能无法验证通过而报错,所以需要将因为属性修正而可能影响到的验证的注解加上groups = {Validator.class}参数,Validator.classonline.dinghuiye.api.validation.Validator.class

    • createTime字段是系统时间字段,使用自定义转换器设值,使用谦虚编写类并实现online.dinghuiye.api.resolution.Convertor接口,重写方法返回需要的特定值即可,代码见下文CurrentTimeConvertor转换器(v2.1.0版本示例)

  • user pojo
package online.dinghuiye.example.entity;

import online.dinghuiye.api.annotation.validate.Validate;
import online.dinghuiye.api.validation.Validator;
import online.dinghuiye.core.annotation.convert.ConstValue;
import online.dinghuiye.core.annotation.convert.ValueConvert;
import online.dinghuiye.core.annotation.excel.SheetTitleName;
import online.dinghuiye.example.convertor.CurrentTimeConvertor;
import online.dinghuiye.example.validator.UsernameUniqueValidator;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.validator.constraints.NotBlank;

import javax.persistence.*;
import javax.validation.Valid;
import javax.validation.constraints.Size;
import java.util.Date;

/**
 * @author Strangeen on 2017/08/27
 *
 * @author Strangeen on 2017/9/3
 * @version 2.1.0
 */
@Entity
@DynamicInsert(true)
@Table(name = "excel_util4j_user")
public class ExcelUtil4JUserEntity {

    @Transient // 不需要执行转换和验证,但并不影响hibernate存表的操作
    private Long id;

    @SheetTitleName("账号") // excel表字段对应
    @NotBlank
    @Size(max = 20, message = "输入最大{max}个字")
    @Validate(validator = UsernameUniqueValidator.class, message = "已被注册") // 自定义检验器,判断重复
    private String username;

    @SheetTitleName("密码")
    // 如果后续需要repaire的属性,需要将repaire可能影响的验证加上groups={Validator.class}
    // 否则可能会导致比如字符串长度改变而无法再存表时通过hibernate的验证
    @NotBlank
    @Size(max = 20, min = 6, message = "输入{min}~{max}个字", groups = {Validator.class})
    private String password;

    @ValueConvert(CurrentTimeConvertor.class) // 自定义转换器,存入当前时间
    private Date createTime;

    @ConstValue("1") // 常量值转换器,导入时会被设置为1
    private Integer enable;

    @Valid // 执行hibernate validator支持的对象属性检测,不注释@Valid则不会对info对象的属性进行检测
    private ExcelUtil4JUserInfoEntity info;

    private ExcelUtil4JUserRoleEntity userRole;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Basic
    @Column(name = "username")
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Basic
    @Column(name = "password")
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Basic
    @Column(name = "create_time")
    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    @Basic
    @Column(name = "enable")
    public Integer getEnable() {
        return enable;
    }

    public void setEnable(Integer enable) {
        this.enable = enable;
    }

    @OneToOne(mappedBy = "user", cascade = {CascadeType.ALL})
    public ExcelUtil4JUserInfoEntity getInfo() {
        return info;
    }

    public void setInfo(ExcelUtil4JUserInfoEntity info) {
        this.info = info;
    }

    // 这里定义为OneToOne并不太合适,只是为了演示
    // 常规应该使用OneToMany,现阶段无法实现OneToMany的导入,就只能使用RowRecordPerPersistentRepairer在存表前进行修正了
    @OneToOne(mappedBy = "user", cascade = {CascadeType.ALL})
    public ExcelUtil4JUserRoleEntity getUserRole() {
        return userRole;
    }

    public void setUserRole(ExcelUtil4JUserRoleEntity userRole) {
        this.userRole = userRole;
    }
}
  • info pojo
package online.dinghuiye.example.entity;

import online.dinghuiye.api.annotation.validate.Validate;
import online.dinghuiye.core.annotation.convert.BlankToNull;
import online.dinghuiye.core.annotation.convert.DateFormat;
import online.dinghuiye.core.annotation.convert.ValueMap;
import online.dinghuiye.core.annotation.excel.SheetTitleName;
import online.dinghuiye.example.validator.PhoneUniqueValidator;
import org.hibernate.validator.constraints.NotBlank;

import javax.persistence.*;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.util.Date;

/**
 * @author Strangeen on 2017/08/27
 *
 * @author Strangeen on 2017/9/3
 * @version 2.1.0
 */
@Entity
@Table(name = "excel_util4j_user_info")
public class ExcelUtil4JUserInfoEntity {

    @Transient
    private Long id;

    @OneToOne // 必须使用hibernate的双向绑定,否则无法生成hibernate的POJO对象
    @JoinColumn(name = "user_id")
    private ExcelUtil4JUserEntity user;

    @SheetTitleName("姓名")
    @NotBlank
    @Size(max = 20, message = "输入最大{max}个字")
    private String name;

    @SheetTitleName("性别")
    @ValueMap("{'男':1,'女':0}") // Map值转换器,将excel的只按照Map映射进行转换
    private Integer gender;

    @SheetTitleName("生日")
    @BlankToNull // 空串转NULL转换器,防止生日字段为空串转换为Date时报错
    @DateFormat("yyyy-MM-dd") // 时间格式转换器,将时间转换为指定格式,如果单元格为“文本”就会使用
    private Date birthday;

    @SheetTitleName("电话")
    // hibernate validator的正则验证,这里大概写一个电话的验证正则
    @Pattern(regexp = "(^(\\+|0)[0-9]{2}[0-9]{11}$)|(^[0-9]{11}$)", message = "填写不正确")
    @Validate(validator = PhoneUniqueValidator.class, message = "已被注册")
    private String phone;

    @SheetTitleName("地址")
    @Size(max = 50, message = "输入最大{max}个字")
    private String address;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Basic
    @Column(name = "name")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Basic
    @Column(name = "gender")
    public Integer getGender() {
        return gender;
    }

    public void setGender(Integer gender) {
        this.gender = gender;
    }

    @Basic
    @Column(name = "birthday")
    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    @Basic
    @Column(name = "phone")
    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Basic
    @Column(name = "address")
    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @OneToOne
    @JoinColumn(name = "user_id")
    public ExcelUtil4JUserEntity getUser() {
        return user;
    }

    public void setUser(ExcelUtil4JUserEntity user) {
        this.user = user;
    }
}
  • user_role pojo
package online.dinghuiye.example.entity;

import online.dinghuiye.core.annotation.convert.ConstValue;
import online.dinghuiye.core.annotation.excel.Transient;

import javax.persistence.*;

/**
 * @author Strangeen on 2017/08/27
 */
@Entity
@Table(name = "excel_util4j_user_role")
public class ExcelUtil4JUserRoleEntity {

    @Transient
    private Long id;

    private ExcelUtil4JUserEntity user;

    @ConstValue("3") // 导入的用户角色均为3
    private Integer roleId;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @OneToOne
    @JoinColumn(name = "user_id")
    public ExcelUtil4JUserEntity getUser() {
        return user;
    }

    public void setUser(ExcelUtil4JUserEntity user) {
        this.user = user;
    }

    @Basic
    @Column(name = "role_id")
    public Integer getRoleId() {
        return roleId;
    }

    public void setRoleId(Integer roleId) {
        this.roleId = roleId;
    }
}
  1. 编写CurrentTimeConvertor转换器代码:
    自定义转换器需要实现online.dinghuiye.api.resolution.Convertor
package online.dinghuiye.example.convertor;

import online.dinghuiye.api.resolution.Convertor;

import java.lang.reflect.Field;
import java.util.Date;
import java.util.Map;

/**
 * 当前时间转换器,该转换器为自定义转换器,用于适应字段为当前时间的情况
 * 自定义转换器需要实现online.dinghuiye.api.resolution.Convertor
 *
 * @author Strangeen on 2017/09/04
 * @version 2.1.0
 */
public class CurrentTimeConvertor implements Convertor {

    // convet方法参数会传入所有可能用到的值
    // obj 需要转换的值
    // field pojo属性字段
    // excelRecordMap excel数据map<表头名称, 单元格值>
    @Override
    public Object convert(Object obj, Field field, Map excelRecordMap) {

        // 返回当前时间即可,自定义转换器也可以用于其他特定值得转换
        return new Date();
    }
}
  1. 编写UniqueValidator验证器代码(UsernameUniqueValidatorPhoneUniqueValidator验证器代码略,请查看示例代码v2.1.0版本):
    自定义判重验证器需要实现online.dinghuiye.api.validation.Validator
package online.dinghuiye.example.validator;

import online.dinghuiye.api.validation.Validator;
import online.dinghuiye.example.util.SessionFactoryUtil;
import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 判重Validator
 *
 * @author Strangeen on 2017/08/16
 */
public class UniqueValidator implements Validator {

    /*
    实现原理是将数据库中唯一值全部读取出来缓存到cache中,
    然后将导入的只和cache比对,如果重复则返回false,如果不重复则加入到cache,返回true

    这样做可以提高检验效率,但是必须要考虑并发问题
     */

    private static Set usernameCache = new HashSet<>();
    private static Set phoneCache = new HashSet<>();

    public UniqueValidator() {
        // 设置username的cache
        setCache(usernameCache, "excel_util4j_user", "username");
        // 设置phone的cache
        setCache(phoneCache, "excel_util4j_user_info", "phone");
    }

    public static void setCache(Set cache, String tableName, String columnName) {
        SessionFactory factory = SessionFactoryUtil.getSessionFactory();
        Session session = factory.openSession();
        SQLQuery query = session.createSQLQuery("select " + columnName + " from " + tableName);
        List list = query.list();
        for (Object obj : list) {
            cache.add(obj);
        }
        session.close();
    }

    @Override
    public  boolean validate(Object fieldValue, Field field, User obj) {

        // 判断是username还是phone,这里只是演示,所以将cache写在一起,常规思路应该是分开的2个类
        if ("username".equals(field.getName())) {
            if (usernameCache.contains(fieldValue)) return false;
            usernameCache.add(fieldValue);
            return true;
        } else if ("phone".equals(field.getName())) {
            if (phoneCache.contains(fieldValue)) return false;
            phoneCache.add(fieldValue);
            return true;
        }

        // 其他字段不用检测,直接返回true
        return true;
    }
}
 
 
  1. 编写PasswordRepairer修正器代码:
    在存表前将密码设置为密码明文的加密字符串,MD5Util的代码略
package online.dinghuiye.example.repairer;

import online.dinghuiye.api.entity.Process;
import online.dinghuiye.api.entity.ResultStatus;
import online.dinghuiye.api.entity.RowRecord;
import online.dinghuiye.api.persistence.RowRecordPerPersistentRepairer;
import online.dinghuiye.example.entity.ExcelUtil4JUserEntity;
import online.dinghuiye.example.util.MD5Util;

import java.util.List;

/**
 * 对密码进行MD5加密处理
 * 由于密码需要验证长度,所以不能在验证前就MD5加密,否则验证是不正确的
 * 所以需要在存表前进行修正
 *
 * 通过实现RowRecordPerPersistentRepairer可以获得hibernate的POJO对象,从而进行修正
 *
 * v2.1.0 进度监控更佳精确,接口提供了进度对象
 *        如果遍历了List list,可以对每一次循环执行process.updateProcess(1)
          需要注意的是,使用前必须判断`process`是否为`null`,如果入口方法出传入的`ProcessObserver`为`null`,那么`process`就会为`null`
 *        如果没有遍历或者不执行上述方法,当repairer执行完毕,程序会自动修正进度,
 *           进度展示效果会立即变更到repairer方法执行完毕的进度状态
 *
 * @author Strangeen on 2017/9/3
 * @version 2.1.0
 */
public class PasswordRepairer implements RowRecordPerPersistentRepairer {

    @Override
    public void repaire(List list, Process process) {
        for (RowRecord rr : list) {
            if (rr.getResult().getResult() != ResultStatus.SUCCESS) continue;
            ExcelUtil4JUserEntity obj =
                    (ExcelUtil4JUserEntity) rr.getPojoRecordMap().get(ExcelUtil4JUserEntity.class);
            obj.setPassword(MD5Util.encode(obj.getPassword()));

            // 精确的进度展示,可以操作process对象
            if (process != null)
                process.updateProcess(1);
        }
    }
}
  1. 编写入口代码:
    配置SessionFactory的代码略,可以使用多种方式配置,如Spring等
package online.dinghuiye.example;

import online.dinghuiye.api.entity.Process;
import online.dinghuiye.api.entity.ResultStatus;
import online.dinghuiye.api.entity.RowRecord;
import online.dinghuiye.api.entity.TransactionMode;
import online.dinghuiye.core.ImportHandler;
import online.dinghuiye.core.persistence.RowRecordPersistencorHibernateImpl;
import online.dinghuiye.core.resolution.torowrecord.RowRecordHandlerImpl;
import online.dinghuiye.core.validation.RowRecordValidatorImpl;
import online.dinghuiye.example.entity.ExcelUtil4JUserEntity;
import online.dinghuiye.example.repairer.PasswordRepairer;
import online.dinghuiye.example.util.SessionFactoryUtil;
import online.dinghuiye.excel.ExcelFactory;
import org.hibernate.SessionFactory;

import java.io.File;
import java.util.List;
import java.util.Observable;
import java.util.Observer;

/**
 * @author Strangeen on 2017/08/30
 *
 * @author Strangeen on 2017/9/3
 * @version 2.1.0
 */
public class ExcelImportor {

    public static void main(String[] args) {

        SessionFactory factory = null;
        try {

            // 获取SessionFactory
            factory = SessionFactoryUtil.getSessionFactory();
            // 设置mode:SINGLETON为单条存储事务,MULTIPLE为整体事务,详见文档
            TransactionMode mode = TransactionMode.SINGLETON;

            // 创建导入器handler
            ImportHandler handler = new ImportHandler();
            handler.setHandler(new RowRecordHandlerImpl()); // 一对一关系解析器
            handler.setValidator(new RowRecordValidatorImpl()); // 验证器
            handler.setPersistencor(new RowRecordPersistencorHibernateImpl(factory)); // 持久化器hibernate实现
            handler.setRepairer(new PasswordRepairer()); // 密码存储修正器
            handler.setMode(mode);

            // 执行excel导入
            List resultList = handler.importExcel(
                    ExcelFactory.newExcel(new File("D:/test_template.xlsx")), // 创建AbstractExcel对象读取excle
                    0, // 读取sheet序号为0的sheet
                    new Observer() {
                        @Override
                        public void update(Observable o, Object arg) {
                            // 创建导入进度观察者,arg为导入进度百分数(没有%)
                            Process process = (Process) arg;
                            System.out.println("进度:" + process.getProcess() + ",当前阶段:" + process.getNode());
                        }
                    },
                    ExcelUtil4JUserEntity.class); // 传入POJO

            // 打印结果,如果有错误可以在resultList中得到
            int successCount = 0;
            int errorCount = 0;
            for (RowRecord rr : resultList) {
                if (rr.getResult().getResult() != ResultStatus.SUCCESS) { // 导入不成功
                    System.out.println(rr.getRowNo() + "行 - " + rr.getResult().getMsg()); // 打印行号和错误信息
                    errorCount ++; // 记录错误数
                } else
                    successCount ++; // 记录成功数
            }
            // 注意:MULTIPLE为整体事务,successCount依然可能不为0,仅作为标识,实际上没有任何数据存入数据库的
            System.out.println("success " + successCount + ", error " + errorCount);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            SessionFactoryUtil.closeSessionFactory(factory);
        }
    }
}

至此,代码全部编写完毕,执行导入后,控制台打印出来的内容类似如下(我导入了10条数据,有6条存在问题,使用SINGLETON事务形式):

进度:2.5,当前阶段:RESOLUTION
进度:5.0,当前阶段:RESOLUTION
进度:7.5,当前阶段:RESOLUTION
进度:10.0,当前阶段:RESOLUTION
进度:12.5,当前阶段:RESOLUTION
进度:15.0,当前阶段:RESOLUTION
进度:17.5,当前阶段:RESOLUTION
进度:20.0,当前阶段:RESOLUTION
进度:22.5,当前阶段:RESOLUTION
进度:25.0,当前阶段:RESOLUTION
进度:27.500000000000004,当前阶段:VALIDATION
进度:30.0,当前阶段:VALIDATION
进度:32.5,当前阶段:VALIDATION
进度:35.0,当前阶段:VALIDATION
进度:37.5,当前阶段:VALIDATION
进度:40.0,当前阶段:VALIDATION
进度:42.5,当前阶段:VALIDATION
进度:45.0,当前阶段:VALIDATION
进度:47.5,当前阶段:VALIDATION
进度:50.0,当前阶段:VALIDATION
进度:52.5,当前阶段:REPAIRATION
进度:55.00000000000001,当前阶段:REPAIRATION
进度:57.49999999999999,当前阶段:REPAIRATION
进度:60.0,当前阶段:REPAIRATION
进度:77.5,当前阶段:PERSISTENCE
进度:80.0,当前阶段:PERSISTENCE
进度:82.5,当前阶段:PERSISTENCE
进度:85.0,当前阶段:PERSISTENCE
进度:87.5,当前阶段:PERSISTENCE
进度:90.0,当前阶段:PERSISTENCE
进度:92.5,当前阶段:PERSISTENCE
进度:95.0,当前阶段:PERSISTENCE
进度:97.5,当前阶段:PERSISTENCE
进度:100.0,当前阶段:PERSISTENCE
4行 - 账号不能为空;密码输入6~20个字;姓名不能为空;电话填写不正确;地址输入最大50个字;
5行 - 电话填写不正确;
7行 - 电话已被注册;账号已被注册;
8行 - 姓名不能为空;账号不能为空;
10行 - 密码不能为空;
11行 - 姓名不能为空;
success 4, error 6

你可能感兴趣的:(Java导入Excel到数据库工具——EXCEL-UTIL4J使用示例)