上次做项目时要求数据源改为Excel,与项目一起打包执行。查了一下数据源是否可以直接使用Excel的解决方案,虽然有提供可以连接Excel的驱动,但是以Excel直接操作数据实在是不方便,没有数据库的功能强大,因此想到了使用了内嵌式数据库,再编写一段上传Excel把数据更新到数据库的代码,即可同样达到数据源与项目打包的效果。
一、解决思路
要求:数据源为Excel,与项目一起打包
方法:使用内嵌式数据库,后台处理上传Excel数据至数据库
内嵌式数据库就是不需要像Mysql/Oracle一样安装启动服务,而是在项目中直接引用它的jar包即可,它会以文件的形式存在于数据库中。在此项目中我使用了sqlite3。
上传Excel数据至数据库这一块我使用了fileUpload和poi,前者是用于上传文件,后者用于从Excel中读取数据;数据库连接我采用了SpringJdbcTemplate。好啦,以下是详细的步骤。
二、详细步骤
1、安装内置数据库sqlite3
1)首先从官网下载,我是在Windows下开发的,虽然后续是在linux环境下运行,但是sqlite3它可以自己跨平台,所以我直接下载windows版本就好了。
2)解压,然后创建一个sqlite3类型的文件,在命令提示符中使用sqlite3 DatabaseName.db的命令即可在当前目录下创建一个sqlite3类型的数据库
3)你可以用Navicat等可视化工具连接数据库,也可以把数据库直接复制到项目中,然后在applicationContext配置文件中配置连接信息即可。我这里采用了C3P0连接池。如果是放在resources下面,URL可以像我这样写,另外sqlite3的所使用的驱动是org.sqlite.JDBC。用户名和密码可以不使用。
测试一下,可发现数据库能够正常连接
2、Excel读取数据并写入数据库
poi提供了Excel读取的了解决方案,有一点要注意的是,分为HSSF和XSSF,其中HSSF是针对的office2007之前的版本xls,而XSSF针对的是office2007以后的版本xlsx,我这里采用的是XSSF。
思路:上传文件存放至临时地点——读取Excel数据封装成List——清空数据库之前的数据——插入数据到数据库
读取Excel数据封装成List,主要思想就是遍历每个sheet,再在每个sheet中遍历行,取列值,每行封装成一个对象,每个sheet又封装成一个list,批量插入数据库,下面直接上代码。
读取Excel.java
public class UploadExcel {
public static List<Ammeter> readXlsx(String path) throws Exception {
List<Ammeter> list = new ArrayList<Ammeter>();
InputStream is = new FileInputStream(path);
XSSFWorkbook xssfWorkbook = new XSSFWorkbook(is);
for (Sheet xssfSheet : xssfWorkbook) {
if (xssfSheet == null) {
continue;
}
for (int rowNum = 1; rowNum <= xssfSheet.getLastRowNum(); rowNum++) {
Row xssfRow = xssfSheet.getRow(rowNum);
if (xssfRow == null) {
continue;
}
List<Ammeter> rowList = new ArrayList<Ammeter>();
Ammeter ammeter = new Ammeter();
int len=0;
Cell cell = xssfRow.getCell(0);
if (cell != null) {
cell.setCellType(Cell.CELL_TYPE_STRING);
}
ammeter.setTabNumber(cell.getStringCellValue());
cell = xssfRow.getCell(1);
if (cell != null) {
cell.setCellType(Cell.CELL_TYPE_STRING);
}if((len=cell.getStringCellValue().length())>6){
ammeter.setActivePower(cell.getStringCellValue().substring(0,6));
}else{
ammeter.setActivePower(cell.getStringCellValue());
}
cell = xssfRow.getCell(2);
if (cell != null) {
cell.setCellType(Cell.CELL_TYPE_STRING);
}if((len=cell.getStringCellValue().length())>6){
ammeter.setReactivePower(cell.getStringCellValue().substring(0,6));
}else{
ammeter.setReactivePower(cell.getStringCellValue());
}
cell = xssfRow.getCell(3);
if (cell != null) {
cell.setCellType(Cell.CELL_TYPE_STRING);
}if((len=cell.getStringCellValue().length())>6){
ammeter.setCumulativePower(cell.getStringCellValue().substring(0,6));
}else{
ammeter.setCumulativePower(cell.getStringCellValue());
}
cell = xssfRow.getCell(4);
if (cell != null) {
cell.setCellType(Cell.CELL_TYPE_STRING);
}if((len=cell.getStringCellValue().length())>6){
ammeter.setVoltage(cell.getStringCellValue().substring(0,6));
}else{
ammeter.setVoltage(cell.getStringCellValue());
}
cell = xssfRow.getCell(5);
if (cell != null) {
cell.setCellType(Cell.CELL_TYPE_STRING);
}if((len=cell.getStringCellValue().length())>6){
ammeter.setElectricity(cell.getStringCellValue().substring(0,6));
}else{
ammeter.setElectricity(cell.getStringCellValue());
}
cell = xssfRow.getCell(6);
if (cell != null) {
cell.setCellType(Cell.CELL_TYPE_STRING);
}
ammeter.setCurrentTime(cell.getStringCellValue());
list.add(ammeter);
}
}
return list;
}
}
这里有几个点要注意一下:
1)我直接跟业务耦合了,但其实可以抽取出来作为一个工具类的;
2)读取的时候我直接将Excel单元格的类型强制转换为字符串类型,使用的cell.setCellType(Cell.CELL_TYPE_STRING),但是这个方法在新版的poi中的已经弃用了的,后续再看抽象出一个方法类的时候再写,现在先过;
3)Excel中使用下拉填充的数字读取出来有时会带若干小数,在这里我因为强制转换为字符类型,所以采用的subString进行截取,可以使用保留小数后再转换为字符串;如果使用我的subString的方法的话要先判断下字符串长度,避免产生index溢出的错误。
清空数据库Dao.java
//清空数据库
public void truncate(){
String sql = "delete from ammeter";
jdbcTemplate.execute(sql);
}
这里要注意的几个点:
1)mysql中可以采用truncate table tableName进行清空数据库,但是sqlite3里是不支持这样的命令的,只能使用delete from tabeName命令,但是这个命令是不能删除自增ID的,即假如你有100条数据,虽然清空掉了,但是插入数据的ID是从101开始的;
2)在JdbcTemplate里,execute()方法可用于执行任何sql语句,但是一般用来执行DDL语句。
插入数据库Dao.java
//插入数据到数据库
public void insert(List<Ammeter> ammeters){
final List<Ammeter> tempAmmeter=ammeters;
String sql = "insert into ammeter(tabNumber,activePower,reactivePower,cumulativePower,electricity,voltage,currentTime) values (?,?,?,?,?,?,?)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String tabNumber=tempAmmeter.get(i).getTabNumber();
String activePower=tempAmmeter.get(i).getActivePower();
String reactivePower=tempAmmeter.get(i).getReactivePower();
String cumulativePower=tempAmmeter.get(i).getCumulativePower();
String electricity=tempAmmeter.get(i).getElectricity();
String voltage=tempAmmeter.get(i).getVoltage();
String currentTime=tempAmmeter.get(i).getCurrentTime();
ps.setString(1, tabNumber);
ps.setString(2, activePower);
ps.setString(3, reactivePower);
ps.setString(4, cumulativePower);
ps.setString(5, electricity);
ps.setString(6, voltage);
ps.setString(7, currentTime);
}
@Override
public int getBatchSize() {
return tempAmmeter.size();
}
});
}
这里要注意的几个点:
1)采用了JdbcTemplate的批量插入方法batchUpdate(),一般来说update()方法用来执行增加、修改和删除等语句,batchUpdate()方法用来执行批处理相关的语句;这里有个疑问,虽然我使用了该批量插入方法,但是在执行5500多条数据时扔用了3-5min时间,具体原因暂未知;
2)如果是想要批量更新数据的话,可以采用临时数据库,先上传数据到临时数据库,再根据外键更新正式数据库,这样速度会快点。
3、客户端上传Excel
思路:浏览器上传Excel——存入项目的某个路径下——UploadExcel读取该路径下的Excel——执行操作
由于牵涉到前端页面,所以采用servlet和jsp;servet中用到了spring框架;上传文件的存放位置采用了相对路径,不能写死,避免了平台问题。直接上代码。
上传按钮.jsp
<form action="${pageContext.request.contextPath}/servlet/UploadHandleServlet" method="post" enctype="multipart/form-data" >
请选择上传文件<input type="file" name="file"><br>
<input type="submit" value="上传">
</form>
处理上传Servlet.java
@Controller
public class UploadHandleServlet extends HttpServlet {
@Autowired
private AmmeterDao ammeterDao;
public void init() throws ServletException {
/**
* 利用init方法来调用Spring容器BeanFactory
* 看看UserServlet是否能够通过Spring容器获取对象
*/
WebApplicationContext wc = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); //通过Web容器去得到BeanFactory对象
AutowireCapableBeanFactory autowireCapableBeanFactory = wc.getAutowireCapableBeanFactory();
autowireCapableBeanFactory.autowireBean(this);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
File file = new File(savePath);
if(!file.exists()&&!file.isDirectory()){
System.out.println(savePath+"目录不存在,需要创建");
file.mkdir();
}
String message = "上传成功";
try {
DiskFileItemFactory diskfactory = new DiskFileItemFactory();//创建工厂类对象
ServletFileUpload fileUpload = new ServletFileUpload(diskfactory);//使用工厂创建解析器对象
if(!ServletFileUpload.isMultipartContent(request)){
return;
}
List<FileItem> fileItems = fileUpload.parseRequest(request);
for(FileItem item:fileItems) {
if(item.isFormField()) {
System.out.println(new String(item.getString().getBytes("ISO-8859-1"),"utf-8"));
} else{
item.write(new File(savePath+"\\"+"temp.xlsx"));
System.out.println(message);
}
}
} catch (FileUploadException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
List<Ammeter> temp= null;
try {
temp = UploadExcel.readXlsx(savePath+"\\"+"temp.xlsx");
System.out.println(temp);
} catch (Exception e) {
e.printStackTrace();
}
ammeterDao.truncate();
ammeterDao.insert(temp);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
这里要注意的几个点:
1)由于Dao是使用了Spring框架用Autowired自动装配的,所以使用new是不会调用的,要想在servlet里使用自动装配,需要利用init方法来调用Spring容器BeanFactory,然后使用@Autowired自动装配;
2)使用fileupload的jar包。同时要有io
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.1</version>
</dependency>
3)不要把路径写死,要用相对路径; this.getServletContext().getRealPath("/WEB-INF/upload");这个路径指的是项目运行的真实路径,到时会在运行的Tomcat服务器下的webapps\ROOT\WEB-INF\upload产生文件;而且指定了文件名后覆盖,不用担心存储问题;
以上。