How to get the AUTOINC value when connecting via ADO? - delphi

I am trying to connect my database (Advantage 7.1 Server) using the Advantage OLE DB provider. So far so good...It connects with no problem with the code below:-
const
// the database we'll be connecting to
ConnectionString = 'Provider=Advantage OLE DB Provider;Data Source=C:\Data\'+
'UsersData.add;ServerType=ADS_REMOTE_SERVER|ADS_LOCAL_SERVER;User ID=ISUsers;Password=aAoO31';
My problem is, even though I'm able to connect to the database, any field with AUTOINC as the data type does not generate next numbers. "ID" as AUTOINC keeps on giving me zero (0) anytime I append the data instead of moving to the next number 1, 2, 3.... But for the same code if I switch to MS ACCESS, it works perfectly. What am I doing wrong? please find code below.
// Add template to database. Returns added template ID.
function TDBClass.addTemplate(template: TTemplate): Integer;
var
rs: TADODataSet;
tptStream: TMemoryStream;
id: Integer;
p: PChar;
begin
// get DB data and append one row
rs := TADODataSet.Create(nil);
rs.Connection := connection;
rs.CursorType := ctStatic;
rs.LockType := ltOptimistic;
rs.CommandText := 'SELECT * FROM enroll';
rs.Open();
rs.Append();
tptStream := TMemoryStream.Create();
// write template data to memory stream.
SafeArrayAccessData(template.tpt, Pointer(p));
tptStream.write(p^, template.size);
SafeArrayUnaccessData(template.tpt);
// save template data from memory stream to database.
(rs.FieldByName('template') as TBlobField).LoadFromStream(tptStream);
// update the database with added template.
rs.post();
// get the ID of enrolled template.
id := rs.FieldByName('ID').AsInteger;
// close connection
tptStream.Free();
rs.Close();
rs.Free();
addTemplate := id;
end;

You should consider using the TADSConnection, TADSQuery, etc. components that you can still download from the DevZone (http://devzone.advantagedatabase.com/dz/content.aspx?key=1) even for ADS 7.1.
If you have to use ADO, you probably have to use a different approach. (But see also bummi's comment about a possible bug in Delphi).
One way would be to use the LASTAUTOINC scalar function:
INSERT INTO
enroll
(
template
)
VALUES
(
:template
);
SELECT
LASTAUTOINC(CONNECTION) AS "id"
FROM
system.iota

In Delphi ADO you can pass more than 1 statement in your SQL query string - This might true for ADO in general.
Insert into test (text) Values('ddd'); Select * from test where AutoIncColumn = Scope_identity()
This will return the inserted row with the Autoinc value
or if you need just the autoinc value
Insert into test (text) Values('ddd'); Select Scope_identity() AutoIncColumn

Related

Making TJvAppDBStorage work with FireDAC/Firebird

Using either Delphi 10.2 or 10.3, FireDAC and either Firebird 2.5 or 3.0: I've used the JVCL TJvAppStorage components for years and never had a problem with them, either to INI/XML storage or to a table in an AbsoluteDB database. I'm trying to migrate an app from AbsoluteDB to Firebird via FireDAC, and can't get the TJvAppDBStorage to write entries - it returns no errors, but nothing actually gets written into the table.
I have a datamodule containing the FireDAC connection and driver components, the JvAppDBStorage, a TDataSource and a TFDTable component. The FDB exists containing an appropriate table, the TFDTable is open on that table, the JvAppDBStorage has its properties set to match the table's fields, and the TFDTable, datasource and JvAppDBStorage are properly linked. (This all mirrors what has existed and worked against AbsoluteDB.) A call to dmStorage.FBStorage.WriteString(dmStorage.FBStorage.ConcatPaths(['General', 'LastStarted']), FormatDateTime(StdDTFmtStr, Now)); does not throw any exceptions, but nothing actually gets written into the table. Doing a normal append/set fields/post construct via the TFDTable works properly.
Any help appreciated!
Steve
There are 3 critical parts to this:
The underlying table must have a PK. I have an ID column with an associated sequence with before-insert trigger to make an auto-inc column for my PK, and an index that's a merge of the SectionID and KeyID fields to speed up the Locate() calls.
When creating the TFDTable object, you must create a new TFDTransaction object and attach it to the TFDTable and the TFDConnection.
On the TFDTable, you must set UpdateOptions.UpdateMode to upWhereKeyOnly. If you only do the first two steps, reads and appends will work but edits will not.
function TFDDBMgr.MakeTableObj(const ATblName: String): TFDTable;
begin
Result := TFDTable.Create(Self);
with Result do begin
//hook up all our needed stuff
Connection := Self.Connection;
TableName := ATblName;
CachedUpdates := False;
{...}
//!!.SS 09/11/19: REQUIRED for TJvAppDBStorage use,
//also requires that a PK be defined on the table
UpdateOptions.UpdateMode := upWhereKeyOnly;
Transaction := TFDTransaction.Create(Result);
Transaction.Connection := Self.Connection;
end;
end;

Datasnap - insert record with parameter

On DatasnapServer I have :
TSQLConnection connected to my database.
SQLDataset1 (CommandType=ctQuery) that fetches data from my table (lets call it "RESORTS".
DataSetProvider1 is connected to my SQLDataset1.
DataSetProvider1 is set to AllowCommandText.
This works OK. Server starts without a problem. Data is obtained.
On the Client side I have :
SQLConnection1 which is connected OK.Driver is Datasnap.
DSProviderConnection1 is linked to my SQLConnection1.
Both connected without problem.
Then I have DataSource1 which is connected to a ClientDataSet1.
ClientDataSet1 is connected to my DataSetProvider1.
Setting it active retrieves the data from the server.
All displayed right in the grid.
On the Client form I have a Edit1 and a Button1.
I try and run a query using the ClientDataset1
procedure TForm2.Button1Click(Sender: TObject);
begin
ClientDataSet1.Close;
ClientDataSet1.CommandText := ' INSERT INTO RESORTS (RES_NAME) VALUES (:RN)';
ClientDataSet1.FieldByName('RN').AsString := Edit1.Text;
ClientDataSet1.Execute;
ClientDataSet1.Open;
end;
I get : ClientDataSet1: Field 'RN' not found.
So, I am wondering what is going on? Why cant I insert data with parameter?
If I substitute the parameter with :
ClientDataSet1.CommandText := ' INSERT INTO RESORTS (RES_NAME) VALUES ("TRY")';
I get :Remote error: SQLDataSet1: Cursor not returned from Query.
However, the data does get inserted.
What am I doing wrong here ?
(Rewritten based on new information provided by the poster in comments.)
Your entire approach is wrong. :-) You don't use parameters, SQL or CommandText. The TClientDataSet.CommandText documentation clearly says:
CommandText specifies what data the client dataset wants to receive from its (internal or external) provider. It is either:
An SQL statement (query) for the database server to execute.
The name of a table or stored procedure
An SQL statement (query) means only a SELECT is acceptable SQL. An INSERT is not a query, so it cannot be used in a CommandText to insert data.
To insert data in a TClientDataSet, you simply Insert or Append, and then use FieldByName to set the value, and then call the Post method:
ClientDataSet1.Insert;
ClientDataSet1.FieldByName('RES_NAME').AsString := Edit1.Text;
ClientDataSet1.Post;
To edit, you simply use Edit instead of Insert or Append; the rest stays exactly the same.
ClientDataSet1.Edit;
ClientDataSet1.FieldByName('RES_NAME').AsString := Edit1.Text;
ClientDataSet1.Post;
When you're ready to actually update the server data from the changes made in the TClientDataSet, call it's ApplyUpdates:
ClientDataSet1.ApplyUpdates(0);
You can use ClientDataSet1.ParamByName('RN').AsString := Edit1.Text;

How to read a BLOB(Text) field using TpFIBQuery components from FibPlus

I am using Delphi 7 with the FibPlus components . One of them being TpFIBQuery.
I am loading data from a table with the generic
select * from TableName where Key = 1
One of the fields returned is of type BLOB(Text).
I can't seem to get the value into a string list informatie using either of the following 3 ways :
Informatie.Text := FieldByName('Informatie').AsString // Returns the string 'BLOB'
Informatie.Text := BlobAsString('Informatie') // Returns ''
BlobToStrings('Informatie',Informatie) // Returns ''
I have confirmed using Database Workbench that the field in the table indeed contains the saved text.
Anyone ?
usualy, i do like this
var
sl: TStrings; // blob IS NOT string!
ms: TMemoryStream;
begin
sl := TStringList.Create;
ms := TMemoryStream.Create;
try
q.FieldByName('x').SaveToStream(ms);
ms.Position := 0;
sl.LoadFromStream(ms);
// do what ever you want with sl here
// and here too
finally
sl.Free;
ms.Free;
end; // try..finally
end;
note that q is your TpFibQuery object.
also
select * from table is bad bad practice.
that habit eventually will lead you continuous headache.
After trying the solution of #jiang, which produced the same error, I finally have found the culprit.
Turns out it was an error due to my part ( it usually is, you just have to find it ).
Turns out I had set the read transaction to False sometime during the processing/reading of the fields of the original query.
I perform a lookup in another table to get the description of a integer value in the query.
This lookup query uses the same read transaction , and sets this transaction to False after looking up the description.
AFter returning to the original query, reading integer and string fields pose no problem (although the read transaction has been set to False), but reading a BLOB field into a string with the ...AsString method produces an error or returns 'BLOB'.
Obviously I need to set the read transaction to True at the start of the read actions and set it to False after ALL read transactions. This is the major change when you convert a Paradox BDE application to a Firebird of Sqlserver application.
Anyway, I am glad I have found the solution. Hopefully it will help others too.

Firebird TIBQuery insert with returning ... INTO

I have a firebird 2.x database with Generator and a trigger to generate the key field.
I need to get the returned value from below query.
INSERT INTO XXXX (vdate,description) values ('"+ VDate +"','"+ Description +"') returning vno INTO :ParamVoucherNo
I tried several versions of below code but it dont wrok and I get
Dynamic sql error sql error code = -104
Is it really possible to get the return value in delphi using TIBQuery ?
Query1->SQL->Clear();
Query1->SQL->Add("INSERT INTO XXXX (vodate,description) values ('"+ VDate +"','"+ Description +"') returning vno INTO :ParamVoucherNo");
Query1->Params->ParamByName("ParamVoucherno")->ParamType = ptResult;
Query1->Params->ParamByName("ParamVoucherno")->DataType = ftInteger;
Query1->Params->ParamByName("ParamVoucherno")->Value = "";
Query1->Prepare();
Query1->ExecSQL();
Any suggestions?
From Firebird README.returning:
The INTO part (i.e. the variable list) is allowed in PSQL only (to
assign local variables) and rejected in DSQL.
As IBX uses DSQL, you should exclude INTO part from your query.
INSERT ... RETURNING for DSQL looks the same as a call of a stored procedure, which returns result set. So, you have to use Open instead of ExecSQL.
Your mixing of dynamic SQL with parameters is just confusing.
Do this instead:
Query1->SQL->Clear();
Query1->SQL->Add("INSERT INTO table1 (vodate,description) VALUES"+
"(:VoDate,:Description) RETURNING vno INTO :VoucherNo ");
Query1->Params->ParamByName("VoDate")->Value = VDate;
Query1->Params->ParamByName("description")->Value = Description;
Query1->Prepare();
Query1->ExecSQL();
VoucherNo = Query1->Params->ParamByName("VoucherNo")->AsInteger;
Using Delphi 6 I have the ID returning successfully using an EXECUTE BLOCK statement:
EXECUTE BLOCK
RETURNS ( DeptKey INT )
AS
BEGIN
INSERT INTO DEPARTMENT
( COMPANY_KEY, DEPARTMENT_NAME )
VALUES ( 1, 'TEST1' ) RETURNING DEPARTMENT_KEY INTO :DeptKey;
SUSPEND;
END;
From Delphi you can do the folliowing:
FQuery.SQL.Text := '<Execute Block Statement>';
FQuery.Open();
ANewKey := FQuery.Fields[0].AsInteger;
IBX is not Firebird ready
you can take a look at FIBPLUS who support Firebird features
FIBPlus also supports FB2.0 insert ... into ... returning. Now you
should not bother about getting generator values from the client but
leave them in the trigger. You can also use RDB$DB_KEY. New possible
variants of work with insert returning and RDB$DB_KEY are shown in the
example “FB2InsertReturning”.
Why not get the next value for VoucherNo first, followed by
"INSERT INTO table1 (vno, vodate,description) VALUES (:VoucherNo,:VoDate,:Description)");
?
Your trigger can then either be dispensed with (which is nice), or modified to detect null (or <= zero can be useful too) and only then populate the vno field.
create trigger bi_mytable
active before insert position 1
on mytable
as
begin
if (new.vno is null)
then new.vno = next value for gen_VoucherNos;
end
Client-side you can :
select gen_id(gen_VoucherNos, 1) from rdb$database;
By modifying the trigger in this manner you save yourself a headache later on if/when you want to insert blocks of records
I wonder if that INSERT can be wrapped into EXECUTE BLOCK command.
Would IBX manage EXECUTE BLOCK then?
http://www.firebirdsql.org/refdocs/langrefupd20-execblock.html
http://firebirdsql.su/doku.php?id=execute_block
Hope to try it in both IBX and Unified Interbase in XE2
PS: Even if it does not, I found the library, that tells to work on top of IBX of Delphi XE2 (both x86 and x64) and to add EXECUTE BLOCK support: http://www.loginovprojects.ru/index.php?page=ibxfbutils#eb.
As I know there should be some changes to IBX made. Internally INSERT ... RETURNING should be treated the same way as a selectable procedure with returning parameters.
i know this question was answered a long time ago, but i must write this as clear as possible, for those who need this as i was.
i too, needed the "INSERT..RETURNING" thing.
the Delphi drove me crazy for a long time, until i changed my Data access components.
i even moved from Delphi XE2, to XE5 only because of that...
conclusion : IBX does NOT support RETURNING!
FireDAC is PERFECT for what i need with Firebird.
just move to FireDAC and you'll be able to do everything you need, and with high performance.
If you have a table with this 2 Fields: GRP_NO and GROUPNAME and you want to get the new GRP_NO you have to use RET_ as prefix, see example:
procedure TFormDatenbank.Button1Click(Sender: TObject);
var
q: Uni.TUniQuery;
ID: Integer;
GroupName: String;
begin
GroupName := 'MyGroupName';
q := TUniQuery.Create(nil);
try
q.Connection := Datenmodul.UniConnection;
q.ParamCheck := true; // the default value of ParamCheck is true.
q.SQL.Clear;
q.SQL.Add('SELECT GRP_NO, GROUPNAME FROM GROUPDATA WHERE GROUPNAME = :GROUPNAME');
q.ParamByName('GROUPNAME').AsString := GroupName;
q.Open;
if q.RecordCount > 0 then
ID := q.FieldByName('GRP_NO').AsInteger
else
begin
// there exist no group with this name, so insert this new name
q.SQL.Clear;
q.SQL.Add('INSERT INTO GROUPDATA');
q.SQL.Add('(GROUPNAME)');
q.SQL.Add('VALUES');
q.SQL.Add('(:GROUPNAME)');
q.SQL.Add('RETURNING GRP_NO;');
q.ParamByName('GROUPNAME').AsString := GroupName;
q.Execute;
ID := q.ParamByName('RET_GRP_NO').AsInteger;
end;
finally
q.Free;
end;
end;
From the IBx2 sources, you can do it like this:
//Uses IBSql;
//var Cur: IResults;
IBSQL1.SQL.Text := 'delete from tbl_document where id = 120 returning id;';
IBSQL1.Prepare;
if IBSQL1.Prepared then
begin
Cur := IBSQL1.Statement.Execute(IBTransaction1.TransactionIntf);
WriteLn(Cur.Data[cou].AsString);
Cur.GetTransaction.Commit(True);
end;
IResults interface Code:
IResults = interface
function getCount: integer;
function GetTransaction: ITransaction;
function ByName(Idx: String): ISQLData;
function getSQLData(index: integer): ISQLData;
procedure GetData(index: integer; var IsNull:boolean; var len: short; var data: PChar);
procedure SetRetainInterfaces(aValue: boolean);
property Data[index: integer]: ISQLData read getSQLData; default;
property Count: integer read getCount;
end;
Test enviroment:
Arch Linux X86
Firebird 3
Lazarus 1.9
FPC 3.0.4
Quick note: This works on new Firebird API in the IBX, But I didn't test it in Legacy Firebird API with the IBX.

Everytime I try to communicate with my database with stored procedures I get this "Cannot perform this operation on a closed dataset"

I'm working on a Delphi project with a MS SQL Server database, I connected the database with ADOConnection, DataSource and ADOProc components from Borland Delphi 7 and I added this code in behind:
procedure TForm1.Button2Click(Sender: TObject);
begin
ADOStoredProc1.ProcedureName := 'sp_Delete_Clen';
ADOStoredProc1.Refresh;
ADOStoredProc1.Parameters.ParamByName('#clenID').Value := Edit6.Text;
ADOStoredProc1.Active := True;
ADOStoredProc1.ExecProc;
end;
The component Edit6 is an editbox that takes the ID of the tuple that should be deleted from the database and ADOStoredProc1 is the stored procedure in the database that takes 1 parametar (the ID you want to delete).
The project runs with no problems, I even got a TADOTable and a DBGrid that load the information from the database, but when I try to delete a tuple from the database using its ID written in the EditBox I get this Error: "Cannot perform this operation on a closed dataset" and the breakpoint of the project is when the application tries to add the value for the 'clenID' parameter.
Where is my mistake and how to fix it?
I think the ADOStoredProc1.Refresh method is not appropriate here. In this case the stored procedure does not return a result set. Could you leave it out? And also the line ADOStoredProc1.Active := True. The connection to the database is open I presume? Could you also check the values of the Parameters collection in the Object Inspector?
I think you want to call ADOStoredProc1.Parameters.Refresh, not ADOStoredProc1.Refresh.
Also, you should only set Active to True if the SQL Server Stored procedure returns a dataset - i.e. the result of a SELECT statement. Setting Active to True is the same as calling Open.
If the stored procedure only returns a result code (RETURN n), then use ExecProc.
In no case should you use both ADOStoredProc1.Active := True; and ADOStoredProc1.ExecProc;
In summary, you probably want something like
procedure TForm1.btnDeleteClick(Sender: TObject);
begin
ADOStoredProc1.ProcedureName := 'sp_Delete_Clen';
ADOStoredProc1.Parameters.Refresh; // gets the parameter list from SQL Server
ADOStoredProc1.Parameters.ParamByName('#clenID').Value := edtID.Text;
ADOStoredProc1.ExecProc;
end;

Resources