为什么我的用户定义表类型 (UDTT) 上的列默认值没有得到遵守?

Rai*_*olt 3 sql-server default-value c# user-defined-table-type table-valued-parameters

我在 SQL 中创建了一个表类型:

CREATE TYPE [dbo].[MyObject_TableType] AS TABLE
(
    [Name] VARCHAR(MAX) DEFAULT '',
    [Index] VARCHAR(MAX) DEFAULT ''
)
Run Code Online (Sandbox Code Playgroud)

我构建了一个DataTable并用一个填充它DataRow。我将该表作为参数提供给存储过程:

// Set up connection and command variables (code omitted)
// ...

// Make the data table
var table = new DataTable();
table.Columns.Add("Name");
table.Columns.Add("Index");

// Add one row that is missing an Index    
var row = table.NewRow();
row["Name"] = "ObjectOne";
table.Rows.Add(row);

// Make a parameter for the table
var tableParameter = new SqlParameter("MyObjects", SqlDbType.Structured)
{
    TypeName = "MyObject_TableType",
    Value = table
};

command.Parameters.Add(tableParameter);
command.ExecuteNonQuery();
Run Code Online (Sandbox Code Playgroud)

我预计在存储过程内部,索引值将默认为''(空 VARCHAR)。相反,我观察到该值为空。我知道这一点是因为我试图将表类型合并到实际表中,并且表上的索引列不允许空值。

为什么 SQL Server 不尊重我放置在表类型上的默认值?有没有办法强制 SQL Server 兑现它?

我想我可以通过使用DataTable.DefaultValue属性来解决这个问题,但该解决方案的缺点是我需要在两个不同的地方声明相同的默认值。如果可能,我想避免冗余代码。

Sol*_*zky 5

为什么 SQL Server 不尊重我放置在表类型上的默认值?

实际上,SQL Server 确实尊重[u]r 该默认值,如以下测试(使用问题中提供的 UDTT)所示:

DECLARE @Test dbo.MyObject_TableType;
INSERT INTO @Test (Name) VALUES ('ObjectOne');
SELECT * FROM @Test;
Run Code Online (Sandbox Code Playgroud)

返回:

Name        Index
ObjectOne              <--- empty string, not NULL
Run Code Online (Sandbox Code Playgroud)

您得到NULL而不是空字符串的原因是因为这是您要求应用程序代码提供的内容。您甚至(当然是在不知不觉中;-)在陈述(强调)时确定问题的根本原因:

我构建了一个DataTable并用一个填充它DataRow。我将该作为参数提供给存储过程:

ADataTable是内存中实际表的表示,而不是一个或多个INSERT语句。因此,当您创建该单行时,该Index列不能是未定义的:它要么是NULL某个值,要么是某个值。当您将其DataTable作为 TVP传递到 SQL Server 时,您传递的是等效于表的内容。

是的,您可能可以通过DataColumnDataTable. 但我同意这样做不是最好的方法。

我的偏好是永远不要使用DataTables ,除非您已经碰巧有一个,无论是否使用 TVP。我想不出有什么理由仅仅为了将数据传递到 TVP 而创建一个。除了这个特殊问题,它还会复制您放入其中的任何集合,只是为了作为 TVP 的临时传输(这会浪费内存和复制该数据所需的时间,即使它们中的任何一个都不多) .

相反,创建一个方法,该方法将获取您已有的任何集合,并简单地将其流式传输到 TVP 中,一次一个项目/一行。您可以通过IEnumerable<SqlDataRecord>从此方法返回来完成此操作,然后将此方法指定为表示 TVP的Value属性SqlParameter

将集合流式传输到 TVP 的方法:

private static IEnumerable<SqlDataRecord> SendRows(List<DbObject> DbObjects)
{
   SqlMetaData[] _TvpSchema = new SqlMetaData[]
   {
      new SqlMetaData("Name", SqlDbType.VarChar, SqlMetaData.Max,
                      true, false, SortOrder.Unspecified, 1),
      new SqlMetaData("Index", SqlDbType.VarChar, SqlMetaData.Max,
                      true, false, SortOrder.Unspecified, 2)
   };
   SqlDataRecord _DataRecord = new SqlDataRecord(_TvpSchema);

   int _NumRows = DbObjects.Count;

   // read a row, send a row.
   // "for" should be faster than "foreach", right?
   for (int _Index = 0; _Index < _NumRows; _Index++)
   {
      _DataRecord.SetString(0, DbObjects[_Index].Name);
      _DataRecord.SetString(1, DbObjects[_Index].Index);
      yield return _DataRecord;
   }
}
Run Code Online (Sandbox Code Playgroud)

使用方法:

private struct DbObject
{
    public string Name;
    public string Index;
}

List<DbObject> _DbObjects = new List<DbObject>();

// Add one row that is missing an Index    
_DbObjects.Add(new DbObject() { Name = "ObjectOne" });

// Make a parameter for the table
SqlParameter _ParamMyObjects = new SqlParameter("MyObjects", SqlDbType.Structured)
{
    TypeName = "MyObject_TableType", // not required for CommandType.StoredProcedure
    Value = SendRows(_DbObjects)
};

command.Parameters.Add(_ParamMyObjects);
command.ExecuteNonQuery();
Run Code Online (Sandbox Code Playgroud)

获取 UDTT 上定义的默认值Index以使用空字符串填充列的技巧是我上面使用的SqlMetaData 构造函数的特定重载。它有一个布尔参数useServerDefault。在传递true导致列无法传递到INSERT在声明中如果没有“设置” SqlDataRecord。或者,至少这就是我一直让IDENTITYUDTT 中的列工作的方式(从未在实际的 中尝试过DEFAULT,但应该是相同的行为)。

PS为什么你VARCHAR(MAX)在UDTT中使用作为这两列的数据类型?如果任一个或两个,这些列中必须包含SQL Server的标识符(即表,视图,存储过程,索引的名字,等等),那么这些都是sysname它的别名NVARCHAR(128)。所以你真的需要至少使用NVARCHAR来避免潜在的数据丢失/难以追踪的错误。