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

7
我在一件应该很简单的事情上碰壁了。我有一个SQL Server数据库,想要用空字符串更新一个非空的varchar或nvarchar字段。我知道这是可能的,因为空字符串''和NULL不是同一回事。然而,使用TADOQuery时,它不允许我这么做。我正在尝试像这样更新现有记录:
ADOQuery1.Edit;
ADOQuery1['NonNullFieldName']:= '';
//or
ADOQuery1.FieldByName('NonNullFieldName').AsString:= '';
ADOQuery1.Post; //<-- Exception raised while posting

如果字符串中有任何内容,即使只有一个空格,它也可以正常保存,这是我们预期的。但是,如果是空字符串,则会失败:

非 null 列无法更新为 Null。

但它不是 null,它是一个空字符串,应该可以正常工作。我发誓我已经在过去很多次传递了空字符串。
我为什么会得到这个错误,我应该怎么做才能解决它?
其他细节:
  • 数据库:Microsoft SQL Server 2014 Express
  • 语言:Delphi 10 Seattle Update 1
  • 数据库驱动程序:SQLOLEDB.1
  • 正在更新的字段:nvarchar(MAX) NOT NULL
2个回答

13
我可以使用以下代码复现你报告的问题,使用SS2014、OLEDB驱动程序和Seattle时,当表格使用MAX作为列大小和具体数字(在我的情况下为4096)创建时,行为上存在差异。我想提供这个作为一个替代答案,因为它不仅展示了如何系统地调查这个差异,而且还指出了 为什么 会出现这种差异(因此,以后应该如何避免)。请参考并执行以下代码,就像它被写出来的那样,即使激活了UseMAX定义。在执行代码之前,在项目选项中打开“使用Debug DCUs”,立即显示所描述的异常发生在Data.Win.ADODB的第4920行。
Recordset.Fields[TField(FModifiedFields[I]).FieldNo-1].Value := Data

我使用TCustomADODataSet.InternalPost函数,调试窗口显示此时DataNull

接下来,请注意:

update jdtest set NonNullFieldName = ''

在SSMS2014查询窗口中执行没有问题(命令已成功完成),因此似乎在第4920行DataNull是导致问题的原因,下一个问题是“为什么?”

那么,首先要注意的是,窗体标题显示的是ftMemo

接下来,注释掉UseMAX定义,重新编译并执行。结果:没有异常,并且注意到窗体标题现在显示的是ftString

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

然而,当您指定MAX时,生成的客户端Field的类型为ftMemo,这是Delphi的BLOB类型之一,当您将字符串值分配给ftMemo字段时,您就要看Data.DB.Pas中的代码,它使用TBlobStream对记录缓冲区执行所有读取(和写入)。问题在于,据我所见,在经过大量实验和代码跟踪之后,TMemoField使用BlobStream的方法未能正确区分将字段内容更新为''和将字段的值设置为Null(如System.Variants中所示)。

简言之,每当您尝试将TMemoField的值设置为空字符串时,实际发生的是该字段的状态被设置为Null,这就是在该问题中引起异常的原因。在我看来,这是不可避免的,因此对我来说,没有明显的解决方法。

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

证毕。注意,这种系统化的调试方法仅需很少的工作量和零试错,就能揭示问题及其原因,这正是我在评论中试图建议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;

@kobik:目前还没有定论。到目前为止,我尝试将字段设置为空字符串的所有尝试都失败了,大多数与OP报告的相同错误有关,包括尝试AdoDB的第4920行的等效方法,但指定为空字符串而不是它使用的Null,尝试将底层记录集字段设置为#0等。尝试使用以bmWrite模式创建的AdoBlobStream执行时没有投诉,但实际上并没有更新服务器字段。令人恼火的是,执行set NonNullFieldName = ''可以正常工作,并且其DataLength随后报告为0,因此SSMS显然知道如何做到这一点。 - MartynA
@MartynA,这绝对不是ADO层的问题。使用空字符串直接更新ADO记录集与大字段(memo fields)完全正常。我认为这是ADODB Delphi层的一个BUG。ftMemo无法通过TADODataSet更新为空字符串-正如你所说,当更新时,空字符串会转换为NULL。我没有太多时间测试代码中实际的问题所在。 - kobik
另外,您是说 Recordset.Fields[TField(FModifiedFields[I]).FieldNo-1].Value := '' 没有起作用吗?还是您没有尝试过这个方法? - kobik
@kobik: “你是说 Recordset.Fields[TField(FModifiedFields[I]).FieldNo-1].Value := '' 没有起作用吗?” 是的,我尝试过了,它没有起作用 - 我需要提醒自己它产生了什么错误。 - MartynA
1
这个 bug 在 BufferToVar -> Data := Variant(Buffer^); 中。 - kobik
显示剩余4条评论

2
问题出现在使用数据类型中的MAX时。无论是varchar(MAX)还是nvarchar(MAX)都会出现这种情况。当你将MAX替换为一个大数值,例如5000,就会允许空字符串。请注意保留HTML标签。

FYI,SQLOLEDB 驱动程序是一个旧的已弃用提供程序。您可以尝试使用 SQL Server 2012 本机客户端 OLE DB 提供程序(SQLNCLI11)。 - Dan Guzman
@Dan 实际上,该项目旨在接受任何连接到任何ADO兼容数据库的任意连接字符串。这包括像Excel这样的东西,以及旧技术。这个特定的项目实际上是一个数据泵,用于将任何旧数据库中的数据迁移到不同的新数据库中。 - Jerry Dodge
据我最近所读,微软放弃了SQLNCLI并回滚了对OLEDB的支持。https://blogs.msdn.microsoft.com/sqlnativeclient/2017/10/06/announcing-the-new-release-of-ole-db-driver-for-sql-server/ - Jerry Dodge
你所提到的博客文章指出,OLE DB作为关系数据库访问的API已经取消废弃,因为微软此前曾宣布其已被废弃。最新的驱动程序"MSSOLEDBSQL"仍然存在且运行良好。但是,Windows附带的SQLOLEDB提供程序仅用于向后兼容,并且不应该用于新应用程序。 - Dan Guzman
@DanGuzman 是的,我看到了。在找到并重新阅读它之前,我没有记得太多细节。无论如何,SQLNCLI 已经被放弃了,甚至在这篇文章发布之前就已经被放弃了。 - Jerry Dodge

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接