跳转到主要内容

快速OpenERP迁移框架

项目描述

此工具的开发考虑了以下初始目标,按照此优先级顺序

  • 合并 2个不同的OpenERP数据库到一个多公司数据库

  • 迁移 从OpenERP 6.1到7.0

  • 从旧版业务应用(Access、Delphi等)迁移数据

  • 从Dolibarr迁移到OpenERP

该工具的原理是从旧应用程序(目前仅限OpenERP)导出CSV数据,然后处理CSV文件以便导入到新安装的OpenERP数据库。这与OpenERP或OpenUpgrade的现场迁移策略完全不同,允许从干净的数据库重新开始,同时保留历史记录。导入和导出使用PostgreSQL特定的COPY命令进行,可实现极快的导出和导入。结合纯内存Python csv处理,该工具通常能够达到超过2000行/秒的整体迁移速率。

安装

此工具仅适用于Python 2.7

使用virtualenv

$ virtualenv sandbox
$ sandbox/bin/pip install anybox.migration.openerp
$ sandbox/bin/migrate -h

使用Buildout

如果您使用Buildout,您可以在新的部分中添加此工具,例如

[migration]
recipe = zc.recipe.egg
eggs = anybox.migration.openerp

然后,别忘了在[buildout]部分的parts中添加migration部分。在重新启动bin/buildout后,migrate脚本将出现在buildout的bin目录中。

OpenERP buildout配方的1.7版开始,您也可以直接在您的[openerp]部分中安装此工具,只需添加

eggs = anybox.migration.openerp
scripts = migrate

使用方法

此工具提供单个migrate脚本

$ migrate -h

您可以列出可用的默认映射文件

$ migrate -l
openerp6.1-openerp7.0.yml

您应指定源和目标数据库、要迁移的源表的选择以及要使用的映射文件。然后,该工具负责选择相关表

$ migrate -s source_dbname -t target_dbname -r res_partner account_move -p openerp6.1-openerp7.0.yml custom.yml

如果您想检查创建的临时CSV文件,请使用--keepcsv选项。它们将存储在当前目录下的临时目录中。

除非您指定-w选项以在末尾提交事务,否则此脚本实际上不会在目标数据库中写入任何内容。

迁移过程最重要的部分是YML映射文件,该文件描述了如何按表和列处理数据。提供了一个默认映射文件,并作为将两个6.1数据库迁移到单个7.0多公司数据库的实映射文件使用。您可以将提供的默认6.1到7.0文件与其它自定义yml文件混合,它们将被合并。

内部结构

此工具非常受以下灵感的启发

  • 外部参照OpenERP模块

  • OpenUpgrade项目

  • Talend Open Studio

不同的内部步骤如下

  • 从旧数据库导出CSV

  • 将CSV转换为匹配目标数据库

  • 使用区分符检测目标DB中存在的数据

  • 后处理CSV文件以修复外键

  • 重新注入到OpenERP

  • 使用传入数据更新可能存在的现有数据

CSV文件的处理使用Yaml编写的映射文件。映射文件的初始版本是在OpenUpgrade分析文件的帮助下编写的。

映射文件

您应该记住,此迁移工具仅处理数据库表和列:它不知道OpenERP字段。每个表、每行、每个源数据库的单元格都独立处理,映射文件告诉当前单元格要做什么。这导致了限制,并且此工具无法处理极其复杂的迁移。但它足够强大,可以同时将两个6.1数据库合并并迁移到7.0多公司数据库。

对于实际例子,您可以查看此工具的mappings目录中提供的OpenERP 6.1到7.0映射文件。

复制数据

列映射的最简单和基本YML语句如下

module:
    table1.column1:
        table2.column2: __copy__

这表明,如果OpenERP 模块 安装在 目标 数据库中,则源DB的 table1column1 应该复制到目标DB中 table2column2

甚至可以省略 __copy__ 指令,上述语句与以下语句等价

module:
    table1.column1:
        table2.column2:

实际上,此语句内部被转换为Python字典

{'module':
    {'table1.column1':
        {'table2.column2': '__copy__'}}

整个yml文件被转换为一个大映射字典,其叶子是能够处理数据的语句或函数。

复制表的所有列

如果您的目标表与源表结构相同,则可以避免为每个列指定映射语句,并使用通配符

module:
    table1.*:

这意味着:将源DB中table1的所有列复制到目标DB中的table1。此类映射通常在源和表结构相似时用作起点。然后您可以添加特定列的映射语句以覆盖此通配符。

复制所有列到不同的表

如果源表仅已重命名,则可以将源表table1的所有列复制到目标表table2

module:
    table1.*:
        table2.*:

复制所有内容

如果源数据库和目标数据库具有完全相同的结构,并且您只想迁移数据,则可以使用全局通配符(但我们还没有机会真正尝试这个功能)

module:
    .*:

这意味着:不进行任何处理,将所有表复制到目标数据库。与裸数据导出和恢复相比,这似乎没有用,但请记住,这种方式不仅可以替换数据,还可以向目标数据库追加数据。在这种情况下,您应该注意现有数据,如果表有约束(请参阅下面的判别器)

将一条源行拆分到多个表中(表拆分)

对于来自源表的单个源行,您可以向多个目标表提供数据。只需将多个目标行按如下方式放置即可完成此操作

module:
    table1.column1:
        table2.column2:
        table3.column3:

这意味着:对于源DB的 table1 中的每个 column1,创建两个目标行:一个用于 table2,另一个用于 table3

在处理当前行期间,其他映射语句可以提供相同的目标行。以下是一个示例

module:
    table1.column1:
        table2.column2:
        table3.column3:
    table1.column2:
        table2.column2:
        table3.column4:

在这种情况下,table1 中的数据将被引导到 table2table3。然后您可以添加更多行以处理 table1 的所有列

然而,在上面的示例中,存在冲突,因为两个源单元格被引导到同一目标单元格(table2.column2)。在这种情况下,无法预测将使用哪一个(因为映射是Python 字典,而字典是无序的)。您应该避免此类冲突。

在从OpenERP 6.1迁移到7.0的情况下,此类映射实际上用于将一个源 res_users 行迁移到三个不同的行:一个在 res_users 中,一个在 res_partner 中,一个在 mail_alias 中。请参阅默认映射以获取实际示例。

数据移动到另一个表中(表合并)

当输入行必须移动到不同的表时,您希望保持指向它们的引用键,以便迁移后它们指向新表,您应该使用 __moved__ 语句。

OpenERP中当前的唯一情况是 res_partner_address 数据移动到 res_partner

base:
    res_partner_address.id:
        res_partner.id: __moved__

此声明必须与指向移动表的全部外键的 __fk__ 声明一起使用(参见 __fk__ 章节)。

不迁移一列

如果您想删除表中的一列,请使用 __forget__ 声明

module:
    table1.column1: __forget__

当您定义了通配符,此声明可用来防止迁移特定的列。

使用Python代码转换数据

除了使用 __copy__ 声明复制数据外,您还可以使用任何Python代码。Python代码应写在字面Yaml块中,并作为函数体直接执行,因此您必须在某个地方插入一个 return 声明。

mail 模块的示例

mail:
    mail_message.type:
        mail_message.type: return 'email'

这意味着 mail_message 表的 type 列将被填充为 'email' 字符串,无论源列有什么数据。

使用Python代码块构建的函数的最终签名是

def mapping_function(self, source_row, target_rows):

这意味着在函数体内,您可以访问完整的 source_row,它是一个包含当前正在处理的行的所有键(列名)和值的字典。但请注意,在此时,您正在处理这一行的一个特定单元格,并且您应该返回将被插入目标表相应单元格中的值。这可以用来将两个源单元格的数据聚合到目标单元格中。

base:
    table1.firstname: __forget__
    table1.name:
        table1.name: return source_row['firstname'] + ' ' + source_row['name']

您还可以访问正在填充的 target_rows,这样来自源单元格的数据就可以影响目标行中的多个单元格,甚至不同的目标表。以下是一个示例

base:
    table1.id:
        table1.id:
        table2.id:
    table1.name:
        table1.name: |
            name = source_row['firstname'] + ' ' + source_row['name']
            target_rows['table1']['display_name'] = name
            target_rows['table2']['display_name'] = name
            return name
        table2.name

注意,在上面的示例中,Python代码跨越了多行,您应该使用 | 定义Yaml字面块。上面的示例最终意味着:将来自 table1firstname 追加到 name,并将其放入目标 table1table2display_name 单元格中。目标 name 单元格将包含源 name 单元格的副本。

如果目标行不应该与源行有相同的 id,您可以使用 newid() 函数创建一个新的 id。这个函数在每次调用时都会返回不同的值,并负责增加 id。以下是一个示例

base:
    res_users.id:
        res_users.id:
        res_users.partner_id:
        res_partner.notification_email_send: return 'comment'
        res_partner.id: |
            i = newid()
            target_rows['res_users']['partner_id'] = i
            target_rows['res_partner']['id'] = i
            target_rows['res_partner']['name'] = source_row['name']
            target_rows['res_partner']['email'] = source_row['user_email']
            return i

每个 res_users 行将生成一个具有新 idres_partner 行,而 res_usersid 将与源相同。(实际上不会相同,因为对所有 ids 都应用了一个偏移量)。

为新列提供数据

如果目标列应该包含数据但没有在源表中对应的列,您可以使用 ‘_’ 作为不存在源列名称的替代。

base:
    res_partner._:
        res_partner.is_company: return False

与现有数据合并

当数据插入目标表时,您可能希望将其与现有数据合并。

想象一下,目标 res_users 表已经包含一个 admin 账户,您不希望通过从源 res_users 表迁移数据来重复此账户。在这种情况下,您应该告诉映射如何识别现有数据。这是通过替换源列名称为 __discriminator__ 声明,并提供一个用于识别现有数据的列名称列表来完成的。

base:
    res_users.__discriminator__:
        - login

使用此语句,您可以安装一个新的OpenERP数据库及其管理员账户,并将所有现有账户与从源表来的数据合并。将使用登录列来匹配数据。现有的管理员账户不会重复,但会用源表中的管理员账户更新。

在多公司场景下的另一个用途是合并目标数据库中存在的合作伙伴,但保持两个公司的合作伙伴独立。

base:
    res_partner.__discriminator__:
        - name
        - company_id

无约束的外键

迁移的第一步是自动检测源表和目标表的所有外键。有时,OpenERP会在没有约束的情况下定义外键。这主要发生在具有store=True相关字段上,它创建了一个没有约束的整数列。如果您不想__忘记__这样的列,您必须告诉映射外键的目标是什么,如下面的真实示例所示

account:
    account_move.company_id:
        account_move.company_id: __fk__ res_company

这是对crm_lead表的一个示例,该表可能包含来自一个__移动__表的字段。想象一下,您希望OpenERP 7.0中CRM线索的partner_id字段来自OpenERP 6.1中同一表的partner_address_id字段。新字段是到res_partner的外键,而旧的一个指向res_partner_address。您可以使用以下语句来告知这一点

crm_lead.partner_address_id:
    crm_lead.partner_id: __fk__ res_partner_address

但是,您也不应该忘记忘记partner_id字段,否则如果您为表使用了通配符,您将遇到冲突并混合数据

crm_lead.*:
crm_lead.partner_id: __forget__

参考字段

有时列定义了到另一表的动态引用ID,就像外键一样,只是表的名称实际存储在另一个列中。

id

model

res_id

1

cr.claim

23

2

cr.claim

35

3

base.action.rule

27

在上面的例子中,由于res_id不是一个真正的外键,其值不会固定以对应目标数据库。在这种情况下,您应该使用__ref__语句,后跟包含表或模型名称的列名。此语句假设OpenERP使用模型到表的转换(将“.”替换为“_”)

mail_message.res_id:
    mail_message.res_id: __ref__ model

处理循环依赖的表

在最后一步中,逐个导入迁移的CSV文件。一些表通过外键约束依赖于其他表,并且这种依赖有时会形成循环。在这种情况下,无法导入表,因为它们都依赖于另一个表。一种解决方案是__忘记__该列,这通常是不希望的,因为您会丢失数据。为了能够保留此类数据,您应该使用__defer__语句,这样列将在所有数据导入后更新

base:
    res_users.create_uid:
        res_users.create_uid: __defer__
    res_users.write_uid:
        res_users.write_uid: __defer__

在迁移期间运行SQL请求

如果所需的迁移过于复杂,无法通过常规语句处理,则可以在源数据库和目标数据库上运行SQL查询。这应在有限的情况下使用,因为查询将针对映射定义的每个源单元格执行,迁移可能会减慢,除非您通过手动缓存来限制查询。(请参阅映射中的工作流程迁移)

映射文件中提供了一个简单的sql()函数,其签名如下

sql(db, query, args)

where:
- db is the string 'source' or 'target'
- query is the SQL query
- args is the arguments to insert in the query
The query is actually executed with: cursor.execute(query, args)

以下是一个示例

base:
    res_users._:
        (...)
        mail_alias.alias_model_id: return sql('target', "select id from ir_model where model='res.users'")[0][0]

字段大小限制

在运行迁移时,您可能会遇到csv.Error: field larger than field limit错误。这是由于csv模块默认将csv字段大小限制为128k。默认值已增加到20MB。如果这对您的迁移还不够,您可以通过插入对csv.field_size_limit()的直接调用来增加限制。

例如

module:
    table1.column1:
        table2.column2: |
            import csv
            csv.field_size_limit(262144)
            return source_row['column1']

整体迁移过程

迁移需要以下几个步骤。如果您需要,可以轻松编写一个小脚本来自动化整个过程。

迁移前

迁移前的不同步骤如下。它们对于迁移的成功至关重要,应在目标数据库上执行。

  • 使用最新的迁移代码创建一个干净的目标数据库,不包含演示数据。

  • 安装预期的模块。

  • 重命名目标公司,使其名称与源数据库中的公司名称完全一致。

  • 通过运行以下SQL语句删除所有内部序列的公司: update ir_sequence set company_id=NULL;。这将允许在迁移后删除重复项。

迁移

迁移包括运行migrate脚本并选择正确的选项。如果目标数据库中的数据不是您预期的,您必须调整选项和映射文件以获得所需的结果。

这里有一个真实示例

../bin/migrate -s sourcedb -t targetdb -p openerp6.1-openerp7.0.yml custom.yml
-r res_partner account_move res_users pos_order pos_order_line account_move_line
account_journal sale_order_line stock_inventory_line account_tax
product_supplierinfo wkf_instance wkf_workitem wkf_triggers -w

迁移后

migrate脚本可能不足以使您的数据库干净且可用。您可能需要处理额外的修正。请彻底测试您的实例!从版本0.6开始,您不需要手动修复内部序列,因为它们现在由映射文件处理。您只需将其清理以删除重复项(从菜单设置/技术/序列和标识符/序列)。然而,所需剩余的修正之一是删除一些parent_leftparent_right列。以下是以会计模块为例的示例

psql targetdb -c 'alter table account_account drop parent_left;'
psql targetdb -c 'alter table account_account drop parent_right;'

您可能还需要强制重新计算数据库中持久化的新或更改的相关字段(store=True)。以下是以account_report_company模块为例的示例

psql targetdb -c 'alter table account_invoice drop commercial_partner_id;'

最后,您应该运行数据库的最终全局更新。如果您使用的是buildout配方,它应该看起来像这样

../bin/start_openerp -u all -d targetdb --stop-after-init

理解错误

使用此工具最困难的部分是在处理过程中理解错误,因为它需要深入了解其内部工作原理。大多数错误通常来自错误的映射文件。错误可能在处理CSV文件时发生,但最困难的是在最后导入步骤,因为某些表可能无法导入。在这种情况下,您应仔细查看日志消息,并尝试理解约束错误或为什么表无法导入。您还应使用--keepcsv选项,并检查中间CSV文件以了解问题。使用此选项,您将最终得到一个目录,其中包含每个表的五个CSV文件。

例如,对于res_partner表,您将找到以下文件

  • res_partner.csv是从源数据库导出的原始数据

  • res_partner.target.csv包含经过映射文件第一次处理后的数据,但外键错误

  • res_partner.target2.csv包含最终数据,外键已修复,最终将导入

  • res_partner.update.csv包含在目标数据库中检测到的现有数据,但外键错误。

  • res_partner.update2.csv包含最终现有数据,外键已修复,将在导入后用于更新目标表。

如果您在导入步骤中遇到外键错误而陷入困境,请查看此日志,因为它包含解决大多数常见问题的信息:https://bitbucket.org/anybox/anybox.migration.openerp/issue/3/foreign-key-constraints

贡献

作者和贡献者

  • Christophe Combelles

  • Florent Jouatte

  • Guy-Clovis Nzouendjou

  • Stéphane Bidoul

代码仓库和错误跟踪器

请看这里:https://bitbucket.org/anybox/anybox.migration.openerp

请随时给我们反馈,报告错误或贡献映射文件到Bitbucket。

更改

0.9 (2013-12-31)

  • 清理并添加缺失的表

  • 许多映射改进、修复和清理

  • 添加了人力资源迁移映射

0.8 (2013-09-30)

  • 恢复了用户/公司分配的映射

  • 改进了消息迁移

0.7 (2013-09-24)

  • 修复了CRM迁移

  • 迁移email_template

  • 迁移email_message

  • 支持使用__ref__语句的引用

  • 修复并改进了映射

  • 将默认的csv字段大小限制增加到20MB

0.6 (2013-08-24)

  • 迁移ir_sequence无需使用后迁移脚本

  • 修复了工作流实例和工作项迁移

  • 数据库合并情况下的主要性能提升(3倍)

  • 修复了由于外键区分符偏移错误引起的不必要的合并

  • 打破一些依赖循环和其他映射改进

0.5 (2013-08-02)

  • 修复了指向已存在数据的__moved__表的 外键

  • 更新文档

0.4 (2013-07-28)

  • 修复了潜在客户和采购订单的迁移

  • 简化了__moved__语句处理

  • 改进了工作流迁移

  • 迁移员工和费用

  • 默认将供应商设置为公司

  • 如何在buildout中安装

  • 更新文档

0.3 (2013-07-11)

  • 针对6.1到7.0迁移的大量改进

  • 修复了由于错误引用导致的导入错误

  • 允许m2o到m2m迁移,无需自定义代码

  • 添加了项目、CRM和auth_ldap模块的映射

  • 修复了移动行

  • 允许请求源数据库

  • 改进了文档

  • 运行工作流的迁移

0.2 (2013-07-01)

  • 初始发布

项目详情


下载文件

下载适用于您平台的文件。如果您不确定要选择哪个,请了解更多关于安装包的信息。

源分布

anybox.migration.openerp-0.9.tar.gz (43.5 kB 查看散列)

上传时间

由以下组织支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面