Help us learn about your current experience with the documentation. Take the survey.

为直接传输导入器添加新关系

总体而言,要为直接传输导入器添加新关系,您必须:

  1. 将新关系添加到导出数据列表中。
  2. 在导入端添加一个新的 ETL(提取/转换/加载)管道,并包含数据处理指令。
  3. 将新创建的管道添加到导入阶段列表中。
  4. 为新创建的关系添加一个标签,以便在 UI 中显示。
  5. 确保有充分的测试覆盖。

为了降低引入错误和性能问题的风险,新添加的关系应该通过功能标志(feature flag)来控制。

从源端导出

我们导出几种类型的关系:

  • ActiveRecord 关联。从 import_export.yml 文件读取,序列化为 JSON,写入到 NDJSON 文件中。每个关系被导出到 .gz 文件,如果是集合则导出到 .tar.gz 文件,然后上传并通过目标 GitLab 实例的 REST API 提供下载和导入服务。
  • 二进制文件。例如上传的文件或 LFS 对象。
  • 少量不导出但在导入期间直接从 GraphQL API 读取的关系。

对于 ActiveRecord 关联,出于性能考虑,您应该使用 NDJSON 而不是 GraphQL API。深度嵌套的关联可能会产生大量网络请求,从而减慢整体迁移速度。

导出 ActiveRecord 关联

直接传输导入器的底层行为很大程度上基于文件导入器,它使用 import_export.yml 文件来描述导出时要包含的 Project 关联列表。对于 Group 也有类似的 import_export.yml 文件。

例如,要为名为 documents 的新 Project 关联添加导入支持,您必须:

  1. 将其添加到 import_export.yml 文件中。
  2. 为新关系添加测试覆盖。
  3. 验证添加的关系是否按预期导出。

将其添加到 import_export.yml 文件中

此文件中列出的关联按从上到下的顺序导入。如果您有顺序依赖的关联,请将依赖项放在需要它们的关联之前。例如,文档必须在合并请求之前导入,否则它们将无效。

  1. 将您的关联添加到 import_export.yml 中的 tree.project

    diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
    index 43d66e0e67b7..0880a27dfce2 100644
    --- a/lib/gitlab/import_export/project/import_export.yml
    +++ b/lib/gitlab/import_export/project/import_export.yml
    @@ -122,6 +122,7 @@ tree:
             - label:
               - :priorities
         - :service_desk_setting
    +    - :documents
       group_members:
         - :user
    

    如果您的关联仅与企业版(Enterprise Edition)功能相关,请将其添加到文件末尾的 ee.tree.project 树中,这样它只会在 GitLab 企业版实例中导出和导入。

    如果您的关联不需要包含任何子关系,这样就足够了。但如果它需要包含更多子关系(例如 notes),您必须将它们列出来。例如,documents 可以有 notes(带有表情符号)和 award emojis(在 documents 上),我们想要迁移这些内容。在这种情况下,我们的关系变为:

    diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
    index 43d66e0e67b7..0880a27dfce2 100644
    --- a/lib/gitlab/import_export/project/import_export.yml
    +++ b/lib/gitlab/import_export/project/import_export.yml
    @@ -122,6 +122,7 @@ tree:
             - label:
               - :priorities
         - :service_desk_setting
    +    - documents:
           - :award_emoji
           - notes:
             - :award_emoji
       group_members:
         - :user
    
  2. 添加关系的 included_attributes。默认情况下,YAML 文件中未在 included_attributes 中列出的任何关系属性都会在导出和导入时被过滤掉。要包含您需要的属性,必须将它们添加到 included_attributes 列表中,如下所示:

    diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
    index 43d66e0e67b7..dbf0e1275ecf 100644
    --- a/lib/gitlab/import_export/project/import_export.yml
    +++ b/lib/gitlab/import_export/project/import_export.yml
    @@ -142,6 +142,9 @@ import_only_tree:
    
     # Only include the following attributes for the models specified.
     included_attributes:
    +  documents:
    +    - :title
    +    - :description
       user:
         - :id
         - :public_email
    
  3. 添加关系的 excluded_attributes。文件中也有 excluded_attributes 列表。您不需要为 Project 添加排除属性,但仍需要为 Group 添加。此列表表示不应包含在导出中且应在导入时忽略的属性。这些属性通常是:

    • 任何以 _id_ids 结尾的内容
    • 任何包含 attributes 的内容(custom_attributes 除外)
    • 任何以 _html 结尾的内容
    • 任何敏感信息(例如令牌、加密数据)

    完整的禁止引用列表请参见 这里

  4. 添加关系的 methods。如果您的关联有一个必须导出的方法(例如 document.signature),您可以在 methods 部分添加它。导出的值将出现在导出文件中,您可以在导入时使用它。例如,将其分配给某个字段。

例如,我们导出 note_diff_file.diff_export 方法 的返回值,在导入时 note_diff_file.diff 设置为该方法的导出值。

为新关系添加测试覆盖

由于直接传输在底层使用了文件导入器,我们必须为文件导入器范围内的新关系添加测试覆盖,这也覆盖了直接传输导入器的导出端。将测试添加到:

  1. spec/lib/gitlab/import_export/project/tree_saver_spec.rbGroup 也有类似的文件。
  2. ee/spec/lib/ee/gitlab/import_export/project/tree_saver_spec.rb 用于 EE 特定的关系。

遵循其他关系的示例来添加新测试。

验证添加的关系是否按预期导出

import_export.yml 中指定的任何新添加的关系都会自动添加到磁盘上写入的导出文件中,因此不需要额外操作。

一旦关系被添加并添加了测试,我们就可以手动检查该关系是否被导出。它应该自动包含在:

  • 基于文件的导入和导出中。使用 项目导出功能 来导出、下载和检查导出的数据。
  • 直接传输导出中。使用 export_relations API 来导出、下载和检查导出的关系(可能会分批导出)。

导出二进制关系

如果要添加对二进制关系的支持:

  1. 创建一个新的导出服务,在磁盘上执行导出。请参见示例 BulkImports::LfsObjectsExportService
  2. 将关系添加到 file_relations 列表中。
  3. 将关系添加到 BulkImports::FileExportService

示例

在目标端导入

如上所述,直接传输导入中有三种类型的关系:

  1. export_relations API 下载的 NDJSON 导出的关系。例如 documents.ndjson.gz
  2. GraphQL API 关系。例如,使用 GraphQL 获取组和项目用户成员资格的 members 信息。
  3. export_relations API 下载的二进制关系。例如 lfs_objects.tar.gz

由于直接传输导入器基于提取/转换/加载数据处理技术,要开始导入关系,我们必须定义:

  • 一个新的关系导入管道。例如 DocumentsPipeline
  • 一个数据提取器,让管道知道从哪里以及如何提取数据。例如 NdjsonPipeline
  • 一组转换器,这些是用于将数据转换为您所需格式的类。
  • 一个加载器,用于将数据持久化到某处。例如,在数据库中保存一行或创建新的 LFS 对象。

无论导入什么类型的关系,Pipeline 类的结构都是相同的:

module BulkImports
  module Common
    module Pipelines
      class DocumentsPipeline
        include Pipeline

        def extract(context)
          BulkImports::Pipeline::ExtractedData.new(data: file_paths)
        end

        def transform(context, object)
          ...
        end

        def load(context, object)
          document.save!
        end
      end
    end
  end
end

从 NDJSON 导入关系

定义管道

从前面的示例,我们的 documents 关系被导出到 NDJSON 文件,在这种情况下我们可以使用:

  • NdjsonPipeline,它包含从 JSON 到 ActiveRecord 对象的自动数据转换(底层使用文件导入器)。
  • NdjsonExtractor,它使用 /export_relations/download REST API 端点从源实例下载 .ndjson.gz 文件。

ETL 管道的每个步骤都可以定义为方法或类。

  class DocumentsPipeline
    include NdjsonPipeline

    relation_name 'documents'

    extractor ::BulkImports::Common::Extractors::NdjsonExtractor, relation: relation
end

这个新管道现在将:

  1. 从源实例下载 documents.ndjson.gz 文件。
  2. 读取 NDJSON 文件的内容并反序列化 JSON 以转换为 ActiveRecord 对象。
  3. 在项目范围内将其保存到数据库中。

管道可以放在以下命名空间中:

  • 如果是共享的并且要在组和项目迁移中使用,则放在 BulkImports::Common::Pipelines 命名空间中。例如 LabelsPipeline 是一个通用管道,在组和项目阶段列表中都被引用。
  • 如果管道属于项目迁移,则放在 BulkImports::Projects::Pipelines 命名空间中。
  • 如果管道属于组迁移,则放在 BulkImports::Groups::Pipelines 命名空间中。

将新管道添加到阶段中

直接传输导入器按阶段执行组和项目的迁移。阶段列表定义在:

  • 对于 Projectlib/bulk_imports/projects/stage.rb
  • 对于 Grouplib/bulk_imports/groups/stage.rb

每个阶段:

  • 可以有多个并行运行的管道。
  • 必须完全完成后才能进入下一阶段。

让我们将管道添加到 Project 阶段:

module BulkImports
  module Projects
    class Stage < ::BulkImports::Stage
      private

       def config
        {
          project: {
            pipeline: BulkImports::Projects::Pipelines::ProjectPipeline,
            stage: 0
          },
          repository: {
            pipeline: BulkImports::Projects::Pipelines::RepositoryPipeline,
            maximum_source_version: '15.0.0',
            stage: 1
          },
          documents: {
            pipeline: BulkImports::Projects::Pipelines::DocumentsPipeline,
            minimum_source_version: '16.11.0',
            stage: 2
          }
       end
    end
  end
end

我们指定了:

  • stage: 2,因此项目和存储库阶段必须首先完成,然后我们的管道才能在第 2 阶段运行。
  • minimum_source_version: '16.11.0'。因为我们在这个里程碑中为导出引入了 documents 关联,所以在之前的 GitLab 版本中不可用。因此,此管道仅在源版本为 16.11 或更高时运行。

如果某个关系已被弃用,只需要在某个版本之前运行管道,我们可以指定 maximum_source_version 属性。

用测试覆盖管道

因为我们已经用测试覆盖了导出端,我们必须对导入端做同样的事情。对于直接传输导入器,每个管道都有一个单独的规范文件,看起来类似于 这个示例

示例

使用自定义关联名称导入关系

存在一些与其 ActiveRecord 类名不匹配的关联。例如:

class Release
  has_many :links, class_name: 'Releases::Link'
end

这样的关联在 releases.ndjson 中以 links 导出。但是,在导入时,当我们常量化(constantize)关系类时,无法常量化 links,因为该类不存在。类应该是 Releases::Link

在这种情况下,我们必须将此关联名称添加到 OVERRIDES 哈希中,它表示关联及其相应 ActiveRecord 类的映射,这样导入器就知道如何正确地常量化它们。

module Gitlab
  module ImportExport
    module Project
      class RelationFactory < Base::RelationFactory
        OVERRIDES = {
          links: 'Releases::Link'
        }
      end
    end
  end
end

这样,导入器将每个导出的 link 映射到相应的 Releases::Link 类。

导入被多个其他关系引用的现有对象

如果关系在多个关联中被引用(或在单个关联中的多个记录之间被引用),我们不想导入重复项。

例如,考虑一个应用于多个不同问题和合并请求的标签。每当我们导出问题和合并请求时,导出的标签都包含在每个记录中作为其子关系。当我们导入导出的问题和合并请求时,我们只想导入一次标签并在所有记录中重用它。否则,我们会得到重复项(多个具有相同名称的标签)。

要像这样只导入一次对象并在多个地方重用它,我们必须将该对象定义为现有对象关系。

首先,我们必须将标签关联添加到 EXISTING_OBJECT_RELATIONS 中。关系被添加到现有对象关系列表后,导入器知道这样的关系必须与其他关系不同处理,并经过不同的导入流程。它不使用常规路由导入这样的关系,而是使用 ObjectBuilder

ObjectBuilder 尝试:

要将新关系添加到 ObjectBuilder,您必须:

  1. 如上所述将您的关系添加到 EXISTING_OBJECT_RELATIONS
  2. 更新组或项目的 ObjectBuilder,具体取决于它是项目关联还是组关联。
  3. 定义应使用哪些属性来执行现有对象查找。例如,对于标签,我们希望按 titledescriptioncreated_at 搜索。如果项目中存在具有定义参数的标签,则重用它而不是创建新标签。

从 GraphQL API 导入关系

如果您的关系可以通过 GraphQL API 获取,您可以使用 GraphQlExtractor 并在管道类中执行转换和加载。

MembersPipeline 示例:

module BulkImports
  module Common
    module Pipelines
      class MembersPipeline
        include Pipeline

        transformer Common::Transformers::ProhibitedAttributesTransformer
        transformer Common::Transformers::MemberAttributesTransformer

        def extract(context)
          graphql_extractor.extract(context)
        end

        def load(_context, data)
          ...

          member.save!
        end

        private

        def graphql_extractor
          @graphql_extractor ||= BulkImports::Common::Extractors::GraphqlExtractor
            .new(query: BulkImports::Common::Graphql::GetMembersQuery)
        end
      end
    end
  end
end

其余步骤与上述步骤相同。

导入二进制关系

二进制关系管道与其他管道具有相同的结构,您只需要定义在提取/转换/加载步骤中会发生什么。

LfsObjectsPipeline 示例:

module BulkImports
  module Common
    module Pipelines
      class LfsObjectsPipeline
        include Pipeline

        file_extraction_pipeline!

        def extract(_context)
          download_service.execute
          decompression_service.execute
          extraction_service.execute

          ...
        end

        def load(_context, file_path)
          ...

          lfs_object.save!
        end
      end
    end
  end
end

有一些辅助服务类可以帮助数据下载:

  • BulkImports::FileDownloadService:从给定位置下载文件。
  • BulkImports::FileDecompressionService:带有必要验证的 Gzip 解压缩服务。
  • BulkImports::ArchiveExtractionService:Tar 提取服务。

适配 UI

为新关系添加标签

一旦新关系被添加到直接传输中,您需要确保该关系在 UI 中以人类可读的形式显示。

  1. 将新的键值对添加到 BULK_IMPORT_STATIC_ITEMS
diff --git a/app/assets/javascripts/import/constants.js b/app/assets/javascripts/import/constants.js
index 439f453cd9d3..6b4119a0af9 100644
--- a/app/assets/javascripts/import/constants.js
+++ b/app/assets/javascripts/import/constants.js
@@ -31,6 +31,7 @@ export const BULK_IMPORT_STATIC_ITEMS = {
   service_desk_setting: __('Service Desk'),
   vulnerabilities: __('Vulnerabilities'),
   commit_notes: __('Commit notes'),
+  documents: __('Documents')
 };

 const STATISTIC_ITEMS = {