为什么只有 1 行的 DataTable.Load 结果表现得好像是 DataRow 而不是 DataTable?

Luc*_*VdV 4 datatable powershell

我想查询不同服务器上的两个数据库以获取相同类型的信息,并将结果合并到一个列表中。

我创建了这个示例,它可以工作,但前提是第一个服务器返回至少行。

该示例使用整数列表 (1 .. 5),但我连接两个字符串列表的原始代码也存在相同的问题。

# 'quick and dirty' sample
# to be considered insecure,
# do not use as a basis for production code.
function GetData($dbserver, $start, $end)
{
    $qry = "WITH q AS (SELECT  $start AS num UNION ALL SELECT num + 1 FROM q WHERE num < $end) SELECT * FROM q"
    $con = New-Object System.Data.SqlClient.SqlConnection
    $con.ConnectionString = "server=$dbserver;database=A2SDataCentral;Integrated Security=SSPI"
    $con.Open()
    $com = $con.CreateCommand()
    $com.CommandText = $qry
    $res = $com.ExecuteReader()
    $table = New-Object System.Data.DataTable
    $table.Load($res)
    return $table
}

$all = $null # erase any result remaining from a previous run
$t1 = GetData "(local)" 1 2
$t2 = GetData "(local)" 3 5
$all = $t1 + $t2
$all
Run Code Online (Sandbox Code Playgroud)

我在这里使用的 SQL 查询只是一个生成序列号的虚拟查询。它仅适用于 SQL 服务器。在此示例中,我在同一服务器(本地主机)上运行它两次,实际上它必须查询两个不同的服务器以获取两个名称列表并将它们连接起来。

如上所示,这个示例完成了我想要做的事情,最终结果是一个从 1 到 5 的整数列表。

编号
---
  1
  2
  3
  4
  5

但是当我将调用更改GetData为这样时,使第一个调用仅返回一行,

$t1 = GetData "(local)" 1 1
$t2 = GetData "(local)" 2 5
Run Code Online (Sandbox Code Playgroud)

它崩溃了

方法调用失败,因为 [System.Data.DataRow] 不包含名为“op_Addition”的方法。

所以看起来该GetData函数返回的不是 a DataTable,而是 aDataRow如果结果中只有一行。

我尝试过这个,但这也不是解决方案:

$all = $t1
$all.Rows.Add($t2)
Run Code Online (Sandbox Code Playgroud)

这会抛出“集合具有固定大小”($t1有 2 行)或“您无法在空值表达式上调用方法”($t1包含 1 行)。

有人可以解释为什么只有 1 行的结果会导致与多行不同的行为吗?

(编辑:提请注意脚本的示例/不安全性质)

Mat*_*sen 7

你是 PowerShell 的受害者,试图显得过于友好

当 PowerShell看到DataTable从管道上的函数传递的数据时,它会尝试将其分解并将各个行一一向下游发送。

例如,当从数据库查询中过滤数据集时,这可能非常有用,但如果您依赖某些内在DataTable行为,这显然会非常令人沮丧。

为了避免 PowerShell 解开行,请使用Write-Output $table -NoEnumerator或 - 如果您CmdletBinding向函数添加属性 - 您可以$PSCmdlet.WriteObject($table,$false)直接调用:

function GetData($dbserver, $start, $end)
{
    $qry = "WITH q AS (SELECT  $start AS num UNION ALL SELECT num + 1 FROM q WHERE num < $end) SELECT * FROM q"
    $con = New-Object System.Data.SqlClient.SqlConnection
    $con.ConnectionString = "server=$dbserver;database=A2SDataCentral;Integrated Security=SSPI"
    $con.Open()
    $com = $con.CreateCommand()
    $com.CommandText = $qry
    $res = $com.ExecuteReader()
    $table = New-Object System.Data.DataTable
    $table.Load($res)

    Write-Output $table -NoEnumerate
}
Run Code Online (Sandbox Code Playgroud)

或者

function GetData
{
    [CmdletBinding()]
    param($dbserver, $start, $end)

    $qry = "WITH q AS (SELECT  $start AS num UNION ALL SELECT num + 1 FROM q WHERE num < $end) SELECT * FROM q"
    $con = New-Object System.Data.SqlClient.SqlConnection
    $con.ConnectionString = "server=$dbserver;database=A2SDataCentral;Integrated Security=SSPI"
    $con.Open()
    $com = $con.CreateCommand()
    $com.CommandText = $qry
    $res = $com.ExecuteReader()
    $table = New-Object System.Data.DataTable
    $table.Load($res)

    $PSCmdlet.WriteObject($table,$false)
}
Run Code Online (Sandbox Code Playgroud)

安全审查奖励!

我不能良心回答这个问题而不指出你的函数目前容易受到最基本形式的 SQL 注入的影响。

如果用户要将非数字值作为$start$end参数参数值传递怎么办?有人可以这样做:

$untrustedUserInput = '0 OR 1 = 1; xp_cmdshell "cmd /c calc.exe" --'
Run Code Online (Sandbox Code Playgroud)

恭喜,我现在拥有您的数据库服务器:)

在这种特定情况下,您可以通过强类型输入参数来轻松降低风险:

function GetData([string]$dbServer, [int]$start, [int]$end)
{
...
Run Code Online (Sandbox Code Playgroud)

或者

param([string]$dbServer, [int]$start, [int]$end)
Run Code Online (Sandbox Code Playgroud)

更好的解决方案是参数化SQL 查询:

$qry = "WITH q AS (SELECT @start AS num UNION ALL SELECT num + 1 FROM q WHERE num < @end) SELECT * FROM q"
# ...
$com.Parameters.Add('@start','Int').Value = $start
$com.Parameters.Add('@end','Int').Value = $end
Run Code Online (Sandbox Code Playgroud)

这样,邪恶的字符串值就不会在未经清理的情况下潜入查询中