如何将 'Real' 类型添加到 DataTable?

How to add 'Real' type to a DataTable?

我正在将记录从 csv 文件批量复制到 sql table。 sql table 具有 varchar 列和真实数据类型的列(基于我们给出的 csv 属性)

假设前 7 列是 varchar(100) 的外键,其余 80 多列是 Real 数据类型。

在批量复制期间,我使用了 Out-DataTable 函数,因为显然这是批量复制的最有效方式(尤其是我们的文件包含 1000 条记录)。

但是,我收到以下错误:

Exception calling "WriteToServer" with "1" argument(s): "The given value of type String from the data source cannot be converted to type real of the specified target column."

现在我希望错误可以准确指定哪一列,但根据我的研究,我发现这可能与数据类型有关 presumed 所有列都是字符串类型。

验证以下内容:$column.DataType

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object
True     True     String                                   System.Object

所以问题是:我如何告诉数据table允许前 7 列是字符串,而其余的是真实数据类型?

代码如下:

function Get-Type 
{ 
    param($type) 

$types = @( 
'System.Boolean', 
'System.Byte[]', 
'System.Byte', 
'System.Char', 
'System.Datetime', 
'System.Decimal', 
'System.Double', 
'System.Guid', 
'System.Int16', 
'System.Int32', 
'System.Int64', 
'System.Single', 
'System.UInt16', 
'System.UInt32', 
'System.UInt64') 

    if ( $types -contains $type ) { 
        Write-Output "$type" 
    } 
    else { 
        Write-Output 'System.String' 

    } 
} #Get-Type

function Out-DataTable 
{ 
    [CmdletBinding()] 
    param([Parameter(Position=0, Mandatory=$true, ValueFromPipeline = $true)] [PSObject[]]$InputObject) 

    Begin 
    { 
        $dt = new-object Data.datatable   
        $First = $true  
    } 
    Process 
    { 
        foreach ($object in $InputObject) 
        { 
            $DR = $DT.NewRow()   
            foreach($property in $object.PsObject.get_properties()) 
            {   
                if ($first) 
                {   
                    $Col =  new-object Data.DataColumn   
                    $Col.ColumnName = $property.Name.ToString()   
                    if ($property.value) 
                    { 
                        if ($property.value -isnot [System.DBNull]) { 
                            $Col.DataType = [System.Type]::GetType("$(Get-Type $property.TypeNameOfValue)") 
                        } 
                    } 
                    $DT.Columns.Add($Col) 
                }   
                if ($property.Gettype().IsArray) { 
                    $DR.Item($property.Name) =$property.value | ConvertTo-XML -AS String -NoTypeInformation -Depth 1 
                }   
               else { 
                    $DR.Item($property.Name) = $property.value 
                } 
            }   
            $DT.Rows.Add($DR)   
            $First = $false 
        } 
    }  

    End 
    { 
        Write-Output @(,($dt)) 
    } 

} #Out-DataTable

$SqlConnection = New-Object System.Data.SqlClient.SqlConnection
$SqlConnection.ConnectionString = $connectionstring
$SqlConnection.Open()

$CSVDataTable = Import-Csv $csvFile | Out-DataTable

# Build the sqlbulkcopy connection, and set the timeout to infinite
$sqlBulkCopy = New-Object ("Data.SqlClient.SqlBulkCopy") -ArgumentList $SqlConnection
$sqlBulkCopy.DestinationTableName = "$schemaName.[$csvFileBaseName]"
$sqlBulkCopy.bulkcopyTimeout = 0
$sqlBulkCopy.batchsize = 50000
$sqlBulkCopy.DestinationTableName = "$schemaName.[$csvFileBaseName]"

#This mapping helps to make sure that the columns match exactly because BulkCopy depends on indexes not column names by default. 
#However, with the DataTable, the correct mappings seems to be already taken care of, but putting this here regardless, because why not?
#Better safe than sorry, right? ;)
#
foreach ($column in $CSVDataTable.Columns) { $sqlBulkCopy.ColumnMappings.Add($column.ColumnName, $column.ColumnName) > $null }

$sqlBulkCopy.WriteToServer($CSVDataTable)

# Clean Up
$sqlBulkCopy.Close(); $sqlBulkCopy.Dispose()
$CSVDataTable.Dispose()
# Sometimes the Garbage Collector takes too long to clear the huge datatable.
[System.GC]::Collect()

也许是这样的?

伪代码:

foreach ($column in $CSVDataTable.Columns) { 
    $sqlBulkCopy.ColumnMappings.Add(
        if($DestinationTableName.Column.type -eq 'Real') {
            $column.type() = 'Real'
        }
        $column.ColumnName, $column.ColumnName
    ) > $null 
}

Out-DataTable 正在检查第一个输入对象的属性...

foreach($property in $object.PsObject.get_properties())
{
    if ($first) 
    {  

...确定对应的DataColumn...

DataType
if ($property.value -isnot [System.DBNull]) { 
    $Col.DataType = [System.Type]::GetType("$(Get-Type $property.TypeNameOfValue)") 
} 

问题是,输入对象是由 Import-Csv...

生成的
$CSVDataTable = Import-Csv $csvFile | Out-DataTable

...不对 CSV 字段进行任何转换;每个 属性 都是 [String] 类型,因此每个 DataColumn 也是。

The .NET equivalent of real is Single, so you either need to hard-code which columns (by name or ordinal) should be of type [Single]...

$objectProperties = @($object.PSObject.Properties)
for ($propertyIndex = 0; $propertyIndex -lt $objectProperties.Length)
{
    $property = $objectProperties[$propertyIndex]
    if ($propertyIndex -lt 7) {
        $columnDataType = [String]
        $itemValue = $property.Value
    }
    else {
        $columnDataType = [Single]
        $itemValue = if ($property.Value -match '^\s*-\s*$') {
            [Single] 0
        } else {
            [Single]::Parse($property.Value, 'Float, AllowThousands, AllowParentheses')
        }
    } 

    if ($first) 
    {   
        $Col =  new-object Data.DataColumn   
        $Col.ColumnName = $property.Name
        $Col.DataType = $columnDataType

        $DT.Columns.Add($Col) 
    }

    $DR.Item($property.Name) = $itemValue
}

...或增强您的检测逻辑...

foreach($property in $object.PSObject.Properties)
{
    $singleValue = $null
    $isSingle = [Single]::TryParse($property.Value, [ref] $singleValue)

    if ($first) 
    {   
        $Col =  new-object Data.DataColumn   
        $Col.ColumnName = $property.Name
        $Col.DataType = if ($isSingle) {
            [Single]
        } else {
            [String]
        }

        $DT.Columns.Add($Col) 
    }

    $DR.Item($property.Name) = if ($isSingle) {
        $singleValue
    } else {
        $property.value
    }
}

为了符合DataType 列,此代码在解析成功时用[Single] 值替换原始属性 [String] 值。请注意,我已经删除了 [DBNull]IsArray 的检查,因为它们永远不会计算为 $true,因为 Import-Csv 只会产生 [String] 属性。

上面假设如果来自第一个输入对象的 属性 的值可以被解析为 [Single] 那么同样适用于 every 输入对象。如果这不能保证,那么您可以通过所有输入对象来确定适当的列类型,然后通过第二次来加载数据...

function Out-DataTable
{ 
    End 
    {
        $InputObject = @($input)
        $numberStyle = [System.Globalization.NumberStyles] 'Float, AllowThousands, AllowParentheses'
        $dt = new-object Data.datatable 

        foreach ($propertyName in $InputObject[0].PSObject.Properties.Name)
        {
            $columnDataType = [Single]

            foreach ($object in $InputObject)
            {
                $singleValue = $null
                $propertyValue = $object.$propertyName
                if ($propertyValue -notmatch '^\s*-?\s*$' `
                    -and -not [Single]::TryParse($propertyValue, $numberStyle, $null, [ref] $singleValue))
                {
                    # Default to [String] if not all values can be parsed as [Single]
                    $columnDataType = [String]
                    break
                }
            }

            $Col =  new-object Data.DataColumn   
            $Col.ColumnName = $propertyName
            $Col.DataType = $columnDataType

            $DT.Columns.Add($Col) 
        }

        foreach ($object in $InputObject)
        { 
            $DR = $DT.NewRow()   
            foreach($property in $object.PSObject.Properties) 
            {   
                $DR.Item($property.Name) = if ($DT.Columns[$property.Name].DataType -eq [Single]) {
                    if ($property.Value -match '^\s*-?\s*$') {
                        [Single] 0
                    } else {
                        [Single]::Parse($property.Value, $numberStyle)
                    }
                } else {
                    $property.value
                }
            }   
            $DT.Rows.Add($DR)   
        } 

        Write-Output @(,($dt)) 
    }  

} #Out-DataTable

我在通过 .readXml 导入 XML-data 后遇到了类似的挑战,因为 XML 包含空字符串而不是 dbnull。我做了很多测试以尽快转换它,对我来说效果最好:

  1. 创建一个数据表,其中所有列作为 data-import
  2. 的字符串
  3. 在同一 table 中使用正确的目标类型创建名称略有不同的相同列,并创建对 string-column 的引用(例如,如果类型 'string' 的第一列是命名为“c1”然后我将类型为'real'的新列命名为“c1_”)
  4. 在步骤 2 中创建每一列的过程中,还创建了一个类似于 'IIF(LEN([c1]=0),NULL,[c1])' 的表达式来解决“empty-string”-困境。
  5. 现在执行 bulk-import,最后仅通过 dataTableReader 将引用列导出到它自己的数据表中。