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

Jer*_*dge 6 sql-server delphi ado sql-server-2014 delphi-10-seattle

我很难接受一些非常直截了当的事情.我有一个SQL Server数据库,我正在尝试使用空字符串更新不可为空的varchar或nvarchar字段.我知道这是可能的,因为空字符串''一样的东西NULL.但是,使用它TADOQuery,它不允许我这样做.

我正在尝试更新现有记录,如下所示:

ADOQuery1.Edit;
ADOQuery1['NonNullFieldName']:= '';
//or
ADOQuery1.FieldByName('NonNullFieldName').AsString:= '';
ADOQuery1.Post; //<-- Exception raised while posting
Run Code Online (Sandbox Code Playgroud)

如果字符串中有任何内容,即使只是一个空格,它也会像预期的那样保存得很好.但是,如果它是一个空字符串,它会失败:

不可为空的列无法更新为Null.

但它不是空的.它是一个空字符串,应该可以正常工作.我发誓我过去曾多次通过空串.

为什么我会收到此错误,我应该怎么做才能解决它?


额外细节:

  • 数据库:Microsoft SQL Server 2014 Express
  • 语言:Delphi 10 Seattle Update 1
  • 数据库驱动: SQLOLEDB.1
  • 正在更新的字段: nvarchar(MAX) NOT NULL

Mar*_*ynA 13

我可以使用下面的代码使用SS2014,OLEDB驱动程序和西雅图重现您报告的问题,以及使用MAX创建表作为列大小和特定数字(在我的情况下为4096)时的行为差异.我认为我会发布这个作为替代答案,因为它不仅展示了如何系统地研究这种差异,而且还确定了为什么会出现这种差异(以及将来如何避免这种差异).

请参考并执行下面的代码,如下所示,即UseMAX定义有效.

在执行代码之前打开项目选项中的"使用调试DCU",立即显示所描述的异常发生在Data.Win.ADODB 第4920行

Recordset.Fields[TField(FModifiedFields[I]).FieldNo-1].Value := Data
Run Code Online (Sandbox Code Playgroud)

TCustomADODataSet.InternalPost和调试评估窗口显示, Data在这一点上Null.

接下来,请注意

update jdtest set NonNullFieldName = ''
Run Code Online (Sandbox Code Playgroud)

执行无投诉(一SSMS2014查询窗口Command(s) completed successfully.),如此看来,这其实DataNull在4920线是什么原因造成的问题,下一个问题就是:"为什么?"

嗯,首先要注意的是表单的标题是显示的 ftMemo

接下来,注释掉UseMAXdefine,重新编译和执行.结果:没有例外,并注意到表单的标题现在正在显示ftString.

这就是原因:对列大小使用特定数字意味着RTL检索的表元数据会导致客户端Field创建为a TStringField,其值可以通过字符串赋值语句设置.

OTOH,当您指定MAX时,生成的客户端Field类型为ftMemo,这是Delphi的BLOB类型之一,当您将字符串值分配给ftMemo字段时,您将受Data.DB.Pas中的代码的支配,使用a完成对记录缓冲区的所有读取(和写入)TBlobStream.问题是,据我所知,经过大量实验和跟踪代码后,TMemoField使用BlobStream的方式无法正确区分将字段内容更新为''并将字段值设置为Null(与在System.Variants中一样).

简而言之,每当您尝试将TMemoField的值设置为空字符串时,实际发生的情况是该字段的状态设置为Null,这就是导致q中的异常的原因.AFAICS,这是不可避免的,所以对我来说无论如何也没有明显的解决方法.

我没有调查是否之间的选择ftMemo,并ftString通过Delphi的RTL代码或MDAC(ADO)层它坐落于言:我希望它实际上是决定由RecordSetTAdoQuery使用.

QED.请注意,这种系统化的调试方法只需要很少的努力和零尝试和错误就可以解决问题和原因,这正是我在q的评论中所建议的.

另一点是,可以完全跟踪此问题,而无需借助服务器端工具(包括SMSS分析器).没有必要使用分析器来检查客户端发送到服务器的内容,因为没有理由认为服务器返回的错误不正确.这证实了我在客户端开始调查所说的话.

此外,使用使用IfDefed Sql动态创建的表,通过简单观察应用程序的两次运行,可以在一个步骤中有效地隔离问题.

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;
Run Code Online (Sandbox Code Playgroud)