Question
I am wondering if there is any way I can move to a specific record in a DataSet (FDQuery) by knowing only the Primary Key of the table.
What I know
I know I can move to the next/prior record by using FDQuery.Next; FDQuery.Prior; or move to a specific record number using FDQuery.RecNo := 2.
In my case, I just know the Primary Key (id) of the item and I want to move to this specific record that has the same id as mine.
To make it clear I can achieve this by iterating through the records, however, I would like to know if the is a way to move directly to the record, without needing to iterate through all the records.
with FDQuery do
begin
First;
while not Eof do
begin
if FieldByName(C_ID).AsInteger = IDAsPrimaryKey then
// Found!
Break;
Next;
end;
end;
The simplest way to do this is to use the Locate boolean function, as in
if FDQuery.Locate(C_ID, IDAsPrimaryKey, []) then
// do whatever
See e.g. http://docwiki.embarcadero.com/RADStudio/Rio/en/Using_Locate for more info.
Note that Locate will accept a ;-separated list of field names as the first argument. In that case, the second argument needs to be a variant array of field values, which you can construct at run-time using the VarArrayOf function.
Btw, Locate is defined in the TDataSet class, but it is up to the authors of a given descendant library like FireDAC whether and how it is implemented in specific component classes.
You could also use the GoToKey or FindKey methods - see http://docwiki.embarcadero.com/RADStudio/Rio/en/Executing_a_Search_with_Goto_Methods. You may find one or other of these to be quicker than Locate, especially if your dataset already has a client-side index on the primary key but Locate is generally more convenient and concise, because the others are more long-winded to use.
Related
I located some records by this code:
ADOQuery1.Locate('field1',ADOQuery2.FieldByName('field2').Value,[])
How to go to the last one of these records?
You have a number of options. The best depends on a whole lot of considerations you haven't mentioned in your question. I'll provide a very brief overview of the options to avoid this becoming "too broad". It'll be up to you to make your choice and figure out the details. If you get stuck, you can ask a new, more specific question.
Using Locate
A solution involving Locate is only feasible if your dataset is sorted by the same field you're searching on.
Clearly your Search Value is not a unique key. So I'm guessing that you're trying to find the last row matching Search Key in data sorted by some other unique field. (Otherwise the concept of last is meaningless.)
So it's highly probable this is not appropriate for you; unless your data is ordered by a composite key of your search field followed by a unique key.
The approach is simple: navigate forwards until you find a row where the search value doesn't match, then backtrack by 1 row.
if not DataSet.Locate(SearchField, SearchValue, []) then
{ handle not found case as desired }
else
begin
while (not DataSet.Eof) and (DataSet.FieldByName(SearchField).Value = SearchValue) do
DataSet.Next;
{ Watch out for case that last row in dataset matches search value }
if (DataSet.FieldByName(SearchField).Value <> SearchValue) then
DataSet.Prior;
end;
Implement your own search
This is straight-forward and will always work. But it is inefficient, having O(n) complexity. So not advised for large datasets.
DataSet.Last;
while (not DataSet.Bof) and (DataSet.FieldByName(SearchField).Value <> SearchValue) do
DataSet.Prior;
NOTE: In order to mirror behaviour of Locate it would be advisable to enhance this method to deal with the case where a match is not found at all. In that case the active record should not be inadvertently changed as a side-effect of the search.
Use filtering
Obviously this solution depends on whether filtering the dataset is appropriate to the rest of your code. But it is a fairly simple option, and depending factors beyond the scope of this answer, it can be more performant than the previous option.
DataSet.Filtered := False;
{ The next line may be a little tricky.
Ensure the filter string is appropriate for the data-types involved. }
DataSet.Filter := '<string of the form SearchField = SearchValue>';
DataSet.Filtered := True;
DataSet.Last;
See documentation on the Filter property.
NOTE: It may be advisable to take precaution against setting the filter redundantly.
Use a master-detail relationship
This option is included because your question code indicates the SearchValue comes from the active record of another dataset. You're using ADO, so this option is available to you.
DataSet.MasterSource := <Appropriate DataSource>;
DataSet.MasterFields := SearchField;
DataSet.Last;
See documentation on master-detail relationships and on ADO MasterFields.
Offload the work to the RDBMS
Finally, it's worth considering using a stored procedure to get the information you need directly from the database. The advantage is that the server can leverage available indexes and have the potential to provide the most performant option. Again though, a lot depends on the particulars of your application.
A query along the following lines can form the basis of your stored procedure.
select MAX(UniqueField) as RowKey
from Table
where SearchField = SearchValue
Then call your stored procedure, and use its result to find the desired row.
DataSet.Locate(UniqueField, RowKey, []);
NOTE: Don't forget to consider the stored procedure returning NULL if no rows with SearchValue exist.
General Disclaimer
All the above code is extremely brief and for illustrative purposes only. In many cases additional code is required for a robust implementation.
E.g. It might be necessary to DisableControls and enable them again.
NOTE: It's very important with the above to be aware of the actual ordering of the data in your datasets. Failure to take this into account can lead to incorrect behaviour. Even the last option may exhibit worse than expected performance if your dataset is not sorted by UniqueKey.
If your table has an Autoincrement identity field you can do this
adoquery1.sql.clear;
adoquery1.sql.add('select top 1 * from yourtablename where field1=value1 and filed2=value2 order by yourAIcolums desc')
adoquery1.execsql;
value1 and value2 are your desired values.pass them as parameters or put them in command text
this way you get only row you want and no need to loop
I need to implement a combobox, which is bound to a TpFIBDataSet (descendant of TDataSet). I've done this several times before. It's not a big thing if it contains only predefined values.
This time, I'd like to have a combobox that accepts custom values entered by the user, also giving the ability to the user to select some predefined value. Newly entered values shall be inserted into some table of the database just before the record the combobox's field belongs to is posted.
The main problem seems to me, that predefined values are internally represented as integer IDs (the combobox I use is TwwDBComboBox from Roy Woll's InfoPower package, as it implements maplist functionality) because the field is a foreign key, while custom values may be nearly everything (only restricted by a mask).
How can I distinguish between an integer ID and integer user-input, for example?
See the set properties of the combobox:
AComboBox.Style := csDropDown;
AComboBox.MapList := True;
I don't request a solution as take this piece of code and be happy. I'm rather looking for some advice by others who might have or had a similar problem.
How can I distinguish between an integer ID and integer user-input, for example?
You go back to the database. Either query directly select count(*) from table where id = ComboBoxId.
Or use the Locate method of the dataset.
Or keep a cache handy in a MyList: TList<Integer> and do a MyList.BinarySearch to see if the item is already in the DB.
Obviously the cache will only work if the DB is single-user, because otherwise you will not be able to keep it up-to-date.
If it is not in the DB, you run the insert query.
After it's inserted you run the default combobox behavior, because now the values is sure to be in the DB.
I have written a stored procedure that includes a SELECT on a number of tables that applies logic to calculate values and transforms some of the data.
I have been asked if I can exclude records from the resultset in the stored procedure and write the record to a separate log table. I was looking to loop through the result set from the SELECT statement and delete the record I want to exclude once I have written it to a table. At the moment I am struggling to find the syntax to delete from the result set of a SELECT statement in a stored procedure and can only find how to use the cursor reference to delete from the original database table.
I need to remove the records in the same stored procedure and I am looking to avoid duplicating the logic by using some of the logic to find the records to include and repeat some of the logic again to be able to find the records to exclude. The only other alternative I can think of is using a temporary table, but I think what I am trying to do should be possible.
Any help appreciated.
When you have an open cursor in a stored procedure (or in an application), you can perform positioned deletes. You can execute the statement,
DELETE WHERE CURRENT OF cursorname;
Please be aware that by default issuing a COMMIT statement will close any open cursors, so if you plan to have this delete operation spread over multiple transactions you will need to declare your cursors using WITH HOLD.
I am scanning an SQLite database looking for all matches and using
OneFound:=False;
if tbl1.FieldByName('Name').AsString = 'jones' then
begin
OneFound:=True;
tbl1.Next;
end;
if OneFound then // Do something
or should I be using
if not(OneFound) then OneFound:=True;
Is it faster to just assign "True" to OneFound no matter how many times it is assigned or should I do the comparison and only change OneFuond the first time?
I know a better way would be to use FTS3, but for now I have to scan the database and the question is more on the approach to setting OneFound as many times as a match is encountered or using the compare-approach and setting it just once.
Thanks
Your question is, which is faster:
if not(OneFound) then OneFound:=True;
or
OneFound := True;
The answer is probably that the second is faster. Conditional statements involve branches which risks branch mis-prediction.
However, that line of code is trivial compared to what is around it. Running across a database one row at a time is going to be outrageously expensive. I bet that you will not be able to measure the difference between the two options because the handling of that little Boolean is simply swamped by the rest of the code. In which case choose the more readable and simpler version.
But if you care about the performance of this code you should be asking the database to do the work, as you yourself state. Write a query to perform the work.
It would be better to change your SQL statement so that the work is done in the database. If you want to know whether there is a tuple which contains the value 'jones' in the field 'name', then a quicker query would be
with tquery.create (nil) do
begin
sql.add ('select name from tbl1 where name = :p1 limit 1');
sql.params[0].asstring:= 'jones';
open;
onefound:= not isempty;
close;
free
end;
Your syntax may vary regarding the 'limit' clause but the idea is to return only one tuple from the database which matches the 'where' statement - it doesn't matter which one.
I used a parameter to avoid problems delimiting the value.
1. Search one field
If you want to search one particular field content, using an INDEX and a SELECT will be the fastest.
SELECT * FROM MYTABLE WHERE NAME='Jones';
Do not forget to create an INDEX on the column, first!
2. Fast reading
But if you want to search within a field, or within several fields, you may have to read and check the whole content. In this case, what will be slow will be calling FieldByName() for each data row: you should better use a local TField variable.
Or forget about TDataSet, and switch to direct access to SQLite3. In fact, using DB.pas and TDataSet requires a lot of data marshalling, so is slower than a direct access.
See e.g. DiSQLite3 or our DB classes, which are very fast, but a bit of higher level. Or you can use our ORM on top of those classes. Our classes are able to read more than 500,000 rows per second from a SQLite3 database, including JSON marshalling into objects fields.
3. FTS3/FTS4
But, as you guessed, the fastest would be indeed to use the FTS3/FTS4 feature of SQlite3.
You can think of FTS4/FTS4 as a "meta-index" or a "full-text index" on supplied blob of text. Just like google is able to find a word in millions of web pages: it does not use a regular database, but full-text indexing.
In short, you create a virtual FTS3/FTS4 table in your database, then you insert in this table the whole text of your main records in the FTS TEXT field, forcing the ID field to be the one of the original data row.
Then, you will query for some words on your FTS3/FTS4 table, which will give you the matching IDs, much faster than a regular scan.
Note that our ORM has dedicated TSQLRecordFTS3 / TSQLRecordFTS4 kind of classes for direct FTS process.
Using TDataSet.FindKey you can locate records. When it results in True the datasets cursor will be positioned on the found record.
I use this when I select items in a list on the left, corresponding data should appear on the right.
When it results in False the cursor is not moved. This results in the record data prior to FindKey being displayed in data aware components on the right.
How can I code the result of FindKey to return an empty record?
if Not tblSomeTable.FindKey([SomeSearchData]) then
begin
< code to return empty or move data cursor to neutral position >
end;
Update: (Waited a few days before selecting right answer as I believe that is the custom and didn't wan to discourage further feedback.) There were several suggestions on tackling this situation although I believe the correct answer was from Marcelo in that it is not possible to have a cursor not be on a record. Several workarounds were suggested. I chose one of my own. It went something like:
If Not tblSomeTable.FindKey([SomeSearchData]) then
begin
tblSomeTable.FindKey([-1,2010]);
end
What I did is create a dummy, blank record with an index that the actual data can never be, ie: The first index value will never be -1. If the initial search comes up empty then the FindKey will position the cursor on this empty record. This will provide the visual effect I was after.
TDataSet does not have a "neutral position". But as always you have few options:
Set dataset into insert / append record mode. So, the controls and code will see empty record. Be careful, as something may incidentally assign data to a field and then the new record may be posted to DB.
Depending on the data access components, you are using, you can set dataset to Cached Updates mode, insert and post new empty record into dataset, and mark all changes as applied. Then assign a filter, normally rejecting this empty record. Then in your code you have to switch the filter over, so it will reject all records, excluding this empty one.
Consider to disconnect the TDataSource from the dataset and later connect it again.
Note sure, but probably there may be invented some other approaches :)
Hallo,
use SetRange instead of FindKey.
tblSomeTable.SetRange([SomeSearchData],[SomeSearchData]);
try
while not tblSomeTable.Eof do begin
<do something with the Record>
tblSomeTable.Next;
end;
finally
tblSomeTable.CanelRange;
end;
when you criteria ensures that the maximum of matching records is one you retrieve with the statement above zero or one record.
This is not possible as far as I know. The cursor must always be on a record unless Bof and Eof are both true (empty data set).