无法将空字符串传递到非空数据库字段

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.

但它不是空的。这是一个空字符串,应该 可以正常工作。我发誓我过去传递过很多次空字符串。

为什么会出现此错误,我应该如何解决?


其他详细信息:

在数据类型中使用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 无论如何对我来说都不明显。

我没有调查 ftMemoftString 之间的选择是由 Delphi RTL 代码还是它所在的 MDAC(Ado) 层做出的:我希望它实际上是由 RecordSet TAdoQuery 使用决定的。

QED。请注意,这种系统的调试方法揭示了 问题和原因只需很少的努力和零试错,这是 我在对 q 的评论中试图提出的建议。

另一点是,这个问题完全可以在没有 求助于 server-side 工具,包括 SMSS 分析器。不需要使用分析器来检查客户端发送到服务器的内容 因为没有理由认为服务器返回的错误 是不正确的。这证实了我所说的从客户端开始调查。

此外,使用 IfDefed 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;