Oracle Forms: only iterate through selected records - database

My Problem is the following:
I have a datablock based on a FROM_clause, querying relevant data from two tables. In the same datablock I have a checkbox to select rows, as a non-database item. When the user presses a button, it will insert certain information from those records into a table.
At the moment I'm looping through all the records, checking if the checkbox for this row is activated or not, depending on that doing the inserts. It all works fine, if the datablock only shows a few rows, or the user just selected some rows at the top of the list. (My loop iterates from FIRST_RECORD to LAST_RECORD, or until the amount of insert-operations is equal to the amount of selected rows).
But for most of the time, the datablock will show a few thousand records. If the user now selects some rows at the end of the list (like record #8000) my loop will (uselessly) iterate through thousands of records to insert just a few rows. It takes a lot of time and is just unnecessary.
I'm working with Oracle Form Builder 11g
How can I create a loop, that will only iterate though the records that are selected?
Any hints or code samples would be appreciated!

Generally if a block is likely to contain more than a few dozen records, I'll avoid looping over the records if at all possible. In the past I've solved this problem in two ways - pick which one suits you.
Store the checkbox values in a GTT, then query the GTT
Create a Global Temporary table, and base the checkbox on that GTT. When the user clicks the button, have the button trigger POST the values to the table; then, you can execute a SQL query against the GTT to find the selected values.
Store the selected rows in an array, then loop through the array
Add a trigger to the checkbox so that whenever it is checked or unchecked, a record with the relevant record number is added or removed from a PL/SQL array. You can then loop through this array which will be much faster than navigating through the block of records.

To help others who may be facing similar problems I want to post my solution.
I loosely based my implementation on this tutorial I found during my research.
I created a RecordGroup in the WHEN_NEW_FORM_INSTANCE Trigger and added all columns I needed to store:
declare
rg_name varchar2(40) := 'SELECTED';
rg_id recordgroup;
gc_id groupcolumn;
begin
/* Make sure the record group does not already exist. */
rg_id := find_group(rg_name);
/* If it does not exist, create it and add the
** necessary columns to it. */
if id_null(rg_id) then
rg_id := create_group(rg_name);
/* Add columns to the record group */
gc_id := add_group_column(rg_id, 'Barcode', number_column);
gc_id := add_group_column(..);
end if;
Then I changed my WHEN_CHECKBOX_CHANGED to add or remove the row from the RecordGroup depending on the value of the checkbox.
declare
row_no number;
rg_id recordgroup := find_group('SELECTED');
gc_id groupcolumn;
total_rows number;
barcode number;
begin
total_rows := get_group_row_count(rg_id);
if :block.checkbox = 1 then
/* Add selected row to the RecordGroup */
add_group_row(rg_id, end_of_group);
set_group_number_cell('SELECTED.BARCODE',
total_rows + 1,
:block.number_item);
else
/* Find selected row in RecordGroup and remove it */
for i in 1 .. total_rows loop
barcode := get_group_number_cell('SELECTED.BARCODE', i);
if :block.number_item = barcode then
row_no := i;
exit;
end if;
end loop;
delete_group_row('SELECTED', row_no);
end if;
end;
And in my WHEN_BUTTON_PRESSED Trigger it loops only through the selected rows, which are stored in the RecordGroup
declare
selected number;
row_no number;
begin
..
selected := get_group_row_count('SELECTED');
for j in 1 .. selected loop
begin
barcode := get_group_number_cell('SELECTED.BARCODE', j);
..
insert into (..);
commit;
exception
when others then
error_logging(..);
end;
end if;
delete_group_row('SELECTED', all_rows);
..
end;

Related

Prevent from duplicates on async insert on postgres

I have a trigger that basically get the last number inserted (folio) based on two columns (service_type and prefix) and then increment one value. Everything works fine but in some situations when there a lot of insert statements at the same time it causes that the function use the same last value inserted and it duplicates the folio column
CREATE OR REPLACE FUNCTION public.insert_folio()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE incremental INTEGER;
SELECT max(f.folio) INTO incremental FROM "folios" f
WHERE f.prefix = NEW."prefix" AND service_type = NEW."service_type";
NEW."folio" = incremental + 1;
IF NEW."folio" IS NULL THEN NEW."folio" = 1;
END IF;
RETURN NEW;
END;
$function$
CREATE TRIGGER insert_folio BEFORE INSERT ON "folios" FOR EACH ROW EXECUTE PROCEDURE insert_folio();
sample:
folio
service_type
prefix
1
DOCUMENT
DC
1
IMAGE
IMG
1
IMAGE
O
2
IMAGE
O
2 (This should be 3)
IMAGE
O
Any ideas?
Thanks!
It is because you have concurrent transactions that both see the same data. The second transaction does not see the record inserted by the first transaction until the first commits.
To prevent this behaviour you will have to lock the whole table when inserting to prevent concurrent writes or use advisory locks to prevent concurrent insertions of the same service_type and prefix.

PL/SQL - Declare a record where keep records of a table and an operation?

I'm currently trying to figure out a question on my assignment. I've never worked with arrays before but I've done collections, triggers, functions, procedures, and cursors. I'm not asking for the answer but rather some help on this as I'm confused on how to approach it.
Declare a record where you keep record of LOCATIONS table (LOCATION_ID,
STREET_ADDRESS, POSTAL_CODE, CITY, STATE_PROVINCE and
COUNTRY_ID) and an operation.
Declare an array where you keep location records as elements.
Initialize the array by populating values for each location that you would like
to process
The operation type can be ‘U’ for Update, ‘I’ for Insert and ‘D’ for Delete
Iterate all array elements from beginning to end and execute the following
logic per each location defined in the array:
If operation type is ‘U’, then update LOCATIONS table with the values
coming from the array. If location id is not found on the table, insert it
as a new record.
If operation type is ‘I’, insert the record to the table. Values should
come from the array
If operation type is ‘D’, delete that location from the table.
For each operation, display a message after the process is completed
If operator type is different than U, I, D, then display a proper
message indicating 'Invalid operation'.
Commit all transactions
The part about the operations also confuses me because I have done some work where you would use the operators in triggers but it didn't involve an array:
BEGIN
IF INSERTING THEN
INSERT INTO carlog
VALUES ('INSERT',user, SYSDATE, UPPER(:NEW.serial), UPPER(:NEW.make),
UPPER(:NEW.model), UPPER(:NEW.color));
END IF;
IF UPDATING THEN
INSERT INTO carlog
VALUES ('UPDATE',user, SYSDATE,:old.serial, old.make,
old.model, old.color);
END IF;
IF DELETING THEN
INSERT INTO carlog
VALUES ('DELETE',user, SYSDATE,:old.serial, old.make,
old.model, old.color);
END IF;
END;
An array is just a type of collection. If you look at the documentation here: https://docs.oracle.com/cd/B28359_01/appdev.111/b28370/collections.htm#LNPLS00501
You can see that an "Associative Array" is simply a PL/SQL collection type.
As far as the "Operation" is concerned, my understanding based on the spec is that it is purely an attribute of the location record itself. I would make something similar to the below:
DECLARE
--declare your location record based on the spec
TYPE location IS RECORD (
location_id integer,
operation VARCHAR2(1)
--additional values from the spec would go here
);
--declare an array as a table of the location type
TYPE location_array IS TABLE OF location INDEX BY BINARY_INTEGER;
--two variables to hold the data
example_location location;
locations location_array;
BEGIN
--start building your location record for the location you want to modify
example_location.location_id := 1;
example_location.operation := 'T';
--..... additional values here
--add the location to the array
locations(locations.count + 1) := example_location;
--repeat the above for any other locations, or populate these some other way
--loop through the locations
FOR i IN locations.first..locations.last
LOOP
--decide the logic based on the operation, could just as easily use your if logic here
CASE locations(i).operation
WHEN 'U' THEN
dbms_output.put_line('your update logic here');
WHEN 'I' THEN
dbms_output.put_line('your insert logic here');
WHEN 'D' THEN
dbms_output.put_line('your delete logic here');
ELSE
dbms_output.put_line('other operations');
END CASE;
END LOOP;
END;
You would need to adapt the above to suit your needs, adding in the relevant logic, messages, commits, error handling etc.

delphi DBGrid cell not empty

In my form, i have a TDBGRid, TDatasource , MessageTable and 2 button. I have a button to add new row in my DBGRID :
MessageTable.Append;
MessageTable.Edit;
MessageTable.FieldByName('FieldName').AsString := sName;
MessageTable.Post;
The second button is used to delete a current row :
MessageTable.Edit ;
MessageTable.Delete ;
How can ensure all Cell not empty before the post?
If there is an empty Cell, i need to ignore this row !
how can I do that?
You should use the features the dataset (in this case, TTable) already give you instead of trying to reinvent the wheel. TDataSet provides an event (OnBeforePost) that is specifically designed for this purpose.
Click on your TTable, and then switch to the Events tab in the Object Inspector. Find the OnBeforePost event and double click it to generate the event shell in the Code Editor. You'll see something like this:
procedure TForm1.Table1BeforePost(DataSet: TDataSet);
begin
// DataSet is the TDataSet (TTable, TQuery, TADOQuery, etc.) to which
// event is attached
end;
You can do all of your validations needed before a record is actually written to the database here. For instance, if you want to make sure every single field has something in it, you can do this:
procedure TForm1.Table1BeforePost(DataSet: TDataSet);
var
i: Integer;
begin
// You can replace DataSet with your actual table variable name, but using it
// this way allows you to use this same event for more than one table if you want.
for i := 0 to DataSet.FieldCount - 1 do
if DataSet.Fields[i].IsNull then
raise Exception.CreateFmt('Field %s has no value', DataSet.Fields[i].FieldName);
end;
If you want to make sure only certain fields have values, or that the value is within a certain range, you can access the field directly:
procedure TForm1.Table1BeforePost(DataSet: TDataSet);
begin
if DataSet.FieldByName('MyField').IsNull then
Abort; // This silently cancels the post without telling the user
if DataSet.FieldByName('AField').AsInteger = 0 then
raise Exception.Create('AField must not be 0');
end;
Now you don't have to do anything at all in your TDBGrid. If the user hits DownArrow on the last row and a new row is inserted, and they enter incomplete or wrong data, the events above will take care of it. They'll also work if you use two buttons (one to insert or edit and the other to post), because the events will handle everything else.
procedure TForm1.ButtonInsertClick(Sender: TObject);
begin
Table1.Insert; // Or Append - if you have an index on the table they're the same thing
end;
procedure TForm1.ButtonPostClick(Sender: TObject);
begin
Table1.Post; // This is 100% of the code you need here
end;

Trigger warning: created with compilation errors

i have this trigger and it says Warning: Trigger created with compilation errors.
can you give me quick advice?
/
CREATE OR REPLACE TRIGGER type_check
BEFORE INSERT ON carrs
FOR EACH ROW
BEGIN
IF :new.weight > 3500
THEN
:new.type := 'nakladne';
ELSE
:new.type := 'osobne';
END IF;
END;
/
edit: i still got the same warning
edit: here is table definition
create table carrs (
id_Car Integer not null,
id_board_unit Integer not null,
id_evc_numbers Integer not null,
id_owner Integer,
weight Integer not null );
here are the errors:
LINE/COL ERROR
-------- -----------------------------------------------------------------
4/5 PLS-00049: bad bind variable 'NEW.TYPE'
6/5 PLS-00049: bad bind variable 'NEW.TYPE'
If you're using SQL*Plus, type show errors after getting that warning to display the list of syntax errors that were reported. It's far easier to diagnose problems when you know what they are rather than guessing.
At a minimum, your references to the :new pseudorecord need to include the colon prefix :new rather than new. The assignment operator in PL/SQL is also := not =. So, at a minimum, you'd want something like
CREATE OR REPLACE TRIGGER type_check
BEFORE INSERT ON carrs
FOR EACH ROW
BEGIN
IF :new.weight > 3500
THEN
:new.type := 'nakladne';
ELSE
:new.type := 'osobne';
END IF;
END;
There may be additional errors as well. If there are, type show errors and edit your question to include them.
If you wish to add a new column named type to your table, you would do that before creating your trigger. That's not a particularly good name for a column, though, since type is also a reserved word in Oracle. I'd choose something more meaningful like, say, carr_type. You'd have to specify how large the strings the new column would need to hold when creating it. I'll guess that you want space for 10 characters
ALTER TABLE carrs
ADD( carr_type VARCHAR2(10 CHAR) );
You then probably want to UPDATE your table to populate the new column for your existing data
UPDATE carrs
SET carr_type = (case when weight > 3500
then 'nakladne'
else 'osobne'
end)
before creating your trigger
CREATE OR REPLACE TRIGGER type_check
BEFORE INSERT ON carrs
FOR EACH ROW
BEGIN
IF :new.weight > 3500
THEN
:new.carr_type := 'nakladne';
ELSE
:new.carr_type := 'osobne';
END IF;
END;

Why doesn't my query return any results when I add a WHERE clause?

My Delphi application is connected to SQLite successfully.
procedure TForm1.Button1Click(Sender: TObject);
begin
ZQuery1.Close;
ZQuery1.SQL.Clear;
ZQuery1.SQL.Text := 'SELECT Name FROM city;';
ZQuery1.Open;
while not ZQuery1.EOF do
begin
Memo1.Lines.Add(ZQuery1.FieldValues['name']);
ZQuery1.Next;
end;
end;
The above code works fine and loads contents of field name from table city.
However,
procedure TForm1.Button1Click(Sender: TObject);
begin
ZQuery1.Close;
ZQuery1.SQL.Clear;
ZQuery1.SQL.Text := 'Select name from city WHERE district = :aField';
ZQuery1.Params.ParamByName('aField').Value := 'kabol';
ZQuery1.Open;
while not ZQuery1.EOF do
begin
Memo1.Lines.Add(ZQuery1.FieldValues['name']);
ZQuery1.Next;
end;
end;
Surprisingly, when I add a where clause, the query returns nothing! Could anyone suggest what is wrong in my code?
Here is an image of the data in my table:
You probably don't have any data that has a district of kabol. The addition of the WHERE clause would then result in no rows being returned, meaning that ZQuery1.Eof is immediately true, and your while not ZQuery1.Eof do loop never gets entered.
You can check this by changing your first query (the one that works) to something like this:
ZQuery1.SQL.Text := 'SELECT Name, District FROM City';
Then change the output to
Memo1.Lines.Add(ZQuery1.FieldValues['name'] + #9 +
ZQuery1.FieldValues['district']);
If you don't see at least one line in the memo that contains kabol in the rightmost column, you don't have any rows that match your WHERE criteria. (Note that most databases are case-sensitive, so kabol is not equal to Kabol; the first would match your WHERE, but the second would not.)
Your screenshot shows one database row where district is 'Kabol' (uppercase K), but your SQL query is looking for 'kabol' (lowercase k) instead. Assuming the query is comparing strins case-sensitively, that would explain why no row is found. So either fix the case in your query input, or else perform a case-insensitive query instead.

Resources