登录
首页 >  数据库 >  MySQL

导入图片引发出的对图片、视频、文档等上传的思考

来源:SegmentFault

时间:2023-02-16 15:38:39 177浏览 收藏

哈喽!今天心血来潮给大家带来了《导入图片引发出的对图片、视频、文档等上传的思考》,想必大家应该对数据库都不陌生吧,那么阅读本文就都不会很困难,以下内容主要涉及到MySQL、拦截器、windows-server、java-web、poi,若是你正在学习数据库,千万别错过这篇文章~希望能帮助到你!

导读

我们在开发的过程中,经常会遇到导入和导出:

  1. 从哪里导入到哪里?我们在客户端选择上传Excel文件,同时调用服务端的某个接口。服务端通过HttpServletRequest获取Excel的数据流,通过poi的相关操作获取单元格的数值,并填充到相应的javabean的实例化对象中。再调用事务的保存方法,利用hibernate框架或mybatis框架,将对象数据保存到数据库中。
  2. 从哪里导出到哪里?从服务器上的数据库的数据导出到本地,并以Excel文件的方式存储。我们在客户端选择导出Excel文件,同时调用服务端的某个接口。服务端通过HttpServletResponse响应客户端的请求,同时,调用事务层的查询方法,拿到待导出的数据源,过滤我们想要的数据。调用poi的相关操作,将数据填充到Excel表中。

导入

这里以导入材料为主,材料中存有图片,如图所示:

材料图片

你会发现,Excel表中存储的是图片在服务器上的路径,为什么存储的是图片在服务器的路径,而不是图片的字节码数据?

我们都知道任何文件都可以按照字节码的方式存储,比如视频文件、音乐文件、图片文件、GIF文件、文本框文件等。但是字节码的存储和读取都占用内存,如果在大批量的导入和导出的情况下,势必会占用JVM内存,造成资源阻塞。

因而,我们存储的是图片的路径,这还不是随随便便的路径,而是其所在服务器的路径。为什么选择路径。从上图中的图片路径来看,路径的字符比较短。占用的内存比较少,存储和读取相对来说快。因而,我们读入的图片的路径。比如上图中ENGINE_PLATFORM1TENANTTHUMBNAILTENANT-LOGO_1_1534415695498_1.jpg的图片:

示例中的图片

其所对应的服务器的图片地址是http://cw.rosunn.com/upload/i...。因而,我们只要在数据库中存储/ENGINE_PLATFORM/1/TENANT/THUMBNAIL/TENANT-LOGO_1_1534415695498_1.jpg这部分路径就可以的。我们的域名前缀http://cw.rosunn.com/是固定的,其所对应的图片的文件夹upload是固定的,该文件夹下有很多的图片文件夹。每个图片的文件夹都是不一样的。如图所示:

clipboard.png

因为我们是Windows服务器,所以服务器是Windows界面化操作。其实,一台电脑就是一台服务器,要不然,怎么说本地服务器呢?在该文件夹下,有三个子文件夹。

  1. attach文件夹,存储与附件相关的文件夹。
  2. image文件夹,存储图片的文件夹
  3. uEditor文件夹,前端会使用uEditor框架,这是多文本编辑器,可以上传图片、视频等。

以后,可能会有视频文件夹,如果做教学软件的话。不管是存储图片文件和附件文件,还是视频文件,我们在数据库中都只存储该文件路径。当我们从数据库中读出图片到前台页面,我门只要拿到其存储的路径,并在前端做如下配置即可:

http://域名/upload/图片在数据...

明白这一点,我们就好往下进行,当我们点击前端代码的导入按钮时,如图所示:

clipboard.png

其首先会进入到拦截器,然后再进入到服务器的三层架构中。


服务器的三层架构

我们常说服务器有三层架构,即dao层,service层,controller层。实际上这是个通俗的概念,然而,在真正的开发过程中,并非只有三层架构,其中还会有拦截器的概念。如果你用servlet开发,会涉及到过滤器。拦截器和过滤器的功能是一样的,只不过用法是不一样的。它俩到底有什么区别,我想网上的博客非常多。这里就不在细说了。也许,你可以参考这篇博客:拦截器(Interceptor)和过滤器(Filter)的执行顺序和区别

一般项目启动后,首先进入的不是controller层,而是拦截器,controller层只是针对接口而言的。

拦截器,听名知其意,主要做数据的过滤和拦截。对于数据库中不时常改变的数据,比如系统变量和数据字典等,我们可以放到拦截器的缓存中,当我们加载数据字典时,不必再从数据库中读取,而是读取缓存的数据字典。这样,减少了与数据库的连接,从而提升了效率。

曾经在实习时,有个老大教我,说影响服务端的效率一般是db操作、网络调用操作、线程、JVM优化等。至于,我们是用++i,还是i++,哪个效率高一点。当然,是++i效率高一点。i++内部会有一个临时变量,其存储的i改变前值,然后再执行i = i + 1,返回的也是临时变量。++i直接执行的是i = i + 1,并返回改变后的值。但是,不会考虑到这个问题,因为它的影响微乎其微。

同时,我们每打开一个页面,都要经过拦截器,有些页面需要登录才能看,有些页面可以不用登录。这就是拦截器的作用。

我们这个项目使用的是Apache Shiro框架。其是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理的拦截器框架。

除了,我们登录后台需要身份验证,需要shiro的拦截。或者,我们调用第三方支付接口,其要回调我们的接口。但是,我们对每个接口,都要进行拦截,防止其恶意攻击。此时,我们需要忽略第三方回调的我们的接口,也就是说,这个接口不在我们的拦截范围之内,如下代码:

备注,因为涉及到隐私,部分代码省略,或以 ** 代替,望请见谅。

http://域名/upload//image/sys...。如果其在服务器中存在,我们就创建图片的对象,图片的属性remoteRelativeUrl,存储该路径的后半部分,也就是相对路径,并保存到数据库中,返回一个图片对象。并把图片对象放到集合中,然后保存到材料对象中。材料对象再保存到数据中,这就完成一次导入。但是jsonObjectList可能有多个对象,再遍历一次,直到遍历所有的对象。

如下是材料的java类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "zq_material")
public class Material extends BaseObj {

/**
 * 材料名称
 */
@Column(name = "material_name")
private String materialName;

/**
 * 单位
 */
@ManyToOne
@JoinColumn(name = "unit_code")
private DataDict unit;

/**
 * 零售价
 */
@Column(name = "retail_price", precision = 12, scale = 2)
private BigDecimal retailPrice;

/**
 * 状态
 */
@Enumerated(EnumType.STRING)
private MaterialStateEnum status;

/**
 * 浏览量
 */
@Column(name = "page_view")
private Long pageView;

/**
 * 图片
 */
@ManyToMany(fetch = FetchType.EAGER)
@Fetch(FetchMode.SELECT)
@JoinTable(
        name = "zq_material_pictures",
        joinColumns = {@JoinColumn(name = "zq_material_id")},
        inverseJoinColumns = @JoinColumn(name = "core_picture_id")
)
@JSONField(serialize = false)
private List pictureList = new ArrayList();

/**
 * 材料标签
 */
@ManyToMany(fetch = FetchType.EAGER)
@Fetch(FetchMode.SELECT)
@JoinTable(
        name = "zq_material_tag",
        joinColumns = {@JoinColumn(name = "zq_material_id")},
        inverseJoinColumns = @JoinColumn(name = "core_data_dict_code")
)
@JSONField(serialize = false)
private List tagList = new ArrayList();

/**
 * 备注
 */
@Column(name = "note", columnDefinition = "longtext")
private String note;

}

为何向上遍历三次

我们看一下字节码文件在服务器的位置,如图所示:

图片所在的服务器的位置

由图可知。一次次的向上遍历,只为找到根路径,也就是http://域名。这是Tomcat的配置。然后再配置upload文件夹,即http://域名/upload


导入的执行效率

以上导入可以分为两种方式。一种是如果导入的数据中,但凡有一条数据不成功,所有的数据都无法导入。这就涉及到了事务一致性的问题。因而,我们需要放在事务层,也就是service层。为什么spring直到service层是事务层,这和我们的框架配置有关,把service层定义为事务层。如果某一条数据导入失败,并不影响其他数据的导入,我们可以放在controller层。

但是,如果处理的不当,便影响导入的执行效率。为什么这么说?比如,我们现在导入的是材料,材料有单位。单位放置在数据字典中,假设单位有16条数据,如图所示:

材料单位图

假如jsonObjectList的集合有1000条数据。我们每次遍历jsonObjectList集合,都要创建一次查询,也就是与数据库创建一次连接,保存之后再关掉连接,势必会减低导入效率:

//单位 String unitValue = json.getString("I"); if (isNotNull(unitValue)) { List units = dataDictService.getDataDictList("unit").getResultData(); if (null != units && units.size() > 0) { for (DataDict unit : units) { if (unit.getValue().equals(unitValue)) { dbMaterial.setUnit(unit); break; } } } }

我们从数据查找出当前单位的行数据,封装成我们想要的数据字典的对象。此时与数据库建立连接和释放数据库的连接,最多需要16000次,这势必会会增加服务器的资源,降低导入的执行效率。最少也需要1000次。

同时,系列也是来源于数据字典,然而,系列是以逗号分割的字符串,也就是说,我们需要将字符串分割成数组,再遍历这个数组,获取数据字典的对象,此时,最少语句数据库的连接数为16000,最多就不大清楚了。因而,严重降低导入的效率。

我们为什么不采用最少的呢?因而,我们在遍历jsonObjectList 之前,就从数据库中的加载出所有的单位的数据字典的集合,同时,也加载出系列的集合。放置在map的键值对当中,根据key值来取value值,如代码所示:

根据数据字典的父code值加载出所有的子code对象。

 /**
 * Created By zby on 14:12 2019/3/24
 * 将dict封装成map
 */
private Map dict2Map(String parentCode) {
    Map dictMap = null;
    if (StringUtils.isNotBlank(parentCode)) {
        List units = dataDictService.getDataDictList(parentCode).getResultData();
        dictMap = new HashMap();
        if (!CollectionUtils.isEmpty(dictMap)) {
            for (DataDict dict : units) {
                dictMap.put(dict.getValue(), dict);
            }
        }
    }
    return dictMap;
}

对于上面的一串代码,我们省略其他的代码,只加载和数据字典相关的代码,于是乎,得到:

/**
 * Created By zby on 17:35 2019/2/20
 * 导入
 */
@RequestMapping(value = "/import", method = RequestMethod.POST)
public Result importMaterials(HttpServletRequest request) {
    JSONObject body = new JSONObject();
    int totalNum = 0, successNum = 0;
//        单位
    Map unitDict = dict2Map("unit");
//        系列
    Map tagDict = dict2Map("material_tag");

    synchronized (this) {
        try {
            List jsonObjectList = PoiUtil.importSimpleExcel(request, 1, "P");
            if (null != jsonObjectList || jsonObjectList.size() > 0) {
                totalNum = jsonObjectList.size();
                for (JSONObject json : jsonObjectList) {
                     //单位
                    String unitValue = json.getString("I");
                    if (StringUtils.isNotBlank(unitValue)) {
                        dbMaterial.setUnit(unitDict.get(unitValue));
                    }
                    //系列
                    String tags = json.getString("N");
                    if (isNotNull(tags)) {
                        String[] tagArr = StringUtils.split(tags, ",");
                        List tagDicts = new ArrayList();
                        for (String tag : tagArr) {
                            if (StringUtils.isNotBlank(tag)) {
                                tagDicts.add(tagDict.get(tag));
                            }
                        }
                        dbMaterial.setTagList(tagDicts);         
                    }
                    materialService.saveUpdateMaterialAccount(dbMaterial);
                    successNum++;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    body.put("totalNum", totalNum);
    body.put("successNum", successNum);
    body.put("errNum", totalNum - successNum);
    return ResultUtil.buildSuccess(body);
}

这就是数据库导入优化,但是导入图片,和我们上传图片、视频、文档有关系吗?


图片、视频、附件上传

我们在做java开发时,势必会涉及到文件操作。我们一般会上传图片、文件、视频等,但它们以什么样的格式存储。正如上面提到的,我们上传图片、视频、附件等,会在服务器上创建一个文件夹,他们存储在该文件夹中。我们只要获取文件夹的相对路径即可,就能将其加载出来,这样比较节省数据库的资源。如图所示:

删除图片、视频、附件等

这一般都是异步上传,先将文件的路径以对象的保存到数据库中,再返回文件被保存后的带有主键id的对象。我们拿到持久态的文件对象后,在前端页面展示出来。因而,我们在保存材料时,前端只要向后端传输文件的id,或者是文件.id即可,比如 logo.id。spring会自动创建该文件对象,并将id到注入文件对象中。


总结

我们在开发过程中,要知其然,知其所以然。

以上就是《导入图片引发出的对图片、视频、文档等上传的思考》的详细内容,更多关于mysql的资料请关注golang学习网公众号!

声明:本文转载于:SegmentFault 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>
评论列表