and I am tearing my hair out!!
Even something simple like this work:
procedure MyAdoQueryTest();
const MYSQL_CONNECT_STRING='Driver={MySQL ODBC 5.1 Driver};Server=%s;Port=3306;Database=%s;User=%s;Password=%s;Option=3;';
var AdoConnection : TADOConnection;
ADOQuery : TADOQuery;
Param : TParameter;
begin
AdoConnection := TADOConnection.Create(Nil);
AdoConnection.ConnectionString := Format(MYSQL_CONNECT_STRING,['localhost',
'mysql',
'root',
'']);
AdoConnection.LoginPrompt := False;
AdoConnection.Connected := True;
ADOQuery := TADOQuery.Create(Nil);
ADOQuery.Connection := AdoConnection;
ADOQuery.Sql.Clear();
ADOQuery.SQl.Add('SHOW :what_to_show');
Param := ADOQuery.Parameters.ParamByName('what_to_show');
Param.DataType := ftString;
Param.Value := 'databases';
ADOQuery.Prepared := true;
ADOQuery.Active := True;
end;
(btw, do I really need to use the 'Param' variable and 3 statements, or can I just ` ADOQuery.Parameters.ParamByName('what_to_show').Value := 'databases';?)
Anyway, when I run it, I get an exception at ADOQuery.SQl.Add('SHOW :what_to_show'); which says "Arguments are of the wrong type, are out of the acceptable range or are in conflict with one another".
What I am trying to do is to make 2 central functions: one which will accept and execute any SQL statement which will not return any data (such as INSERT INTO) and oen which will (such as SELECT).
I currently have these working with AdoConnection only, but am now trying to use AdoQuery because I want to parametrize my SQL statements to handle strings with quotes in them.
I can has halpz?
The error is here:
ADOQuery.SQl.Add('SHOW :what_to_show');
The :Param can only be used for values, not for dynamic column/keyword/table/database names.
This is because if it worked like that you'd have an SQL-injection risk depending on the contents of your parameter.
In order to fix that you'll have to inject your what_to_show thingy into the SQL-string.
Like so:
var
what_to_show: string;
begin
....
what_to_show:= 'tables';
ADOQuery.SQL.Text:= ('SHOW '+what_to_show);
....
Now it will work.
Warning
Make sure test everything you inject into the SQL to prevent users from being able inject their SQL-code into your queries.
Parameters prevent SQL injection, but because you cannot use them here you need to check them against a list of pre-approved values. e.g. a stringlist holding all the allowed what_to_shows.
Escaping or use of special chars is useless.
Safe injection example code
var
what_to_show: string;
i: integer;
inputapproved: boolean;
begin
....
what_to_show:= lower(trim(someinput));
i:= 0;
inputapproved:= false;
while (i < WhiteList.count) and not(inputapproved) do begin
inputapproved:= ( what_to_show = lower(Whitelist[i]) );
Inc(i);
end; {while}
if inputapproved then ADOQuery.SQL.Text:= ('SHOW '+what_to_show);
....
Related
I write datasnap application. I should know id of last uploaded record on server and after that upload new records from local db using cliendataset component.
I don't realise meaning of this code, what are there Parameters[0].Value, Parameters[1].Value and how it change correctly in order to return result from server:
FSelectCountryCommand.Parameters[0].Value.SetInt32();
FSelectCountryCommand.ExecuteUpdate;
Result := FSelectCountryCommand.Parameters[1].Value.GetInt32;
if I write manually random integer value, for example
result:=10; //FSelectCountryCommand.Parameters[1].Value.GetInt32;
then it works correctly and returns this value in ClientFormUnit.
My final goal is return result value(maxcountry_id) from Server to buttonclick procedure of ClientForm unit
Query on server:
select max(country_id) as maxcountry_id from country
ClientForm unit:
procedure Tclient.buttonCountryClick(Sender: TObject);
var c:integer;
begin
c:=Clientmodule1.ServerMethods1Client.SelectCountry;
Clientmodule1.lqCountry.close;
Clientmodule1.lqCountry.Params[0].AsInteger:= c;
Clientmodule1.lqCountry.close;
end;
ClientClassesUnit:
function TServerMethods1Client.SelectCountry:Integer;
begin
if FSelectCountryCommand = nil then
begin
FSelectCountryCommand := FDBXConnection.CreateCommand;
FSelectCountryCommand.CommandType :=
TDBXCommandTypes.DSServerMethod;
FSelectCountryCommand.Text := 'TServerMethods1.SelectCountry';
FSelectCountryCommand.Prepare;
end;
FSelectCountryCommand.Parameters[0].Value.SetInt32();
FSelectCountryCommand.ExecuteUpdate;
Result := FSelectCountryCommand.Parameters[1].Value.GetInt32;
end;
ServerMethodUnit:
function TServerMethods1.SelectCountry:Integer;
begin
qCountry.close;
qCountry.Open;
Result:= qCountryMaxCountry_id.AsInteger;
end;
I tried use global variable but without success: it is seen only in project area, not in project group area.
SOLUTION
this code works fine:
FSelectCountryCommand.ExecuteUpdate;
Result := FSelectCountryCommand.Parameters[0].Value.GetInt32;
As I understand, as select query doesn't contain parameters therefore this operator:
FSelectCountryCommand.Parameters[0].Value.SetInt32();
is redundant.
I write this code :
Var Q : TFDQuery;
begin
Q := TFDQuery.Create(Self);
Q.Connection := FDConnection1;
Q.Params.CreateParam(ftString,'N',ptOutput);// Try also ptResult
Q.Params.CreateParam(ftInteger,'ID',ptInput);
Q.SQL.Text := 'SELECT NOM FROM EMPLOYEE WHERE ID_EMP = :ID';
Q.Params.ParamByName('ID').Value := 1;
Q.Active := True;
ShowMessage( VarToStr(Q.Params.ParamByName('N').Value) );
The result should be the name of the employer.
I get an error :
'N' parameter not found
How can I get the result from the Query using the parameter?
If I can't , what is the the function of :
ptOutput
ptResult
Try this code:
procedure TForm1.ExecuteQuery;
var
SQL : String;
Q : TFDQuery;
begin
SQL := 'select ''Sami'' as NOM'; // Tested with MS Sql Server backend
try
Q := TFDQuery.Create(Self);
Q.Connection := FDConnection1;
Q.Params.CreateParam(ftString, 'Nom', ptOutput);// Try also ptResult
Q.SQL.Text := SQL;
Q.Open;
ShowMessage( IntToStr(Q.ParamCount));
Caption := Q.FieldByName('Nom').AsString;
finally
Q.Free; // otherwise you have a memory leak
end;
end;
You'll see that the created parameter no longer exists once the FDQuery is opened, because FireDAC "knows" that there is nothing it can do with it.
Then, replace Q.Open by Q.ExecSQL. When that executes you get an exception
with the message
Cannot execute command returning result set.
Hint: Use Open method for SELECT-like commands.
And that's your problem. If you use a SELECT statement, you get a result set whether
you like it or not, and the way to access its contents is to do something like
Nom := Q.FieldByName('Nom').AsString
You asked in a comment what is the point of ptOutput parameters. Suppose your database has a stored procedure defined like this
Create Procedure spReturnValue(#Value varchar(80) out)
as
select #Value = 'something'
Then, in your code you could do
SQL := 'exec spReturnValue :Value'; // note the absence of the `out` qualifier in the invocation of the SP
try
Q := TFDQuery.Create(Self);
Q.Connection := FDConnection1;
Q.Params.CreateParam(ftString, 'Value', ptOutput);// Try also ptResult
Q.SQL.Text := SQL;
Q.ExecSQL;
ShowMessage( IntToStr(Q.ParamCount));
Caption := Q.ParamByName('Value').AsString;
finally
Q.Free; // otherwise you have a memory leak
end;
which retrieves the output parameter of the Stored Proc into Q's Value parameter.
There is no need to manually create parameters. Data access components are smart enough to parse the SQL string and populate the parameters collection by themselves
Also to get the result you must read the query's fields. When you call Open on a Query component, the fields collection will be populated with the fields that you specified in the SELECT [fields] SQL statement
As a side note, I advice that you use the type-safe version to get the value from a TField or TParameter object: See more here
var
q : TFDQuery;
begin
q := TFDQuery.Create(Self);
q.Connection := FDConnection1;
q.SQL.Text := 'SELECT NOM FROM EMPLOYEE WHERE ID_EMP = :ID';
q.ParamByName('ID').AsInteger := 1;
q.Open;
ShowMessage(q.FieldByName('Nom').AsString);
end;
With an TADOQuery.Locate that uses a list of fields and a VarArray of values, if one of the values contains a # sign, we get this exception:
'Arguments are of the wrong type, are out of acceptable range, or are in conflict with one another.'
I've traced this down to ADODB which itself seems to be using # signs as delimiters.
Is there a way to escape #-signs so that the query doesn't fail?
* EDIT 1 *
I was wrong. What causes this failure is a string that has a pound sign and a single quote. The code shown below fails with error message noted above.
What really worries us is that when it fails running as an .exe outside the IDE, there's no runtime exception. We only see the exception when we're in the IDE. If our programmers hadn't happened to be using data that triggers this we never would have known that the .Locate returned FALSE because of a runtime error, not because a matching record was not found.
Code:
var
SearchArray: Variant;
begin
SearchArray := VarArrayCreate([0,1], VarVariant);
SearchArray[0] := 'T#more''wo';
SearchArray[1] := 'One';
ADOQuery.Locate('FieldName1;FieldName2', SearchArray, []);
Please see Updates below; I've found a work-around that's at least worth testing.
Even with Sql Server tables, the # shouldn't need to be escaped.
The following code works correctly in D7..XE8
procedure TForm1.Button1Click(Sender: TObject);
begin
AdoQuery1.Locate('country;class', VarArrayOf(['GB', Edit1.Text]), []);
end;
when Edit1.Text contains 'D#E', so I think your problem must lie elsewhere. Try a minimalist project with just that code, after rebooting your machine.
Update: As noted in a comment, there is a problem with .Locate where the expression
passed to GetFilterStr (in ADODB.Pas) contains a # followed by a single quote. To try and
work out a work-around for this, I've transplanted GetFilterStr into my code and have
been experimenting with using it to construct a recordset filter on my AdoQuery, as I noticed
that this is what .Locate does in the statement
FLookupCursor.Filter := LocateFilter;
The code I'm using for this, including my "corrected" version of GetFilterStr, is below.
What I haven't managed to figure out yet is how to avoid getting an exception on
AdoQuery1.Recordset.Filter := S;
when the filter expression yields no records.
(Btw, for convenience, I'm doing this in D7, but using XE8's GetFilterStr, which is why I've had to comment out the reference to ftFixedWideChar)
function GetFilterStr(Field: TField; Value: Variant; Partial: Boolean = False): WideString;
// From XE8 Data.Win.ADODB
var
Operator,
FieldName,
QuoteCh: WideString;
begin
QuoteCh := '';
Operator := '=';
FieldName := Field.FieldName;
if Pos(' ', FieldName) > 0 then
FieldName := WideFormat('[%s]', [FieldName]);
if VarIsNull(Value) or VarIsClear(Value) then
Value := 'Null'
else
case Field.DataType of
ftDate, ftTime, ftDateTime:
QuoteCh := '#';
ftString, ftFixedChar, ftWideString://, ftFixedWideChar:
begin
if Partial and (Value <> '') then
begin
Value := Value + '*';
Operator := ' like '; { Do not localize }
end;
{.$define UseOriginal}
{$ifdef UseOriginal}
if Pos('''', Value) > 0 then
QuoteCh := '#' else
QuoteCh := '''';
{$else}
QuoteCh := '''';
if Pos('''', Value) > 0 then begin
QuoteCh := '';
Value := QuotedStr(Value);
end;
{$endif}
end;
end;
Result := WideFormat('(%s%s%s%s%2:s)', [FieldName, Operator, QuoteCh, VarToWideStr(Value)]);
end;
procedure TForm1.CreateFilterExpr;
var
S : String;
begin
// clear any existing filter
AdoQuery1.Recordset.Filter := adFilterNone;
AdoQuery1.Refresh;
if edFilter.Text = '' then Exit;
S := GetFilterStr(AdoQuery1.FieldByName('Applicant'), edFilter.Text, cbPartialKey.Checked);
// Add the filter expr to Memo1 so we can inspect it
Memo1.Lines.Add(S);
try
AdoQuery1.Recordset.Filter := S;
AdoQuery1.Refresh;
except
end;
end;
procedure TForm1.FilterClick(Sender: TObject);
begin
CreateFilterExpr;
end;
Update 2: Try the following:
Copy Data.Win.ADODB.Pas to your project directory
In it, replace GetFilterExpr by the version above, making sure that UseOriginal
isn't DEFINEd, and that ftFixedWideChar is reinstated in the Case statement.
Build and run your project
In XE8 at any rate, my testbed now correctly Locate()s a field ending with ' or #'
(or containing either of them if loPartialKey is specified. (I can't test in XE4/5
because my XE4 now says it's unlicenced since I upgraded to Win10 last week, thanks EMBA!)
I hestitate to call this a solution or even a work-around as yet, but it is at least worth testing.
I'm not sure whether I'd call the original version of GetFilterExpr bugged, because I'm not sure
what use-case its treatment of values containing quotes was intended to handle.
1st off I am Still a little green to Delphi so this might be a "mundane detail" that's being over looked. [sorry in advance]
I have a need to create a TSQLDataset or TClientDataSet from an Oracle 11g cursor contained in a package. I am using Delphi XE2 and DBExpress to connect to the DB and DataSnap to send the data back to the client.
I'm having problems executing the stored procedure from the Delphi code.
Package Head:
create or replace
PACKAGE KP_DATASNAPTEST AS
procedure GetFaxData(abbr varchar2, Res out SYS_REFCURSOR);
END KP_DATASNAPTEST;
Package Body:
create or replace
PACKAGE body KP_DATASNAPTEST AS
procedure GetFaxData(abbr varchar2, Res out SYS_REFCURSOR)is
Begin
open Res for
SELECT Name,
Address1,
City,
fax_nbr
FROM name
JOIN phone on name.Abrv = phone.abrv
WHERE phone.fax_nbr is not null and name.abrv = abbr;
end;
END KP_DATASNAPTEST;
I have no problem executing this procedure in SQL Developer the problem resides in this code on the DataSnap server:
function TKPSnapMethods.getCDS_Data2(): OleVariant;
var
cds: TClientDataSet;
dsp: TDataSetProvider;
strProc: TSQLStoredProc;
begin
strProc := TSQLStoredProc.Create(self);
try
strProc.MaxBlobSize := -1;
strProc.SQLConnection:= SQLCon;//TSQLConnection
dsp := TDataSetProvider.Create(self);
try
dsp.ResolveToDataSet := True;
dsp.Exported := False;
dsp.DataSet := strProc;
cds := TClientDataSet.Create(self);
try
cds.DisableStringTrim := True;
cds.ReadOnly := True;
cds.SetProvider(dsp);
strProc.Close;
strProc.StoredProcName:= 'KP_DATASNAPTEST.GetFaxData';
strProc.ParamCheck:= true;
strProc.ParamByName('abbr').AsString:= 'ZZZTOP';
strProc.Open; //<--Error: Parameter 'Abbr' not found.
cds.Open;
Result := cds.Data;
finally
FreeAndNil(cds);
end;
finally
FreeAndNil(dsp);
end;
finally
FreeAndNil(strProc);
self.SQLCon.Close;
end;
end;
I have also tried assigning the param value through the ClientDataSet without any luck.
I would not be apposed to returning a TDataSet from the function if its easier or produces results. The data is used to populate custom object attributes.
As paulsm4 mentioned in this answer, Delphi doesn't care about getting stored procedure parameter descriptors, and so that you have to it by yourself. To get params of the Oracle stored procedure from a package, you can try to use the GetProcedureParams method to fill the list with parameter descriptors and with the LoadParamListItems procedure fill with that list Params collection. In code it might look like follows.
Please note, that following code was written just in browser according to documentation, so it's untested. And yes, about freeing ProcParams variable, this is done by the FreeProcParams procedure:
var
ProcParams: TList;
StoredProc: TSQLStoredProc;
...
begin
...
StoredProc.PackageName := 'KP_DATASNAPTEST';
StoredProc.StoredProcName := 'GetFaxData';
ProcParams := TList.Create;
try
GetProcedureParams('GetFaxData', 'KP_DATASNAPTEST', ProcParams);
LoadParamListItems(StoredProc.Params, ProcParams);
StoredProc.ParamByName('abbr').AsString := 'ZZZTOP';
StoredProc.Open;
finally
FreeProcParams(ProcParams);
end;
...
end;
I don't think Delphi will automagically recognize Oracle parameter names and fill them in for you. I think you need to add the parameters. For example:
with strProc.Params.Add do
begin
Name := 'abbr';
ParamType := ptInput;
Value := ZZZTOP';
...
end;
In my app I have different forms that use the same datasource (so the queries are the same too), defined in a common datamodule. Question is, is there a way to know how many times did I open a specific query? By being able to do this, I could avoid close that query without closing it "every where else".
Edit: It's important to mention that I'm using Delphi3 and it is not a single query but several.
The idea is to use the DataLinks property of the TDataSource.
But, as it is protected, you have to gain access to it. One common trick is to create a fake descendant just for the purpose of casting:
type
TDataSourceHack = class(TDataSource);
Then you use it like:
IsUsed := TDataSourceHack(DataSource1).DataLinks.Count > 0;
You can get creative using a addref/release like approach. Just create a few functions and an integer variable in your shared datamodule to do the magic, and be sure to call them..partial code follows:
TDMShared = class(tDataModule)
private
fQueryCount : integer; // set to 0 in constructor
public
function GetQuery : tDataset;
procedure CloseQuery;
end;
function TDMShared.GetQuery : tDataset;
begin
inc(fQueryCount);
if fQueryCount = 1 then
SharedDatsetQry.open;
Result := shareddatasetqry; // your shared dataset here
end;
procedure TDMShared.CloseQuery;
begin
dec(fQueryCount);
if fQueryCount <= 0 then
shareddatasetqry.close; // close only when no refs left.
end;
EDIT: To do this with multiple queries, you need a container to hold the query references, and a way to manipulate them. a tList works well for this. You will need to make appropriate changes for your TDataset descendant, as well as create a FreeAndNil function if you are using an older version of Delphi. The concept I used for this was to maintain a list of all queries you request and manipulate them by the handle which is in effect the index of the query in the list. The method FreeUnusedQueries is there to free any objects which no longer have a reference...this can also be done as part of the close query method, but I separated it to handle the cases where a specific query would need to be reopened by another module.
Procedure TDMShared.DataModuleCreate(Sender:tObject);
begin
dsList := tList.create;
end;
Function TDMShared.CreateQuery(aSql:String):integer;
var
ds : tAdoDataset;
begin
// create your dataset here, for this example using TADODataset
ds := tAdoDataset.create(nil); // self managed
ds.connection := database;
ds.commandtext := aSql;
ds.tag := 0;
Result := dsList.add(ds);
end;
function TDMShared.GetQuery( handle : integer ) : tDataset;
begin
result := nil;
if handle > dsList.count-1 then exit;
if dsList.Items[ handle ] = nil then exit; // handle already closed
result := tAdoDataset( dsList.items[ handle ]);
Inc(Result.tag);
if Result.Tag = 1 then
Result.Open;
end;
procedure TDMShared.CloseQuery( handle : integer );
var
ds : tAdoDataset;
begin
if handle > dsLIst.count-1 then exit;
ds := tAdoDataset( dsList.items[ handle ]);
dec(ds.Tag);
if ds.Tag <= 0 then
ds.close;
end;
procedure TDMShared.FreeUnusedQueries;
var
ds : tAdoDataset;
ix : integer;
begin
for ix := 0 to dsList.Count - 1 do
begin
ds := tAdoDataset(dsLIst.Items[ ix ]);
if ds.tag <= 0 then
FreeAndNil(dsList.Items[ix]);
end;
end;
procedure TDMShared.DataModuleDestroy(Sender: TObject);
var
ix : integer;
begin
for ix := 0 to dsList.count-1 do
begin
if dsLIst.Items[ix] <> nil then
FreeAndNil(dsLIst.Items[ix]);
end;
dsList.free;
end;
Ok, a completely different solution...one that should work for Delphi 3.
Create a new "Descendant Object" from your existing dataset into a new unit, and add some behavior in the new object. Unfortunately I do not have Delphi 3 available for testing, but it should work if you can find the proper access points. For example:
TMySharedDataset = class(tOriginalDataset)
private
fOpenCount : integer;
protected
procedure Internal_Open; override;
procedure Internal_Close; override;
end;
TMySharedDataset.Internal_Open;
begin
inherited Internal_Open;
inc(fOpenCount);
end;
TMySharedDataset.Internal_Close;
begin
dec(fOpenCount);
if fOpenCount <= 0 then
Inherited Internal_Close;
end;
Then just include the unit in your data module, and change the reference to your shared dataset (you will also have to register this one and add it to the palette if your using components). Once this is done, you won't have to make changes to the other units as the dataset is still a descendant of your original one. What makes this all work is the creation of YOUR overridden object.
You could have a generic TDataSet on the shared datamodule and set it on the OnDataChange, using the DataSet property of the Field parameter
dstDataSet := Field.DataSet;
This way, when you want to close the dataset, close the dataset on the datamodule, which is a pointer to the correct DataSet on some form you don't even have to know