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;
Related
i'm learning to use firemonkey, and i made an app that i'm using on my phone (it works like a reminder if u want) so basically there's a database file (SQLite) in my phone's internal storage, and in the form i put a TMemo + FDConnection + FDPhysSQLiteDriverLink so the app can read the database and display it on the TMemo, now i did put a TEdit and i added a button i want what i write on the TEdit to be added to the database.
i'm new to SQL delphi and stack overflow in general.
thanks
PS: i'm trying to do it without using livebindings.
var
query: TFDQuery;
begin
query := TFDQuery.Create(nil);
try
// Define the SQL Query
query.Connection := FDConnection1;
query.SQL.Text := 'SELECT * FROM Employee';
query.Open();
outputMemo.Text := '';
// Add the field names from the table.
outputMemo.Lines.Add(String.Format('|%8s|%-25s|%-25s|', [' ID ', ' NAME >',
' DEPARTMENT ']));
// Add one line to the memo for each record in the table.
while not query.Eof do
begin
outputMemo.Lines.Add(String.Format('|%8d|%-25s|%-25s|',
[query.FieldByName('ID').AsInteger, >query.FieldByName('Name').AsString,
query.FieldByName('Department').AsString]));
query.Next;
end;
finally
query.Close;
query.DisposeOf;
end;
end;
this is the code to display the database, i want to know if there's a way to write on the memo or a TEdit and press a button to change back the database, i'm wondering if it works both ways.
I'm stumped on something which should be very straight-forward. I have a SQL Server database, and I'm trying to update a non-nullable varchar or nvarchar field with an empty string. I know it's possible, because an empty string '' is not the same thing as NULL. However, using the TADOQuery, it is not allowing me to do this.
I'm trying to update an existing record like so:
ADOQuery1.Edit;
ADOQuery1['NonNullFieldName']:= '';
//or
ADOQuery1.FieldByName('NonNullFieldName').AsString:= '';
ADOQuery1.Post; //<-- Exception raised while posting
If there is anything in the string, even just a single space, it saves just fine, as expected. But, if it is an empty string, it fails:
Non-nullable column cannot be updated to Null.
But it's not null. It's an empty string, which should work just fine. I swear I've passed empty strings many, many times in the past.
Why am I getting this error, and what should I do to resolve it?
Additional details:
Database: Microsoft SQL Server 2014 Express
Language: Delphi 10 Seattle Update 1
Database drivers: SQLOLEDB.1
Field being updated: nvarchar(MAX) NOT NULL
I can reproduce your reported problem using the code below with SS2014, the OLEDB driver and
Seattle and the difference in behaviour when the table has been created with MAX as the column size and a specific number (4096 in my case). I thought I would post this is as an alternative
answer because it not only shows how to investigate this difference systematically
but also identifies why this difference arises (and hence how to avoid it in future).
Please refer to and execute the code below, as written, i.e. with the UseMAX define
active.
Turning on "Use Debug DCUs" in the the project options before executing the code, immediately
reveals that the described exception occurs in Data.Win.ADODB at line 4920
Recordset.Fields[TField(FModifiedFields[I]).FieldNo-1].Value := Data
of TCustomADODataSet.InternalPost and the Debug evaluation window reveals that
Data at this point is Null.
Next, notice that
update jdtest set NonNullFieldName = ''
executes in an SSMS2014 Query window without complaint (Command(s) completed successfully.), so it seems that the
fact that Data is Null at line 4920 is what is causing the problem and the next question is "Why?"
Well, the first thing to notice is that the form's caption is displaying ftMemo
Next, comment out the UseMAX define, recompile and execute. Result: No exception
snd notice that the form's caption is now displaying ftString.
And that's the reason: Using a specific number for the column size means that
the table metadata retrieved by the RTL causes the client-side Field to be created
as a TStringField, whose value you can set by a string assignment statement.
OTOH, when you specify MAX, the resulting client-side Field is of type ftMemo,
which is one of Delphi's BLOB types and when you assign
string values to an ftMemo field, you are at the mercy of code in Data.DB.Pas , which does all the reading (and writing) to the record buffer using a TBlobStream. The problem with that is that as far as I can see, after a lot of experiments and tracing through the code, the way a TMemoField uses a BlobStream fails to properly distinguish between updating the field contents to '' and setting the field's value to Null (as in System.Variants).
In short, whenever you try to set a TMemoField's value to an empty string, what actually happens is that the field's state is set to Null, and this is what causes the exception in the q. AFAICS, this is unavoidable, so no work-around is obvious, to me at any rate.
I have not investigated whether the choice between ftMemo and ftString is made by the Delphi RTL code or the MDAC(Ado) layer it sits upon: I would expect it is actually determined by the RecordSet TAdoQuery uses.
QED. Notice that this systematic approach to debugging has revealed the
problem & cause with very little effort and zero trial and error, which was
what I was trying to suggest in my comments on the q.
Another point is that this problem could be tracked down entirely without
resorting to server-side tools including the SMSS profiler. There wasn't any need to use the profiler to inspect what the client was sending to the server
because there was no reason to suppose that the error returned by the server
was incorrect. That confirms what I said about starting investigation at the client side.
Also, using a table created on the fly using IfDefed Sql enabled the problem effectively to be isolated in a single step by simple observation of two runs of the app.
Code
uses [...] TypInfo;
[...]
implementation[...]
const
// The following consts are to create the table and insert a single row
//
// The difference between them is that scSqlSetUp1 specifies
// the size of the NonNullFieldName to 'MAX' whereas scSqlSetUp2 specifies a size of 4096
scSqlSetUp1 =
'CREATE TABLE [dbo].[JDTest]('#13#10
+ ' [ID] [int] NOT NULL primary key,'#13#10
+ ' [NonNullFieldName] VarChar(MAX) NOT NULL'#13#10
+ ') ON [PRIMARY]'#13#10
+ ';'#13#10
+ 'Insert JDTest (ID, [NonNullFieldName]) values (1, ''a'')'#13#10
+ ';'#13#10
+ 'SET ANSI_PADDING OFF'#13#10
+ ';';
scSqlSetUp2 =
'CREATE TABLE [dbo].[JDTest]('#13#10
+ ' [ID] [int] NOT NULL primary key,'#13#10
+ ' [NonNullFieldName] VarChar(4096) NOT NULL'#13#10
+ ') ON [PRIMARY]'#13#10
+ ';'#13#10
+ 'Insert JDTest (ID, [NonNullFieldName]) values (1, ''a'')'#13#10
+ ';'#13#10
+ 'SET ANSI_PADDING OFF'#13#10
+ ';';
scSqlDropTable = 'drop table [dbo].[jdtest]';
procedure TForm1.Test1;
var
AField : TField;
S : String;
begin
// Following creates the table. The define determines the size of the NonNullFieldName
{$define UseMAX}
{$ifdef UseMAX}
S := scSqlSetUp1;
{$else}
S := scSqlSetUp2;
{$endif}
ADOConnection1.Execute(S);
try
ADOQuery1.Open;
try
ADOQuery1.Edit;
// Get explicit reference to the NonNullFieldName
// field to make working with it and investigating it easier
AField := ADOQuery1.FieldByName('NonNullFieldName');
// The following, which requires the `TypInfo` unit in the `USES` list is to find out which exact type
// AField is. Answer: ftMemo, or ftString, depending on UseMAX.
// Of course, we could get this info by inspection in the IDE
// by creating persistent fields
S := GetEnumName(TypeInfo(TFieldType), Ord(AField.DataType));
Caption := S; // Displays `ftMemo` or `ftString`, of course
AField.AsString:= '';
ADOQuery1.Post; //<-- Exception raised while posting
finally
ADOQuery1.Close;
end;
finally
// Tidy up
ADOConnection1.Execute(scSqlDropTable);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Test1;
end;
The problem occurs when using MAX in the data type. Both varchar(MAX) and nvarchar(MAX) exploit this behavior. When removing MAX and replacing it with a large number, such as 5000, then it allows empty strings.
Okay so basically I've been working on my computing project for a while now and I've got 90% of it working however I'm having a problem with Delphi where is says that my database is not connected/ there is a problem connecting however I've already tried writing the information to the screen and this showed me that the items I was looking to pick up where in fact being picked up so the failure is when the items are being input in to the database. This however shouldn't be happening as the System already has database information displayed from that table and the user can physically select things from the database tables within the program however when trying to store the information back into the database it just breaks. Me and my computing teacher can not work it out, any help would be appreciated.
The problem appears on the new orders page. If you'd rather look at the system then you can download it from here https://drive.google.com/folderview?id=0B_iRfwwM9QpHVXJnSkx4U1FjMlk&usp=sharing
procedure Tform1.btnSaveClick(Sender: TObject);
var orderID:integer;
count:integer;
begin
try
//save into the order table first
tblOrder.Open;
tblOrder.Insert;
tblOrder.FieldByName('CustomerID').value:= strtoint(cboCustomer.Text);
tblOrder.Close;
tblOrder.Open;
tblOrder.Last;
orderID:=tblOrder.FieldByName('OrderID').Value;
showmessage(inttostr(orderID));
for count := 1 to nextFree-1 do
begin
if itemOrdered[count,1]<>0 then
begin
tblOrderLine.Open;
tblOrderLine.AppendRecord([orderID, itemOrdered[count,1],itemOrdered[count,2]]);
end;
end;
showmessage('The order has been saved');
except
showmessage('There was a problem connecting to the database');
end;
end;
You're doing far too much open, do something, close, open. Don't do that, because it's almost certain that is the cause of your problem. If the data is already being displayed, the database is open already. If you want it to keep being displayed, the database has to remain open.
I also removed your try..except. You can put it back in if you'd like; I personally like to allow the exception to occur so that I can find out why the database operation failed from the exception message, rather than hide it and have no clue what caused it not to work.
procedure Tform1.btnSaveClick(Sender: TObject);
var
orderID: integer;
count: integer;
begin
//save into the order table first
tblOrder.Insert;
tblOrder.FieldByName('CustomerID').value:= strtoint(cboCustomer.Text);
tblOrder.Post;
orderID:=tblOrder.FieldByName('OrderID').Value;
showmessage(inttostr(orderID));
for count := 1 to nextFree-1 do
begin
if itemOrdered[count, 1] <> 0 then
begin
tblOrderLine.AppendRecord([orderID, itemOrdered[count,1],itemOrdered[count,2]]);
tblOrderLine.Post;
end;
end;
showmessage('The order has been saved');
end;
For Delphi ClientDataSets where fields have been define at design time, is there a way at runtime to change a specific field's datatype ( change the cds.Fields[n].DataType) ?
I have a legacy Delphi 7 program with both SQLDataSet and ClientDataSet fields set at design time (in order to override various properties).
These are connected to a 3rd-party Sybase SQL Anywhere 11 database.
Recently the vendor changed all 'Description' fields from VarChar(128) to long varchar, but only for certain of his customers. So, my code has to support both types of fields when I query on these 'Description' fields.
I was hoping to set conditional compilation on the class field types (then add the fields before opening the SQL/CLient Dataset), but the compiler ignores {$IFDEF } conditionals in the component definition section of the class (which, the more I think about it, makes good sense)!
There are dozens of modules, with hundreds of fields affected, so any insight is appreciated.
I have also faced this problem before, not with CDS, but with TADODataSet using persistent fields at design time. I think that the code below will help you get the idea of how to fix/patch your CDS datasets.
The idea is to query the relevant table schema; get the actual fileds data type; and "change" the persistent field type by un-attaching it from the DataSet and adding a new matching persistent filed instead:
// TData class
procedure TData.DataModuleCreate(Sender: TObject);
var
I: Integer;
begin
for I := 0 to ComponentCount - 1 do
if (Components[I] is TCustomADODataSet) then
DataSetPrepareMemoFields(TDataSet(Components[I]));
end;
procedure TData.DataSetPrepareMemoFields(DataSet: TDataSet);
var
Fld: TField;
I: Integer;
FldName, CompName: string;
AOwner: TComponent;
begin
// Here you need to query the actual table schema from the database
// e.g. ADOConnection.GetFieldNames and act accordingly
// check which DataSet you need to change
// if (DataSet = dsOrders) or ... then...
if DataSet.FieldList.Count > 0 then
for I := DataSet.FieldList.Count - 1 downto 0 do
begin
if DataSet.FieldList.Fields[I].ClassNameIs('TMemoField') and (DataSet.FieldList.Fields[I].FieldKind = fkData) then
begin
// save TMemoField properties
AOwner := DataSet.FieldList[I].Owner;
CompName := DataSet.FieldList[I].Name;
FldName := DataSet.FieldList.Fields[I].FieldName;
// dispose of TMemoField
DataSet.FieldList[I].DataSet := nil; // Un-Attach it from the DataSet
// create TWideADOMemoField instead
Fld := TWideADOMemoField.Create(AOwner); // Create new persistent Filed instead
Fld.Name := CompName + '_W';
Fld.FieldName := FldName;
Fld.DataSet := DataSet;
end;
end;
end;
That said, After I have fixed that issue, I have never ever used persistent fields again.
All my fields are generated in run time. including calculated/lookup/internal fields.
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;