如何将空值显式插入参数化查询?

Posted

技术标签:

【中文标题】如何将空值显式插入参数化查询?【英文标题】:How do I explicitly insert nulls into a parametized query? 【发布时间】:2011-09-15 04:22:47 【问题描述】:

我使用的是 Delphi 7 和 Firebird 1.5。

我有一个在运行时创建的查询,其中一些值可能为空。我无法弄清楚如何让 Firebird 接受我需要保留为空的值的显式空值。在这个阶段,我正在构建 SQL,以便不包含 null 参数,但这很乏味且容易出错。

var
  Qry: TSQLQuery;
begin
  SetConnection(Query); // sets the TSQLConnection property to a live database connection
  Query.SQL.Text := 'INSERT INTO SomeTable (ThisColumn) VALUES (:ThisValue)';
  Query.ParamByName('ThisValue').IsNull := true; // read only, true by default
  Query.ParamByName('ThisValue').Clear; // does not fix the problem
  Query.ParamByName('ThisValue').IsNull = true; // still true
  Query.ParamByName('ThisValue').Bound := true; // does not fix the problem
  Query.ExecSQL;

目前在 DB.pas 中引发了 EDatabaseError "No value for parameter 'ThisValue'"',所以我怀疑这是设计使然,而不是 firebird 问题。

我可以将参数设置为 NULL 吗?如果有,怎么做?

(编辑:抱歉之前没有明确尝试 .Clear。我把它省略了,而是为了提到 IsNull。添加了声明和更多代码)

抱歉,还有一件事:表上没有“NOT NULL”约束。我不认为它走得那么远,但我认为我应该说。

在我结束时显示问题的完整控制台应用程序:

program InsertNull;

$APPTYPE CONSOLE

uses
  DB,
  SQLExpr,
  Variants,
  SysUtils;

var
  SQLConnection1: TSQLConnection;
  Query: TSQLQuery;
begin
  SQLConnection1 := TSQLConnection.Create(nil);

  with SQLConnection1 do
  begin
    Name := 'SQLConnection1';
    DriverName := 'Interbase';
    GetDriverFunc := 'getSQLDriverINTERBASE';
    LibraryName := 'dbexpint.dll';
    LoginPrompt := False;
    Params.clear;
    Params.Add('Database=D:\Database\ZMDDEV12\clinplus');
    Params.Add('RoleName=RoleName');

    //REDACTED Params.Add('User_Name=');
    //REDACTED Params.Add('Password=');

    Params.Add('ServerCharSet=');
    Params.Add('SQLDialect=1');
    Params.Add('BlobSize=-1');
    Params.Add('CommitRetain=False');
    Params.Add('WaitOnLocks=True');
    Params.Add('ErrorResourceFile=');
    Params.Add('LocaleCode=0000');
    Params.Add('Interbase TransIsolation=ReadCommited');
    Params.Add('Trim Char=False');
    VendorLib := 'gds32.dll';
    Connected := True;
  end;
  SQLConnection1.Connected;
  Query := TSQLQuery.Create(nil);
  Query.SQLConnection := SQLConnection1;
  Query.Sql.Text := 'INSERT INTO crs_edocument (EDOC_ID, LINKAGE_TYPE) VALUES (999327, :ThisValue)';
  //Query.ParamByName('ThisValue').IsNull := true; // read only, true by default
//  Query.ParamByName('ThisValue').Value := NULL;
  Query.ParamByName('ThisValue').clear; // does not fix the problem
  Query.ParamByName('ThisValue').Bound := True; // does not fix the problem
//  Query.ParamByName('ThisValue').IsNull; // still true
  Query.ExecSQL;
end.

【问题讨论】:

@moz,你使用哪些组件来执行sql语句? SQlExpr.pas 中抛出此问题的行是对检查“if iFldType = fldUNKNOWN then”的响应,但设置 .DataType := ftInteger 会给出 Firebird ISC ERROR CODE:335544347 - 验证错误. @moz,显然你在 dbexpress 中遇到了一个错误。谷歌搜索实际上发现了一个类似的 SQL server QC,以及与各种数据类型相关的其他几个相似的错误。有些应该通过对 Delphi 2010 的更新来修复(我在这里使用 D2010 并遇到相同的错误,当我到达办公室时将能够在 XE 上进行测试)。 NULL 一直为我工作,但我使用 Interbase Express 组件(即使是那些不支持 Firebird 的组件)。 @Cosmin Prund:发现这一点非常重要,这非常令人放心。现在,如果我们能改变我们使用的连接组件就好了:( @moz 你不应该仅仅为了接受而接受答案。它解决了你的问题吗?如果不是,请不要接受:发布更新结果,获得更多答案。这样,社区获得知识,您最终将解决您的问题。即使您要发布最终答案。 【参考方案1】:

使用TParam.Clear

Query.ParamByName('ThisValue').Clear;

“使用 Clear 将 NULL 值分配给参数。” (来自文档)

【讨论】:

此外,您必须将Bound 属性设置为true Query.ParamByName('ThisValue').Bound:=True; 这并不能解决问题。调用 clear 前参数为 null 并抛出异常。调用后参数为null,抛出异常。 @RRUZ:我已经尝试过了。 IsNull 在我调用 Clear 之前是真的,之后更是如此。错误仍然存​​在。 我使用 TParam.DataType := ...TParam.ClearTParam.Bound := True(按此顺序),它对我来说一直很好用。 -1 因为将 NULL 分配给已分配为 NULL 的参数是没有意义的。在解析 SQL 时,参数的“FNull”在创建时设置为 true。由于之后没有赋值,所以还是NULL。【参考方案2】:

您确定仅通过设置 SQL 的文本来创建参数吗?

试试

if Query.Params.count <> 0 then
// set params
.
.

反正为什么不做 SQL 文本:

'INSERT INTO crs_edocument (EDOC_ID, LINKAGE_TYPE) VALUES (999327, NULL)';

如果您知道该值将为空...

【讨论】:

因为如果我要为每个参数更改 SQL,那是一段非常难看的代码。这个问题实际上发生在具有 ~20 个参数的插入上(诚然,其中只有 ~10 个参数可以为空)。同样,如果 paramcount=0,查询将永远不会起作用,而不是仅在参数为 null 时才会失败。【参考方案3】:

错误的原因是'dbx'不知道参数的数据类型。由于它从未被赋值,因此它的数据类型在执行时为ftUnknown,因此出现错误。 'ParamType' 也一样,但默认假定为 'ptInput',所以没问题。

  Query.ParamByName('ThisValue').DataType := ftString;

肯定不需要Clear 参数,因为它已经是NULL。我们怎么知道? IsNull 正在返回 true...

来自TParam.Clear Method:

使用 Clear 将 NULL 值分配给 参数。

来自TParam.IsNull Property:

表示是否赋值 到参数为NULL(空白)。

肯定不需要Bound参数,因为它完全不相关。当 'Bound' 为 false 时,数据集将尝试从其数据源中为参数提供默认值。 但您的数据集甚至没有链接到数据源。来自documentation:

[...] 代表查询的数据集 和存储过程使用的值 绑定判断是否分配一个 参数的默认值。如果 Bound 为 false,数据集 表示查询尝试分配一个 数据集中的值 他们的 DataSource 属性。 [...]

如果文档不够,请参考'sqlexpr.pas'中TCustomSQLDataSet.SetParamsFromCursor中的代码。它是 dbx 框架中唯一引用参数“绑定”的地方。

【讨论】:

这实际上让我找到了一个解决方法 - 当我设置 DataType 时遇到一个不同的错误,这导致我进入一个建议不同数据库驱动程序的页面。但这不是我可以在生产中使用的东西,所以我已经修改了空参数的 SQL。不过谢谢你的建议,它有帮助。 @moz - 我有点理解你不想接受正确的答案,可能是因为你的 dbx 驱动程序不适合它对你没有帮助的任务。但是,,不要接受不正确的答案,无论它有多少票.. 这适用于字符串和日期时间类型,我现在正在使用它。但是对于 FireBird 枚举类型来说,没有什么好处。所以我接受了。 对于 Firebird 枚举类型(即域),您需要修改约束检查以允许 null CREATE DOMAIN db_enum AS varchar(20) CHECK (value IS NULL or VALUE IN ('Firebird','mysql ','MSSQL')); ALTER DOMAIN mydomain DROP CONSTRAINT; ALTER DOMAIN mydomain ADD CONSTRAINT CHECK ()【参考方案4】:

Sertac 的回答是最正确的,但我也发现驱动程序的选择有所不同

为了其他人的利益,这里有一个改进的测试程序,演示了如何使用 Firebird 1.5 的参数化查询插入空值。

program InsertNull;

$APPTYPE CONSOLE

uses
  DB,
  SQLExpr,
  Variants,
  SysUtils;

var
  SQLConnection1: TSQLConnection;
  Query: TSQLQuery;
  A, B, C: variant;
begin
  SQLConnection1 := TSQLConnection.Create(nil);
  Query := TSQLQuery.Create(nil);

  try
    try
      with SQLConnection1 do
      begin
        Name := 'SQLConnection1';
        DriverName := 'InterXpress for Firebird';
        LibraryName := 'dbxup_fb.dll';
        VendorLib := 'fbclient.dll';
        GetDriverFunc := 'getSQLDriverFB';
        //DriverName := 'Interbase';
        //GetDriverFunc := 'getSQLDriverINTERBASE';
        //LibraryName := 'dbexpint.dll';
        LoginPrompt := False;
        Params.clear;
        Params.Add('Database=127.0.0.1:D:\Database\testdb');
        Params.Add('RoleName=RoleName');
        Params.Add('User_Name=SYSDBA');
        Params.Add('Password=XXXXXXXXXXXX');
        Params.Add('ServerCharSet=');
        Params.Add('SQLDialect=1');
        Params.Add('BlobSize=-1');
        Params.Add('CommitRetain=False');
        Params.Add('WaitOnLocks=True');
        Params.Add('ErrorResourceFile=');
        Params.Add('LocaleCode=0000');
        Params.Add('Interbase TransIsolation=ReadCommited');
        Params.Add('Trim Char=False');
        //VendorLib := 'gds32.dll';
        Connected := True;
      end;

      Query.SQLConnection := SQLConnection1;
      Query.SQL.Clear;
      Query.Params.Clear;
      // FYI
      // A is Firebird Varchar
      // B is Firebird Integer
      // C is Firebird Date
      Query.Sql.Add('INSERT INTO tableX (A, B, C) VALUES (:A, :B, :C)');
      Query.ParamByName('A').DataType := ftString;
      Query.ParamByName('B').DataType := ftInteger;
      Query.ParamByName('C').DataType := ftDateTime;

      A := Null;
      B := Null;
      C := Null;

      Query.ParamByName('A').AsString := A;
      Query.ParamByName('B').AsInteger := B;
      Query.ParamByName('C').AsDateTime := C;

      Query.ExecSQL;
      writeln('done');
      readln;
    except
      on E: Exception do
      begin
        writeln(E.Message);
        readln;
      end;
    end;
  finally
    Query.Free;
    SQLConnection1.Free;
  end;
end.

【讨论】:

【参考方案5】:

TConnection Options 上有一些属性,名为HandlingStringType/将空字符串转换为null。保持真实并假设Query.ParamByName('ThisValue').AsString:=''; 您可以在

中访问它
TConnection.FetchOptions.FormatOptions.StrsEmpty2Null:=True

【讨论】:

以上是关于如何将空值显式插入参数化查询?的主要内容,如果未能解决你的问题,请参考以下文章

Swift 3.0 错误:转义闭包只能按值显式捕获 inout 参数

Swift 4:转义闭包只能通过值显式捕获 inout 参数

将空值作为参数传递给方法时如何修复 NullPointerException [重复]

迭代的参数化查询

这个参数化查询如何防止 SQL 注入?

参数化查询如何帮助防止 SQL 注入?