无法将空字符串传递到非空数据库字段
Unable to pass empty string into non-null database field
我被一些应该非常简单的事情难住了。我有一个 SQL 服务器数据库,我正在尝试用空字符串更新不可为 null 的 varchar 或 nvarchar 字段。我知道这是可能的,因为空字符串 ''
与 NULL
不是 相同的东西。但是,使用 TADOQuery
,它不允许我这样做。
我正在尝试像这样更新现有记录:
ADOQuery1.Edit;
ADOQuery1['NonNullFieldName']:= '';
//or
ADOQuery1.FieldByName('NonNullFieldName').AsString:= '';
ADOQuery1.Post; //<-- Exception raised while posting
如果字符串中有任何内容,即使只有一个 space,它也会像预期的那样保存得很好。但是,如果它是一个空字符串,它将失败:
Non-nullable column cannot be updated to Null.
但它不是空的。这是一个空字符串,应该 可以正常工作。我发誓我过去传递过很多次空字符串。
为什么会出现此错误,我应该如何解决?
其他详细信息:
- 数据库:Microsoft SQL Server 2014 Express
- 语言:Delphi 10 西雅图更新 1
- 数据库驱动程序:
SQLOLEDB.1
- 正在更新的字段:
nvarchar(MAX) NOT NULL
在数据类型中使用MAX
时出现问题。 varchar(MAX)
和 nvarchar(MAX)
都利用了这种行为。当删除 MAX
并用大数字替换它时,例如 5000
,则它允许空字符串。
我可以使用下面的代码在 SS2014、OLEDB driver 和
西雅图以及当 table 以 MAX 作为列大小和特定数字(在我的例子中为 4096)创建时的行为差异。我想我会 post 这是一个替代方案
回答是因为它不仅展示了如何系统地研究这种差异
但也确定了为什么会出现这种差异(以及将来如何避免这种差异)。
请参考并执行下面的代码,如所写,即使用 UseMAX
定义
活跃。
在执行代码之前在项目选项中打开"Use Debug DCUs",立即
显示所描述的异常发生在 Data.Win.ADODB
的第 4920
行
Recordset.Fields[TField(FModifiedFields[I]).FieldNo-1].Value := Data
of TCustomADODataSet.InternalPost
和调试评估 window 表明
Data
此时是Null
.
接下来,请注意
update jdtest set NonNullFieldName = ''
在 SSMS2014 查询 window 中执行而没有抱怨 (Command(s) completed successfully.
),所以看起来
事实上 Data
在第 4920 行是 Null
是导致问题的原因,下一个问题是 "Why?"
嗯,首先要注意的是表格的标题显示 ftMemo
接下来,注释掉UseMAX
定义,重新编译并执行。结果:无异常
请注意,表单的标题现在显示 ftString
.
这就是原因:对列大小使用特定数字意味着
RTL 检索到的 table 元数据导致创建 client-side Field
作为 TStringField
,您可以通过字符串赋值语句设置其值。
OTOH,当你指定MAX时,结果client-side Field
是ftMemo类型,
这是 Delphi 的 BLOB 类型之一,当您分配
将字符串值添加到 ftMemo 字段,您将受到 Data.DB.Pas 中代码的支配,它使用 TBlobStream
对记录缓冲区执行所有读取(和写入)操作。问题在于,据我所知,经过大量实验和跟踪代码后,TMemoField 使用 BlobStream 的方式无法正确区分将字段内容更新为 '' 和将字段值设置为 Null
(如 System.Variants)。
简而言之,每当您尝试将 TMemoField 的值设置为空字符串时,实际发生的是该字段的状态被设置为 Null,这就是导致 q. AFAICS,这是不可避免的,所以 work-around 无论如何对我来说都不明显。
我没有调查 ftMemo
和 ftString
之间的选择是由 Delphi RTL 代码还是它所在的 MDAC(Ado) 层做出的:我希望它实际上是由 RecordSet
TAdoQuery 使用决定的。
QED。请注意,这种系统的调试方法揭示了
问题和原因只需很少的努力和零试错,这是
我在对 q 的评论中试图提出的建议。
另一点是,这个问题完全可以在没有
求助于 server-side 工具,包括 SMSS 分析器。不需要使用分析器来检查客户端发送到服务器的内容
因为没有理由认为服务器返回的错误
是不正确的。这证实了我所说的从客户端开始调查。
此外,使用 IfDef
ed Sql 即时创建的 table 通过简单观察应用程序的两次运行,可以一步有效地隔离问题.
代码
uses [...] TypInfo;
[...]
implementation[...]
const
// The following consts are to create the table and insert a single row
//
// The difference between them is that scSqlSetUp1 specifies
// the size of the NonNullFieldName to 'MAX' whereas scSqlSetUp2 specifies a size of 4096
scSqlSetUp1 =
'CREATE TABLE [dbo].[JDTest]('#13#10
+ ' [ID] [int] NOT NULL primary key,'#13#10
+ ' [NonNullFieldName] VarChar(MAX) NOT NULL'#13#10
+ ') ON [PRIMARY]'#13#10
+ ';'#13#10
+ 'Insert JDTest (ID, [NonNullFieldName]) values (1, ''a'')'#13#10
+ ';'#13#10
+ 'SET ANSI_PADDING OFF'#13#10
+ ';';
scSqlSetUp2 =
'CREATE TABLE [dbo].[JDTest]('#13#10
+ ' [ID] [int] NOT NULL primary key,'#13#10
+ ' [NonNullFieldName] VarChar(4096) NOT NULL'#13#10
+ ') ON [PRIMARY]'#13#10
+ ';'#13#10
+ 'Insert JDTest (ID, [NonNullFieldName]) values (1, ''a'')'#13#10
+ ';'#13#10
+ 'SET ANSI_PADDING OFF'#13#10
+ ';';
scSqlDropTable = 'drop table [dbo].[jdtest]';
procedure TForm1.Test1;
var
AField : TField;
S : String;
begin
// Following creates the table. The define determines the size of the NonNullFieldName
{$define UseMAX}
{$ifdef UseMAX}
S := scSqlSetUp1;
{$else}
S := scSqlSetUp2;
{$endif}
ADOConnection1.Execute(S);
try
ADOQuery1.Open;
try
ADOQuery1.Edit;
// Get explicit reference to the NonNullFieldName
// field to make working with it and investigating it easier
AField := ADOQuery1.FieldByName('NonNullFieldName');
// The following, which requires the `TypInfo` unit in the `USES` list is to find out which exact type
// AField is. Answer: ftMemo, or ftString, depending on UseMAX.
// Of course, we could get this info by inspection in the IDE
// by creating persistent fields
S := GetEnumName(TypeInfo(TFieldType), Ord(AField.DataType));
Caption := S; // Displays `ftMemo` or `ftString`, of course
AField.AsString:= '';
ADOQuery1.Post; //<-- Exception raised while posting
finally
ADOQuery1.Close;
end;
finally
// Tidy up
ADOConnection1.Execute(scSqlDropTable);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Test1;
end;
我被一些应该非常简单的事情难住了。我有一个 SQL 服务器数据库,我正在尝试用空字符串更新不可为 null 的 varchar 或 nvarchar 字段。我知道这是可能的,因为空字符串 ''
与 NULL
不是 相同的东西。但是,使用 TADOQuery
,它不允许我这样做。
我正在尝试像这样更新现有记录:
ADOQuery1.Edit;
ADOQuery1['NonNullFieldName']:= '';
//or
ADOQuery1.FieldByName('NonNullFieldName').AsString:= '';
ADOQuery1.Post; //<-- Exception raised while posting
如果字符串中有任何内容,即使只有一个 space,它也会像预期的那样保存得很好。但是,如果它是一个空字符串,它将失败:
Non-nullable column cannot be updated to Null.
但它不是空的。这是一个空字符串,应该 可以正常工作。我发誓我过去传递过很多次空字符串。
为什么会出现此错误,我应该如何解决?
其他详细信息:
- 数据库:Microsoft SQL Server 2014 Express
- 语言:Delphi 10 西雅图更新 1
- 数据库驱动程序:
SQLOLEDB.1
- 正在更新的字段:
nvarchar(MAX) NOT NULL
在数据类型中使用MAX
时出现问题。 varchar(MAX)
和 nvarchar(MAX)
都利用了这种行为。当删除 MAX
并用大数字替换它时,例如 5000
,则它允许空字符串。
我可以使用下面的代码在 SS2014、OLEDB driver 和 西雅图以及当 table 以 MAX 作为列大小和特定数字(在我的例子中为 4096)创建时的行为差异。我想我会 post 这是一个替代方案 回答是因为它不仅展示了如何系统地研究这种差异 但也确定了为什么会出现这种差异(以及将来如何避免这种差异)。
请参考并执行下面的代码,如所写,即使用 UseMAX
定义
活跃。
在执行代码之前在项目选项中打开"Use Debug DCUs",立即
显示所描述的异常发生在 Data.Win.ADODB
的第 4920
Recordset.Fields[TField(FModifiedFields[I]).FieldNo-1].Value := Data
of TCustomADODataSet.InternalPost
和调试评估 window 表明
Data
此时是Null
.
接下来,请注意
update jdtest set NonNullFieldName = ''
在 SSMS2014 查询 window 中执行而没有抱怨 (Command(s) completed successfully.
),所以看起来
事实上 Data
在第 4920 行是 Null
是导致问题的原因,下一个问题是 "Why?"
嗯,首先要注意的是表格的标题显示 ftMemo
接下来,注释掉UseMAX
定义,重新编译并执行。结果:无异常
请注意,表单的标题现在显示 ftString
.
这就是原因:对列大小使用特定数字意味着
RTL 检索到的 table 元数据导致创建 client-side Field
作为 TStringField
,您可以通过字符串赋值语句设置其值。
OTOH,当你指定MAX时,结果client-side Field
是ftMemo类型,
这是 Delphi 的 BLOB 类型之一,当您分配
将字符串值添加到 ftMemo 字段,您将受到 Data.DB.Pas 中代码的支配,它使用 TBlobStream
对记录缓冲区执行所有读取(和写入)操作。问题在于,据我所知,经过大量实验和跟踪代码后,TMemoField 使用 BlobStream 的方式无法正确区分将字段内容更新为 '' 和将字段值设置为 Null
(如 System.Variants)。
简而言之,每当您尝试将 TMemoField 的值设置为空字符串时,实际发生的是该字段的状态被设置为 Null,这就是导致 q. AFAICS,这是不可避免的,所以 work-around 无论如何对我来说都不明显。
我没有调查 ftMemo
和 ftString
之间的选择是由 Delphi RTL 代码还是它所在的 MDAC(Ado) 层做出的:我希望它实际上是由 RecordSet
TAdoQuery 使用决定的。
QED。请注意,这种系统的调试方法揭示了 问题和原因只需很少的努力和零试错,这是 我在对 q 的评论中试图提出的建议。
另一点是,这个问题完全可以在没有 求助于 server-side 工具,包括 SMSS 分析器。不需要使用分析器来检查客户端发送到服务器的内容 因为没有理由认为服务器返回的错误 是不正确的。这证实了我所说的从客户端开始调查。
此外,使用 IfDef
ed Sql 即时创建的 table 通过简单观察应用程序的两次运行,可以一步有效地隔离问题.
代码
uses [...] TypInfo;
[...]
implementation[...]
const
// The following consts are to create the table and insert a single row
//
// The difference between them is that scSqlSetUp1 specifies
// the size of the NonNullFieldName to 'MAX' whereas scSqlSetUp2 specifies a size of 4096
scSqlSetUp1 =
'CREATE TABLE [dbo].[JDTest]('#13#10
+ ' [ID] [int] NOT NULL primary key,'#13#10
+ ' [NonNullFieldName] VarChar(MAX) NOT NULL'#13#10
+ ') ON [PRIMARY]'#13#10
+ ';'#13#10
+ 'Insert JDTest (ID, [NonNullFieldName]) values (1, ''a'')'#13#10
+ ';'#13#10
+ 'SET ANSI_PADDING OFF'#13#10
+ ';';
scSqlSetUp2 =
'CREATE TABLE [dbo].[JDTest]('#13#10
+ ' [ID] [int] NOT NULL primary key,'#13#10
+ ' [NonNullFieldName] VarChar(4096) NOT NULL'#13#10
+ ') ON [PRIMARY]'#13#10
+ ';'#13#10
+ 'Insert JDTest (ID, [NonNullFieldName]) values (1, ''a'')'#13#10
+ ';'#13#10
+ 'SET ANSI_PADDING OFF'#13#10
+ ';';
scSqlDropTable = 'drop table [dbo].[jdtest]';
procedure TForm1.Test1;
var
AField : TField;
S : String;
begin
// Following creates the table. The define determines the size of the NonNullFieldName
{$define UseMAX}
{$ifdef UseMAX}
S := scSqlSetUp1;
{$else}
S := scSqlSetUp2;
{$endif}
ADOConnection1.Execute(S);
try
ADOQuery1.Open;
try
ADOQuery1.Edit;
// Get explicit reference to the NonNullFieldName
// field to make working with it and investigating it easier
AField := ADOQuery1.FieldByName('NonNullFieldName');
// The following, which requires the `TypInfo` unit in the `USES` list is to find out which exact type
// AField is. Answer: ftMemo, or ftString, depending on UseMAX.
// Of course, we could get this info by inspection in the IDE
// by creating persistent fields
S := GetEnumName(TypeInfo(TFieldType), Ord(AField.DataType));
Caption := S; // Displays `ftMemo` or `ftString`, of course
AField.AsString:= '';
ADOQuery1.Post; //<-- Exception raised while posting
finally
ADOQuery1.Close;
end;
finally
// Tidy up
ADOConnection1.Execute(scSqlDropTable);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Test1;
end;