Как добавить в DataTable тип Real? - PullRequest
0 голосов
/ 08 мая 2020

Я копирую записи из файла csv в таблицу sql. Таблица sql имеет столбцы, которые являются varchar, и столбцы, которые являются настоящими типами данных (на основе предоставленных нам атрибутов csv)

Предположим, что первые 7 столбцов являются внешними ключами varchar (100), а остальные 80+ столбцов имеют тип данных Real.

Во время массового копирования я использовал функцию Out-DataTable, потому что, по-видимому, это наиболее эффективный способ массового копирования (особенно с нашими файлами, содержащими тысячи записей).

Однако я получаю следующую ошибку:

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."

Теперь я sh ошибка может указывать, какой именно столбец, но на основе моих исследований я обнаружил, что это может быть связано с тем, что тип данных предполагается, что является строковым типом для всех столбцов.

Проверка с помощью следующего: $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

Итак, вопрос: как я могу сообщить Datatable, чтобы первые 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? ;)
#https://stackoverflow.com/a/50995201/8397835
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 
}

Ответы [ 2 ]

2 голосов
/ 08 мая 2020

Out-DataTable проверяет свойства первого входного объекта ...

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

... для определения DataType соответствующего DataColumn ...

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 будет таким же.

. NET эквивалент real равен Single, поэтому вы либо необходимо жестко указать, какие столбцы (по имени или порядковому номеру) должны иметь тип [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
}

... или расширить лог обнаружения c ...

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], то то же самое верно для каждого входного объекта. Если это не гарантируется, вы можете выполнить один проход через все входные объекты, чтобы определить соответствующие типы столбцов, и второй проход, чтобы загрузить данные ...

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
0 голосов
/ 11 августа 2020

У меня была аналогичная проблема после импорта XML -данных через .read Xml, потому что XML включал пустые строки вместо dbnull. Я провел много тестов, чтобы преобразовать это как можно быстрее, и для меня это сработало лучше всего:

  1. создать dataTable со всеми столбцами в качестве строк для импорта данных
  2. create те же столбцы с немного разными именами с правильным целевым типом в той же таблице и создают ссылку на строковый столбец (например, если первый столбец типа 'строка' назван "c1", то я назвал новый столбец типа 'real '"c1 _")
  3. во время создания каждого столбца на шаге 2 также создайте выражение вроде' IIF (LEN ([c1] = 0), NULL, [c1]) ', которое решает "пустую строку "-dilemma.
  4. теперь выполните массовый импорт и, наконец, экспортируйте только ссылающиеся столбцы через dataTableReader в его собственный dataTable.
...