# CSV文件自动导入

# 目标读者

本文档的目标读者可能包括:

  • 本系统的开发及实施人员
  • 准备导入数据的用户数据管理员

# 功能说明

当前系统的数据支持在系统启动时,自动导入一组 CSV 文件

提示

本系统的 CSV 解析格式遵循 RFC 4180 (opens new window) 标准

支持的导入对象范围为:

  • 除去 DomainClass 对象外的所有系统中的对象定义,如用户、用户组、角色等权限相关数据及组织、合同、合同行、合同行定义等数据

如果你习惯使用一个例子来进行学习,可以直接查看一个完整的例子

提示

DomianClass 对象列表由系统启动时,自动从所有 Domain 对象定义自动生成并写入数据库

# 功能启用 开发

本功能默认启用,功能的启用无需进行配置,但本功能正常工作依赖导入文件路径的配置,具体叙述如下

# 配置导入文件路径 开发

需要在 application.yml 文件中配置 seedData.folder 选项,该选项指定了 CSV 文件的读取根路径, 如下展示了一个针对 development 环境的配置示例:

development:
    seedData:
      folder: ${SEED_DATA_FOLDER:/app/data}
1
2
3

提示

该配置会优先从环境变量 SEED_DATA_FOLDER 中读取

其目录结构示意简述如下, 该目录的第一层子目录对应不同的系统环境,如希望在 development 环境导入的所有 CSV 文件,需要放置在 development 子目录下,当前系 统已经预定义的环境如下所列:

在各个环境的目录下,不同的租户的种子文件存放在不同租户的子目录下。

  • development
  • testing
  • staging
  • production

在每个环境的数据目录下,会有一个 Organization.csv 文件,保存系统中首先导入的顶级 租户的列表,该文件将会导入每个租户下的顶级组织及租户标识,是导入所有分租户数据的 依据。

├── README.md
├── attachments --> 一般需要导入的附件放在该目录下,如 DyanmicTheme 使用到的 logo 等
│   ├── README.md
│   ├── background.jpeg
│   ├── favicon.png
│   └── logo.png
├── css --> 一般 DynamicTheme 的客制化 css 文件放在该目录下
│   └── README.md
├── development --> (development 环境) 
├── groovy --> 一般客制化 DynamicLogic 的源代码放在该目录下
├── review --> (review 环境)
│   ├── Organization.csv --> (最先导入的系统中所有顶级租户列表)
│   ├── README.md
│   ├── jiayu --> 对应顶级租户的租户名称
│   │   ├── DomainColumnClientSideTypeConfig.csv
│   │   ├── DynamicAction.csv
│   │   ├── DynamicActionDomainClass.csv
│   │   ├── DynamicActionDomainClass_jiayu.csv
│   │   └── UserGroup.csv
│   └── muyan --> 对应顶级租户的租户名称
│       ├── Group.csv
│       ├── GroupRole.csv
│       ├── Organization.csv
│       ├── RequestMap.csv
│       ├── Role.csv
│       ├── User.csv
│       └── UserGroup.csv
└── tree.txt
    
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

# 文件更新判断

在导入前,对于每个 CSV 文件,系统都会判断本次导入的 CSV 文件的 md5 码与上次导入的 CSV 文件的 md5 码是否相同,如果 两个文件的 md5 码相同,且上次导入的状态为成功,则系统会跳过本次导入。

# 数据准备及导入

以下章节描述了CSV文件数据准备相关的内容

# 文件名称

所有 CSV 文件需要以该对象的,不包含包定义的类名称开始,如 tech.muyan.Contract, tech.muyan.security.GroupRole 对象的导入 CSV 文件可以分别被命名为 Contract.csvGroupRole.csv,或者 Contract_2019.csvGroupRole_security.csv 等形式, .csv 后缀需要小写。

提示

建议在准备种子数据的 csv 文件时,不修改平台默认的 csv 文件,而采用新增加带 _xxx _后缀的 csv 文件的形式进行,以方便在平台版本更新时,直接更新平台自带的 csv 文件。

# 导入顺序及依赖

对于具有关联关系的一组数据,系统会自动根据模型定义中对象间的关联关系,识别出其之间的依赖关系,并在导入时, 自动将被依赖的数据在依赖数据之前导入。

如果需要特殊处理的依赖关系,请在 domain 定义中,通过 loadAfter 进行定义,如下 是 DynamicIntegration Domain 定义中的例子:

  //表示在导入 DynamicIntegration 之前,需要先导入 Organization, DynamicLogic, User 三种对象
  static loadAfter = [Organization, DynamicLogic, User]
1
2

如果某种对象应该首先被导入,为阻止系统从其 Domain 定义中解析出其依赖,可将 loadAfter 属性设置为空数组,如下是 DynamicLogic Domain 定义中的例子:

  static loadAfter = []
1

# 标题行

所有待导入的 CSV 文件中,第一行均应该是标题行,标题行描述了该 CSV 文件中,数据的结构, 以下是一个 Contract 对象导入的 CSV 文件标题行的示例及其说明

name(*),title,effectiveDate,ownerOrganization.name,Tags(#),Purpose(#),Total Amount(#),Seal Type(#),Contract Quantity(#),language,DELETE_FLAG
1
  • 在模型中定义的基本字段,列的名称是该对象在模型定义中的字段名称。
  • 在界面自定义的动态字段,列的名称是该字段在对象的界面上显示的 Label 字段的值。
  • 后缀 (\*) 的列,如 name(\*) ,是查询字段,其描述如后 查询字段 小节所述。
  • 包含 . 的列,如 ownerOrganization.name,是关联对象字段,其描述如后 关联对象的查询 小节所述。
  • 后缀 (#) 的列,如 Purpose(#),是动态字段,其描述如后 动态字段 小节所述。
  • 名为 DELETE_FLAG的列,是一个特殊列,该列的值可为 YN,为 Y时,表示系统在导入时,需要删除通过查询字段找到的对象。
  • 后缀为 (F) 的列,如 code(F), 表示该列的内容会从文件读取,文件的起始目录为 seedData.folder 所配置的目录
  • 如果某一列在 Domain 中定义的字段类型为 StorageFieldValue, 则该列中的值应该为待导入的附件文件的文件路径。

注意

在系统内部,这个字段名称保存在 DynamicFieldInstance 对象的 label 字段中, 要注意区分,这里不是 DynamicFieldDefinition 字段的 label 字段。

# 注释行

以英文分号(;) 开头的行是注释行,在导入时会被忽略,如下是一个包含注释的导入 CSV 文件的样例, 所有以 ; 开头的行,为注释行,不会影响导入过程。

name(*)
; Normal user, only have permission to login
USER
; Admin user
ADMIN
; Developer user
DEVELOPER
; Biz user, can operate on biz objects(contract)
BIZ_USER
; Biz Admin
BIZ_ADMIN
1
2
3
4
5
6
7
8
9
10
11

# 查询字段

为了支持通过 CSV 文件创建或更新现有记录,系统支持通过查询字段查询现有记录,所有在导入文件第一行的标题中, 后缀是 (\*) 的列,会被系统识别为查询字段。系统会根据所有查询字段的值,使用严格相等匹配,从系统中查询现有 记录,并进行如下的相应处理:

  • 如果查询结果为空,则系统会尝试新建一条数据并保存,并将导入记录中,该行数据的操作记录为新建。
  • 如果查询结果不为空且只存在一条记录,则系统会尝试更新该条数据, 并将导入记录中,该行数据的操作记录为更新。
  • 如果查询结果不为空且存在超过一条记录,则系统会跳过该行数据的导入,并将导入记录中,该行数据的操作记录为跳过。

提示

如果导入的 CSV 文件中,没有任何查询字段,则表示更新和删除功能不可用,导入时,所有的记录均会以新建方式处理。

# 关联对象的查询

系统支持通过关联对象字段,查询其关联对象,并将查询到的关联对象与待导入对象进行关联。

如上述字段 ownerOrganization.name, 表示在导入该对象的 ownerOrganzation 字段(该字段是一个 Organzation 对象)时, 对于每一行数据导入时,都使用 Organization 类型的 name 属性,根据 CSV 文件中该列的值去查询现有的 Organzation 对象记录, 并将查询到的记录与待导入的 Contract 对象关联起来,根据查询的结果不同,具体处理可能如下:

  • 如果查询到的关联对象不存在,则终止该行的导入,并将导入记录中,该行数据的操作记录为失败。
  • 如果查询到的关联对象有且只有一个,则继续该行的导入,具体最终结果取决于其他列的操作。
  • 如果查询到的关联对象有多个,则终止该行的导入,并将导入记录中,该行数据的操作记录为跳过。

提示

关联字段也可以作为查询字段

注意

在导入某对象,无论新建还是更新时,其所依赖的关联对象必须已经在系统中存在,否则该对象的导入将会失败

# 动态字段

导入数据时,系统同时支持在界面上建立的动态字段的值的导入,所有动态字段在导入的 CSV 文件的标题行中, 需要以 (#) 结尾。 动态字段值的保存发生在主对象的保存完成后。如上述示例中,CSV 文件中,标题为 Purpose(#) 的列,系统在导入时,系统会查询 Label 为 Purpose 的合同动态字段,并进行数据导入。

如果该字段是一个选择字段,那么在导入时,系统会校验 CSV 文件中, 该列的值是否在该字段的可选项范围中, 如果其值不在备选范围中,则会终止该行数据的导入,并将该行数据的导入标记为失败。

系统限制

因为动态字段的保存与主对象的保存不在同一个事务中,因此可能发生主对象保存成功,但动态字段保存失败的情况, 此时,主对象的状态与 CSV 文件中,会发生数据不同步。

# 特殊字符的转义

  • 如果某列的内容中,包含英文逗号(,),则该列需要使用英文引号(")包裹起来。
  • 如果某列的内容中,包含英文的引号,且该列处于引号的包裹中,则需要将其用\进行转义。

# 空白字符处理

在从 CSV 文件中读取数据时,系统会自动将每列前后的空白字符删除。

注意

如 CSV 文件中的列前后存在空白字符,则每次导入时,该行均会被识别为需要更新,且更 新后,数据库中保存的值,前后不包含空白字符。

# 对象删除

CSV 文件导入时,支持删除现有数据,通过如下的方式实现:

  • 在 CSV 文件的标题行中,增加 DELETE_FLAG
  • 对于要删除的行,在 CSV 文件中,将其 DELETE_FLAG 列的值设置为 Y

注意

CSV 文件的标题行中,必须指定了可以唯一确定一条记录的查询字段,对象的删除操作才能正确工作。

对于被标记为执行删除操作的行,下面是相关的错误处理逻辑:

  • 如果查询到的现有数据有0条,那么系统会跳过这一行的删除处理。
  • 如果查询到的现有数据有多条,那么系统会跳过这一行的删除处理。
  • 如果导入的 CSV 文件中没有查询字段,则系统会跳过所有标记为删除的行的处理。

# 数据类型映射

如下列出了系统当前支持导入的类型,及其导入后的数据类型

数据类型 导入后的数据类型
Boolean java.lang.Boolean
String java.lang.String
datetime java.time.LocalDateTime
date java.time.LocalDate
decimal java.math.BigDecimal
integer java.lang.Integer
long java.lang.Long
Enum java Enum
httpMethod org.springframework.http.HttpMethod
java class name Domain Object
StorageFieldValue tech.muyan.storage.StorageFieldValue

# Boolean

对于 Boolean 类型的字段,如下列出了值的对应关系, 如果 CSV 文件中的字段值不在如下的范围中, 则系统会将该行的导入标记为失败

CSV 文件中的值 导入后的值
Y, y, Yes, YES, true, TRUE, T, t, , 1 true
N, n, No, NO, false, FALSE, F, f, , 0 false

# 日期及时间

对于日期类型的字段,如下是支持的相关日期/时间类型列表,如下列表中的各字母所代表的时间、日期中的相关部分 可参考 SimpleDateFormat (opens new window)

 "yyyyMMdd" 
 "dd-MM-yyyy"
 "yyyy-MM-dd"
 "MM/dd/yyyy"
 "yyyy/MM/dd"
 "dd MMM yyyy"
 "dd MMMM yyyy"
 "yyyyMMddHHmm"
 "yyyyMMdd HHmm"
 "dd-MM-yyyy HH:mm"
 "yyyy-MM-dd HH:mm"
 "MM/dd/yyyy HH:mm"
 "yyyy/MM/dd HH:mm"
 "dd MMM yyyy HH:mm"
 "dd MMMM yyyy HH:mm"
 "yyyyMMddHHmmss"
 "yyyyMMdd HHmmss"
 "dd-MM-yyyy HH:mm:ss"
 "yyyy-MM-dd HH:mm:ss"
 "MM/dd/yyyy HH:mm:ss"
 "yyyy/MM/dd HH:mm:ss"
 "dd MMM yyyy HH:mm:ss"
 "dd MMMM yyyy HH:mm:ss"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 枚举类型

如果枚举类型中定义了可以从其显示值反向查询到枚举对象的 get 方法, 则系统支持在 CSV 文件里, 列值为其界面显示值的枚举数据列导入,否则系统只能支持 CSV 中值与其内部值完全一致的枚举数据导入。

# 附件类型

对于附件类型的列,准备 CSV 文件时,对应列需要填写从配置的 seedData.folder 的目 录起算的相对文件路径,导入时,系统会将该列导入为系统中的 StorageFieldValue 对 象,并建立必要的对象间关联关系。

# 根目录字段

如果某个字段为 tech.muyan.Organization 类型,可以使用 $ROOT_ORG$ 这个占位符来 指定引用到当前租户的根组织。

提示

本处的特殊处理是为了能够在不同租户间快速拷贝系统默认的相关种子文件,而无须针对每 个租户进行组织名称的替换。

# 支持新的数据类型映射 开发

所有导入时的数据类型映射均在 tech.muyan.importexport.converter.ConverterFactory 中定义,该类提供了一个registerConverter的工具方法,用于向系统中注册 CSV 导入时的数据映射实现,举例如下:

ConverterFactory.registerConverter(type -> type != null && type.contains(".enums."), new EnumConverter());
1
  • 如上方法注册了一个名为 EnumConverter 的数据映射实现,
  • 该实现启用的条件使用一个 predictor指定,该predictor接收一个字符类型的参数,该参数为:对待导入的领域模型类调用 DomainMetaController.get 方法返回的元数据中的 type 字段。
  • 如上述的 predictor 限定了该类型映射的条件为:该字段的 type 属性不为空,且包含 .enums. 部分。

# 查看导入记录

系统输入导入的记录保存在 ImportRecord 中,可以使用具有 /ImportRecord 地址的 GET 权限的用户进行查看, 默认情况下,具有 ROLE_ADMINROLE_DEVELOPER 角色的用户具有查看导入记录的权限。

如下的信息会被保存在导入记录中:

  • 导入对象的类型
  • 导入 CSV 文件的 MD5 码
  • 导入状态:可能为
    • 未开始
    • 成功
    • 失败
    • 部分成功
    • 运行中
  • 导入开始时间
  • 导入结束时间
  • 导入 CSV 文件的标题行
  • 插入的对象 id 列表
  • 更新的对象 id 列表
  • 删除的对象 id 列表
  • 失败行 CSV 中的原始信息
  • 跳过行 CSV 中的原始信息
  • 导入的 log 记录
  • 跳过的行数
  • 失败的行数
  • 成功更新的行数
  • 成功创建的行数
  • 成功删除的行数

# 一个完整的例子

如下展示了一个完整的系统可导入的 CSV 文件的示例,并基于此,讲解了上述相关设定、概念等。

CSV 文件名称: Contract.csv

CSV 文件内容示例:

name(*),title,effectiveDate,total,ownerOrganization.name(*),contractStatus,Tags(#),Purpose(#),Total Amount(#),Seal Type(#),Contract Quantity(#),Is Important(#),language,lines.name[:],DELETE_FLAG
牧言科技2020年笔记本采购合同,笔记本采购合同,2020-01-01 00:00:00,45000,牧言科技,Active,"[\"一次性合同\",\"低风险\"]","采购开发使用笔记本",35000,公章,3,是,contracts\language_2020.docx,"[line1:line2]",N
牧言科技2019年笔记本采购合同,笔记本采购合同,2020-01-01 00:00:00,45000,牧言科技,Active,"[\"一次性合同\",\"低风险\"]","采购开发使用笔记本",35000,公章,3,是,contracts\language_2019.docx,"[line3:line4]",Y
1
2
3

# 文件说明

  • 文件名到对象的映射:该文件名称为 Contract.csv,则系统会从 DomainClass 中查询系统中定义的所有领域模型,查询到有不包含 Package 的名称为 Contract 的模型定义,则决定该文件对应的导入对象为 Contract

  • 现有记录查找:系统会根据标题行字段名以(*)结尾的字段: nameownerOrganization.name 进行组合,使用相等匹配,查询系统中的现有记录,如果根据这两个信息查询到数据,则会更新现有记录,否则会创建新的记录。

  • 关联对象查询及依赖排序:从标题行可以看出,ownerOrganization.name 字段中包含 . 字符,说明该字段为一个关联对象字段。

    • 该字段在系统中是 tech.muyan.Organization对象,故如果系统同时检测发现存在 Organization.csv文件,则会自动进行排序,将 Organization.csv 文件在 Contract.csv 文件之前导入
    • ownerOrganization.name 可以判断,系统中导入时,查询 Contract 关联的 Organization 对象时,会使用其 name 字段进行匹配查询,并将查询到的 Organization 对象与待导入的 Contract 对象进行关联。
  • 动态字段:从标题行可见,Tags, Purpose, Total Amount, Seal Type, Contract QuantityIs Important 字段以(#)结尾,故均为动态字段,系统会查找系统中定义的,Label 为相应值的Contract对象的动态字段,并相应的更新相关动态字段的值。

# 字段说明

  • effectiveDate

    • 该字段是日期类型,其值 2020-01-01 00:00:00 符合 yyyy-MM-dd HH:mm:ss格式定义,故系统可以正常解析并导入该数据。
  • contractStatus

    • 该字段是一个枚举类型的数据,其值 Active ,是该枚举类型 ACTIVE 的显示值,这要求系统实现了从其显示值 Active 反查其定义值 ACTIVEget 方法,如果没有实现从显示值反差枚举值的方法,则在 CSV 文件中,必须填写 ACTIVE,系统才可正常识别并导入该数据。
  • Tags

    • 因为该字段的值中包含,字符,故需要使用英文引号"进行包裹。
    • 因为该字段的值中包含了英文引号",故该字符需要使用转义字符\进行转义。
    • 在系统中,Tags字段被定义为一个多选字段,从第二行的值可见,Tags 字段的值为合法的 JSON 字符串,该合同的 JSON 字符串会被保存到动态字段的 jsonValue 列。
    • 系统在导入时,会校验该字段JSON 字符串中的所有选项(此处为:一次性合同低风险) 均为系统中预定义的,合法的备选项。
  • Is Important

    • 该字段是一个 Boolean 类型的字段,根据Boolean类型转换列表,其值在导入时,会被转换为 true
  • language

    • 该字段是一个 StorageFieldValue 字段,故系统在导入时,会将该列的值 contracts\languageg_2020.docxcontracts\languageg_2019.docx 对应的 文件作为 StorageFieldValue 对象导入,创建 StorageFieldValue 对象,并建 立其与主 Contract 对象之间的关联关系。
  • lines.name[:]

    • 表示该字段是一个列表字段,用于导入 lines 字段对象,系统会根据该字段中每 个条目的 name 属性的值,查找对应的 ContractLine 对象,并建立其与主 Contract 对象之间的关联关系。
    • 此处使用的列表分隔符为 :,如果直接使用 lines.name[] 作为列的 header, 则表示使用默认的分隔符英文逗号(,)
    • 需要注意的是,在导入前,lines 字段关联的对象必须已经存在,否则会导致导入 失败。
  • DELETE_FLAG

    • 该字段标识某一行的数据是否执行删除操作,根据上述示例文件中每行该字段的值, 第一行的数据执行更新或者新建操作,第二行的数据执行删除操作

提示

如果待导入的字符串中包含英文的引号,则使用另一个英文引号,或反斜杠进行转义。 如 "" 或者 \" 表示英文引号。

最后更新: 2024/2/23 07:44:20