使用 bcp.exe 成功导入数据,即使格式文件中的列顺序与 table 不同

Using bcp.exe to successfully import data, even if the column order in the format file differs to that of the table

我一直在为我的团队创建一个系统,它允许我们自动生成大量测试数据,以便我们可以 return 我们的数据库在需要时保持一致状态。这需要完全自动化,但我目前有一个问题阻止了这种情况的发生。

我们的数据库是使用 Entity Framework 创建的,我们也在使用迁移。问题在于,由于列顺序可能会因迁移而发生变化,这意味着在迁移之前导出的测试数据在使用 bcp 时可能无法再正确导入。

为了更清楚地说明这一点,以下是重现该行为的步骤:

创建一个简单的 table:

创建格式文件:

C:\bcp [BCPTest].[dbo].[Person] format nul -f person.xml -x -N -T -S .\SQLExpress

创建二进制输出文件:

C:\bcp [BCPTest].[dbo].[Person] out person.dat -N -T -S .\SQLExpress

重新排序列并保存:

尝试导入:

C:\bcp [BCPTest].[dbo].[Person] in person.dat -f person.xml -N -T -S .\SQLExpress

这会导致很多不同的错误。目前可以解决这个问题,方法是手动重新排序 table 中失败的列,保存并重新运行导入,但是,正如我之前所说,这需要完全自动化。

有人对我如何实现这一目标有任何建议吗?

尝试从 BCP in 命令中删除 -N 选项。 -N 选项有效地覆盖格式文件字段序号。无论如何,格式文件指定了本机字段格式,因此 -N 规范是多余的。

编辑:

下面是一个 T-SQL 示例,它更改了原始格式文件 xml 的列映射顺序以匹配当前格式文件 table。这可以扩展以处理丢弃的列和必须手动处理的条件检测,例如新的非 NULL 列。

我建议您从一个 Powershell 脚本中调用它,该脚本遍历您的格式文件列表并调用以原始格式 XML 和 table 名称传递给每个文件的过程。

CREATE PROC dbo.usp_GetNewFormatFileXml
      @TableName nvarchar(261)
    , @OriginalFormatFileXml xml
    , @NewFormatFileXml xml OUTPUT
AS

DECLARE @NewFormatFileColumns xml;

--genrate new field/column mappings based in current column ordinal
WITH
    XMLNAMESPACES (
          'http://www.w3.org/2001/XMLSchema-instance' AS xsi
        ,  DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/bulkload/format'
        )
SELECT @NewFormatFileColumns = (
    SELECT
          RecordField.ID AS [@SOURCE]
        , NewTableColumn.Name AS [@NAME]
        , OldTableColumn.type AS [@xsi:type]
    FROM (
        SELECT 
            Field.value('@ID', 'int') AS ID
        FROM @OriginalFormatFileXml.nodes('/BCPFORMAT/RECORD/FIELD') AS Record(Field)) AS RecordField
    JOIN (
        SELECT 
              TableColumn.value('@SOURCE', 'int') AS FieldId
            , TableColumn.value('@NAME', 'sysname') AS Name
            , TableColumn.value('@xsi:type', 'sysname') AS type
        FROM @OriginalFormatFileXml.nodes('/BCPFORMAT/ROW/COLUMN') AS TableRow(TableColumn)) AS OldTableColumn ON
        OldTableColumn.FieldId = RecordField.ID
    JOIN (
        SELECT
              Name
            , column_id
        FROM sys.columns
        WHERE 
            object_id = OBJECT_ID(@TableName)
        ) AS NewTableColumn ON
            NewTableColumn.Name = OldTableColumn.Name
    ORDER BY NewTableColumn.column_id
FOR XML PATH('COLUMN'), ROOT('ROW'), TYPE);

SET @NewFormatFileXml = @OriginalFormatFileXml;

--remove old column mapping
SET @NewFormatFileXml.modify('
    declare default element namespace "http://schemas.microsoft.com/sqlserver/2004/bulkload/format";
    delete
        (/BCPFORMAT/ROW[1] )[1] 
        ') ;

--add new column mapping
SET @NewFormatFileXml.modify('
    declare default element namespace "http://schemas.microsoft.com/sqlserver/2004/bulkload/format";
    insert sql:variable("@NewFormatFileColumns")
        into (/BCPFORMAT )[1] 
        ') ;
GO

--example usage
DECLARE
    @TableName nvarchar(261) = 'dbo.Person'
    , @OriginalFormatFileXml xml = '<?xml version="1.0"?>
<BCPFORMAT xmlns="http://schemas.microsoft.com/sqlserver/2004/bulkload/format" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <RECORD>
  <FIELD ID="1" xsi:type="NativeFixed" LENGTH="8"/>
  <FIELD ID="2" xsi:type="NCharPrefix" PREFIX_LENGTH="2" MAX_LENGTH="100" COLLATION="SQL_Latin1_General_CP1_CI_AS"/>
 </RECORD>
 <ROW>
  <COLUMN SOURCE="1" NAME="ID" xsi:type="SQLBIGINT"/>
  <COLUMN SOURCE="2" NAME="Name" xsi:type="SQLNVARCHAR"/>
 </ROW>
</BCPFORMAT>'
    , @NewFormatFileXml xml;

EXEC dbo.usp_GetNewFormatFileXml
      @TableName = @TableName
    , @OriginalFormatFileXml = @OriginalFormatFileXml
    , @NewFormatFileXml = @NewFormatFileXml OUTPUT;

SELECT @NewFormatFileXml;