A friend of mine asked me how he can at runtime to create a class to 'map' a database table. He is using ADO to connect to the database.
My answer was that he can fill an ADOQuery with a 'select first row from table_name', set the connection to database, open the query, and after that by using a cycle on the ADOQuery.Fields he can get FieldName and FieldType of all the fields from the table. In this way he can have all the fields from the table and their type as members of the class.
There are other solutions to his problem?
#RBA, one way is to define the properties of the class you want to map as "published", then use RTTI to cycle through properties and assign the dataset rows to each property.
Example:
TMyClass = class
private
FName: string;
FAge: Integer;
published
property Name: string read FName write FName;
property Age: Integer read FAge write FAge;
end;
Now, do a query:
myQuery.Sql.Text := 'select * from customers';
myQuery.Open;
while not myQuery.Eof do
begin
myInstance := TMyClass.create;
for I := 0 to myQuery.Fields.Count - 1 do
SetPropValue(myInstance, myQuery.Fields[I].FieldName, myQuery.Fields[I].Value);
// now add myInstance to a TObjectList, for example
myObjectList.Add(myInstance);
Next;
end;
This simple example only works if all fields returned by the query have an exact match in the class.
A more polished example (up to you) should first get a list of properties in the class, then check if the returned field exists in the class.
Hope this helps,
Leonardo.
Not a real class, but something quite similar. Sometime ago I blogged about a solution that might fit into your needs here. It uses an invokeable custom variant for the field mapping that lets you access the fields like properties of a class.
The Delphi Help can be found here and the two part blog post is here and here. The source code can be found in CodeCentral 25386
This is what is called ORM. That is, Object-relational mapping. You have several ORM frameworks available for Delphi. See for instance this SO question.
Of course, don't forget to look at our little mORMot for Delphi 6 up to XE2 - it is able to connect to any database using directly OleDB (without the ADO layer) or other providers. There is a lot of documentation available (more than 600 pages), including general design and architecture aspects.
For example, with mORMot, a database Baby Table is defined in Delphi code as:
/// some enumeration
// - will be written as 'Female' or 'Male' in our UI Grid
// - will be stored as its ordinal value, i.e. 0 for sFemale, 1 for sMale
TSex = (sFemale, sMale);
/// table used for the Babies queries
TSQLBaby = class(TSQLRecord)
private
fName: RawUTF8;
fAddress: RawUTF8;
fBirthDate: TDateTime;
fSex: TSex;
published
property Name: RawUTF8 read fName write fName;
property Address: RawUTF8 read fAddress write fAddress;
property BirthDate: TDateTime read fBirthDate write fBirthDate;
property Sex: TSex read fSex write fSex;
end;
By adding this TSQLBaby class to a TSQLModel instance, common for both Client and Server, the corresponding Baby table is created by the Framework in the database engine. Then the objects are available on both client and server side, via a RESTful link (over HTTP, using JSON for transmission). All SQL work ('CREATE TABLE ...') is done by the framework. Just code in Pascal, and all is done for you. Even the needed indexes will be created by the ORM. And you won't miss any ' or ; in your SQL query any more.
My advice is not to start writing your own ORM from scratch.
If you just want to map some DB tables with objects, you can do it easily. But the more time you'll spend on it, the more complex your solution will become, and you'll definitively reinvent the wheel! So for a small application, this is a good idea. For an application which may grow in the future, consider using an existing (and still maintained) ORM.
Code generation tools such as those used in O/RM solutions can build the classes for you (these are called many things, but I call them Models).
It's not entirely clear what you need (having read your comments as well), but you can use these tools to build whatever it is, not just models. You can build classes that contain lists of field / property associations, or database schema flags, such as "Field X <--> Primary Key Flag", etc.
There are some out there already, but if you want to build an entire O/RM yourself, you can (I did). But that is a much bigger question :) It generally involves adding the generation of code which knows how to query, insert, delete and update your models in the database (called CRUD methods). It's not hard to do, but then you take away your ability to integrate with Delphi's data controls and you'll have to work out a solution for that. Although you don't have to generate CRUD methods, the CRUD support is needed to fully eliminate the need for manual changes to adapt to database schema changes later on.
One of your comments indicated you want to do some schema querying without using the database connection. Is that right? I do this in my models by decorating them with attributes that I can query at runtime. This requires Delphi 2010 and its new RTTI. For example:
[TPrimaryKey]
[TField('EmployeeID', TFieldType.Integer)]
property EmployeeID: integer read GetEmployeeID write SetEmployeeID;
Using RTTI, I can take an instance of a model and ask which field represents the primary key by looking for the one that has the TPrimaryKeyAttribute attribute. Using the TField attribute above provides a link between the property and a database field where they do not have to have the same name. It could even provide a conversion class as a parameter, so that they need not have the same type. There are many possibilities.
I use MyGeneration and write my own templates for this. It's easy and opens up a whole world of possibilities for you, even outside of O/RM.
MyGeneration (free code generation tool)
http://www.mygenerationsoftware.com/
http://sourceforge.net/projects/mygeneration/
MyGeneration tutorial (my blog)
http://interactiveasp.net/blogs/spgilmore/archive/2009/12/03/getting-started-with-mygeneration-a-primer-and-tutorial.aspx
I've taken about 15 mins to write a MyGeneration script that does what you want it to. You'll have to define your Delphi types for the database you're using in the XML, but this script will do the rest. I haven't tested it, and it will probably want to expand it, but it will give you an idea of what you're up against.
<%# reference assembly = "System.Text"%><%
public class GeneratedTemplate : DotNetScriptTemplate
{
public GeneratedTemplate(ZeusContext context) : base(context) {}
private string Tab()
{
return Tab(1);
}
private string Tab(int tabCount)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int j = 0; j < 1; j++)
sb.Append(" "); // Two spaces
return sb.ToString();
}
//---------------------------------------------------
// Render() is where you want to write your logic
//---------------------------------------------------
public override void Render()
{
IDatabase db = MyMeta.Databases[0];
%>unit ModelsUnit;
interface
uses
SysUtils;
type
<%
foreach (ITable table in db.Tables)
{
%>
<%=Tab()%>T<%=table.Name%>Model = class(TObject)
<%=Tab()%>protected
<% foreach (IColumn col in table.Columns)
{
%><%=Tab()%><%=Tab()%>f<%=col.Name%>: <%=col.LanguageType%>;
<% }%>
<%=Tab()%>public
<% foreach (IColumn col in table.Columns)
{
%><%=Tab()%><%=Tab()%>property <%=col.Name%>: <%=col.LanguageType%> read f<%=col.Name%> write f<%=col.Name%>;
<% }%>
<%=Tab()%><%=Tab()%>
<%=Tab()%>end;<%
}
%>
implementation
end.
<%
}
}
%>
Here is one of the table classes that was generated by the script above:
TLOCATIONModel = class(TObject)
protected
fLOCATIONID: integer;
fCITY: string;
fPROVINCE: string;
public
property LOCATIONID: integer read fLOCATIONID write fLOCATIONID;
property CITY: string read fCITY write fCITY;
property PROVINCE: string read fPROVINCE write fPROVINCE;
end;
Depending on the database, you could query the INFORMATION_SCHEMA tables/views for what you need. I've done this in an architecture I created and still use in DB applications. When first connecting to a database it queries "data dictionary" type information and stores it for use by the application.
Related
I have the following structure in my RTDB (I'm using typescript interface notation to communicate the structure):
interface MyDB {
customers: {
[id: string]: {
firstName: string;
lastName: string;
};
};
projects: {
[id: string]: {
created: string;
customerId: string;
phase: string;
};
};
}
Given that I have two "tables" or document nodes, I'm not certain what the correct format for getting a project, as well as it's associated customer, should be.
I was thinking this:
db.ref('projects').once(projects => {
const customers = db.ref('customers').once(customers => {
const project = projects[SOME_PROJECT_ID];
const customer = customers[project.customerId];
// Proceed to do cool stuff with our customer and project...
});
});
Now, there are plenty of ways to express this. To be honest I did it this way in this example for simplicity, but I would actually not serialize the db.ref calls - I would put them in a combined observable and have them go out in parallel but that doesn't really matter because the inner code wouldn't change.
My question is -- is this how it is expected that one handle multi-document lookups that need to be joined in realtime database, or is there a "better" more "RTDB-y" way of doing it?
The issue I see here is the understanding I have is that we're selecting ALL projects and ALL customers. If I want to only get customers that have associated projects, is there a more efficient way to do that? I have seen that you might want to track project id's on each customer and do a filter there. But, I'm not sure the best way to track multiple project IDs (as a string with some kind of separater, or is there an array search function, etc?)
Thanks
Firebase Realtime Database doesn't offer any type of SQL-like join. If you have two locations to read, it requires two queries. How you do those two queries is entirely up to you. The database and its SDK is not opinionated about how you do that. If what you have works, then go with it.
I want to let the user to create multiple instances of the same form (let's call it Form1 which is a MDI child form). So I have two procedures like this where I create the forms.
procedure MyProcedure1; // procedure 2 is similar. it also has a var called MyFrm
var MyFrm: TFrm1;
begin
...
MyFrm:= TFrm1.create(MainForm);
MyFrm.BringToFront;
MyFrm.LoadFromFile(someFile);
end;
As you can see MyFrm is local var. This is ok for me as I don't need to programatically access the form after I create it. There is no other global variable named Frm1. In the OnClose event of MyFrm I have Action:= caFree;
What could cause the error above?
A user sent that error. It happened only once and I cannot reproduce it.
Edit:
The error appears in the "MyFrm:= TFrm1.create" line.
Some people suggested that I need to programatically give unique names to my dynamically created forms. I also wondered myself what name a form takes when it is created so I stepped into the code while calling the MyProcedure1 procedure.
Delphi automatically gives unique names like
MyFrm.name= MyFrm, then
MyFrm.name= MyFrm_1,
MyFrm.name= MyFrm_2,
MyFrm.name= MyFrm_3, and so on.
The MyFrm.Name is not altered in LoadFromFile. I have checked (breakpoint) the value of 'MyFrm.Name' at the end of procedure MyProcedure1; after LoadFromFile. The name is unique.
As some people suggested, I override the SetName procedure and checked the name of TMyFrm. Indeed each form gets a unique name.
procedure TMyFrm.SetName(const Value: TComponentName);
begin
ShowMessage(Value);
inherited;
end;
I have many forms in this app but only the MainForm is auto-created.
I don't use threads. Anyway this will not be relevant since the forms are created by user (so multi-threading is irrelevant unless the user can create 2 forms at the same time).
Giving MainForm as the Owner in TFrm1.Create will include the newly created form in the components list of MainForm. A component ensures that this list doesn't contain any two components with the same non-empty name (otherwise FindComponent won't work). This mechanism also works when a component changes its name.
As long as you don't specify the name in TFrm1.Create it is most likely that it is set by the LoadFromFile method, which means that you don't have much influence on the name unless you change the file's content.
A valid workaround is to create the form with nil as Owner, load the form from the file, change the name to a unique value or to an empty string and finally call MainForm.InsertComponent.
procedure MyProcedure1;
var MyFrm: TFrm1;
begin
...
MyFrm:= TFrm1.create(nil);
MyFrm.BringToFront;
MyFrm.LoadFromFile(someFile);
MyFrm.Name := ''; // or some unique name
MainForm.InsertComponent(MyFrm);
end;
The message is caused because each form must be uniquely named.
When you create a form twice, you need to ensure each instance has a unique name, or set the Name to an empty string. The latter also is the trick when using multiple instances of a data module, so that the automatic linking of data-aware controls does not end up always using the first instance.
Add
MyFrm.Name := MyFrm.Name + <something unique>;
MyFrm.Name := '';
after the Create call and you should be fine
MyFrm.Name is the same for both instances...
Make sure than MyFrm.Name is unique...
As far as my exploration along this line, yes the problem of "already exists" stems from having intances of the editor with the same value for the Name property. As another work around do not visually create the editor(s). Create a new component based on TForm/TFrame/TPanel for the editor(s) you want the user to be able to create multiple instances of. You will have to hand code the creation & deletion of any sub-controls, Setting their properties within your code and assigning values - anything from V_Btn = new TBitBtn(this), V_Btn->Color = clTeal, to V_Btn->OnClick = Close_The_Window. BUT NEVER assign a value to the Name property of any component in the new class and do not set the Name property of the editor once you have created an instance of the editor. Treat the Name property for editor as if it did not exist. After you have created the class and added it to your project the following is valid :
TMyeditor* Editor_01 = new TMyeditor(Main_Form);
TMyeditor* Editor_02 = new TMyeditor(Main_Form);
Editor_01->Parent = Tab_Sheet_Addresses;
Editor_02->Parent = Tab_Sheet_Billing;
The more complex the design concept for your editor the more effort you will undergoe to code the class. However this approach will resolve the "already exists" error.
End of answer.
The following is tangental to the original question as it is an extension of what you may want to further do with your code & I write it to help you along should it be the case. The following allows you to efficiently store/retrieve the editor(s) and its published properties such as position on the user's screen, themes, etc. If you've gone the above route, add the following :
void RegisterClassesWithStreamingSystem(void)
{
// Make sure that as part of the startup
// code TMyEditor is registered
// with the streaming system.
#pragma startup RegisterClassesWithStreamingSystem
Classes::RegisterClass(__classid(TMyEditor));
}
You can now ComponentToString <---> StringToComponent[*1] the editor(s).
You can now create a simple database of each editor saving it [*2] and re-creating the editor(s) at runtime. Saving & Recreating is almost entirely done by the TReader/TWriter objects.
{It is worth while to read about TReader/TWriter which is included in the Delphi help file}
[ Presupposing you have an instances of TMyEditor you want to save called Editor_01 & Editor_02 and
you've created the dataset and assigned it to a TClientDataSet named "CDS" ]
//How to write the Editors
String_Version_Of_Editor = ComponentToString(Editor_01);
CDS->Insert();
CDS->FieldByName("Data")->AsString = String_Version_Of_Editor;
CDS->Post();
String_Version_Of_Editor = ComponentToString(Editor_02);
CDS->Insert();
CDS->FieldByName("Data")->AsString = String_Version_Of_Editor;
CDS->Post();
//How to read, create an instance of, set the Owner of
//(allowing for automatic destruction/deletion
// if desired, Vis-à-vis Let the compiler/runtime package handle that),
//& setting the form's Parent
AnsiString String_Version_Of_Editor;
TWinControl* New_Editor;
String_Version_Of_Editor = CDS->FieldByName("Data")->AsString;
//The next line creates/constructs the new editor
New_Editor = StringToComponent(String_Version_Of_Editor);
//The next line sets the new editor's Owner to Main_Form
//It also assigns Main_Form the responsibility of object cleanup
Main_Form->Insert(New_Editor);
//The next line sets the Editor's Parent causing it to be part of the
//displayed user interface (it has been invisble since creation)
New_Editor->Parent = Tab_Sheet_Addresses;
//Move on to the next editor;
CDS->Next();
String_Version_Of_Editor = CDS->FieldByName("Data")->AsString;
New_Editor = StringToComponent(String_Version_Of_Editor);
Main_Form->Insert(New_Editor);
New_Editor->Parent = Tab_Sheet_Billing;
People who read the above who are astute will have noted that in the above code the New_Editor is of type TWincontrol not TMyEditor - though it likely should have been. However I did this to draw attention to the fact that problematically the TReader object in Delphi which is really doing the work of converting a string to a component object instance creates/constructs any object which has been registered with the streaming class via RegisterClass. In this manner explicit creation of the editor via explicitedly naming it's type is avoided. If thought is given to the design of TMyEditor and its descendents the only change required to the code is to change TWinControl* to TMyEditor* - even that is not required if published properties beyond TWinControl* are not accessed outside the scope of TMyEditor - Example TMyEditor has access to the variables whose values it is editing and does not require this information to be supplied to the editor.(If working from a DataModule, #include the datamodule's header into TMyEditor).
Side Note:
You may have a utility to know what class was read from the database so that you may place the instance where it belongs. To do this #include <typeinfo> into your code.
Example : If you have instances of TMyEditor, TMyEditor_Generation_01, TMyEditor_Generation_02, etc writen to the database the following will allow you to examine the instances read at runtime for placement into the user interface :
if (typeid(New).name() == "TMyEditor *")
New_Editor->Parent = Tab_Sheet_Addresses;
else
if (typeid(New).name() == "TMyEditor_Generation_01 *")
New_Editor->Parent = Tab_Sheet_Billing;
else
if (typeid(New).name() == "TMyEditor_Generation_02 *")
New_Editor->Parent = Tab_Sheet_Other_Editor;
typeid(__).name() will return a string which is the name of the class, in this case will also inculde " *".
The above allows ANY object(s) to be stored in the database and recreated. The entries within the database are not required to be related. The TReader object buried in Delphi's code will decide at runtime what they are and use the correct constructor.
[*1] Note : The ComponentToString and StringToComponent are examples in the delpi/c++ help file.
[*2] Note : What is being saved are the published properties, therefore in your editor class any values you want stored and retrieved which are not already inherited and published should be declared in the __published section of your new class. Those items may also be custom objects, for those you will likely code custom specific methods/functions for the read/write access specifiers in defining the _property. I would suggest translating any complex object into a string value for ease of examining your code while under development.
I'm building an app around Vici Coolstorage (asp.net version). I have my classes created and mapped to my database tables and can pull a list of all records fine.
I've written a stored procedure where the query jumps across databases that aren't mapped with Coolstorage, however, the fields in the query result map directly to one of my classes. The procedure takes 1 parameter.
so 2 questions here:
how do i execute the stored procedure? i'm doing this
CSParameterCollection collection = new CSParameterCollection();
collection.Add("#id", id);
var result = Vici.CoolStorage.CSDatabase.RunQuery("procedurename", collection);
and getting the exception "Incorrect syntax near 'procedurename'." (i'm guessing this is because it's trying to execute it as text rather than a procedure?)
and also, since the class representing my table is defined as abstract, how do i specify that result should create a list of MyTable objects instead of generic or dynamic or whatever objects? if i try
Vici.CoolStorage.CSDatabase.RunQuery<MyTable>(...)
the compiler yells at me for it being an abstract class.
There's a shortcut in CoolStorage to run a stored procedure. Simply prefix the stored procedure name with "!":
CSDatabase.RunQuery("!procedurename", collection);
I'm working on an application at the moment in ASP.NET MVC which has a number of look-up tables, all of the form
LookUp {
Id
Text
}
As you can see, this just maps the Id to a textual value. These are used for things such as Colours. I now have a number of these, currently 6 and probably soon to be more.
I'm trying to put together an API that can be used via AJAX to allow the user to add/list/remove values from these lookup tables, so for example I could have something like:
http://example.com/Attributes/Colours/[List/Add/Delete]
My current problem is that clearly, regardless of which lookup table I'm using, everything else happens exactly the same. So really there should be no repetition of code whatsoever.
I currently have a custom route which points to an 'AttributeController', which figures out the attribute/look-up table in question based upon the URL (ie http://example.com/Attributes/Colours/List would want the 'Colours' table). I pass the attribute (Colours - a string) and the operation (List/Add/Delete), as well as any other parameters required (say "Red" if I want to add red to the list) back to my repository where the actual work is performed.
Things start getting messy here, as at the moment I've resorted to doing a switch/case on the attribute string, which can then grab the Linq-to-Sql entity corresponding to the particular lookup table. I find this pretty dirty though as I find myself having to write the same operations on each of the look-up entities, ugh!
What I'd really like to do is have some sort of mapping, which I could simply pass in the attribute name and get out some form of generic lookup object, which I could perform the desired operations on without having to care about type.
Is there some way to do this to my Linq-To-Sql entities? I've tried making them implement a basic interface (IAttribute), which simply specifies the Id/Text properties, however doing things like this fails:
System.Data.Linq.Table<IAttribute> table = GetAttribute("Colours");
As I cannot convert System.Data.Linq.Table<Colour> to System.Data.Linq.Table<IAttribute>.
Is there a way to make these look-up tables 'generic'?
Apologies that this is a bit of a brain-dump. There's surely imformation missing here, so just let me know if you'd like any further details. Cheers!
You have 2 options.
Use Expression Trees to dynamically create your lambda expression
Use Dynamic LINQ as detailed on Scott Gu's blog
I've looked at both options and have successfully implemented Expression Trees as my preferred approach.
Here's an example function that i created: (NOT TESTED)
private static bool ValueExists<T>(String Value) where T : class
{
ParameterExpression pe = Expression.Parameter(typeof(T), "p");
Expression value = Expression.Equal(Expression.Property(pe, "ColumnName"), Expression.Constant(Value));
Expression<Func<T, bool>> predicate = Expression.Lambda<Func<T, bool>>(value, pe);
return MyDataContext.GetTable<T>().Where(predicate).Count() > 0;
}
Instead of using a switch statement, you can use a lookup dictionary. This is psuedocode-ish, but this is one way to get your table in question. You'll have to manually maintain the dictionary, but it should be much easier than a switch.
It looks like the DataContext.GetTable() method could be the answer to your problem. You can get a table if you know the type of the linq entity that you want to operate upon.
Dictionary<string, Type> lookupDict = new Dictionary<string, Type>
{
"Colour", typeof(MatchingLinqEntity)
...
}
Type entityType = lookupDict[AttributeFromRouteValue];
YourDataContext db = new YourDataContext();
var entityTable = db.GetTable(entityType);
var entity = entityTable.Single(x => x.Id == IdFromRouteValue);
// or whatever operations you need
db.SubmitChanges()
The Suteki Shop project has some very slick work in it. You could look into their implementation of IRepository<T> and IRepositoryResolver for a generic repository pattern. This really works well with an IoC container, but you could create them manually with reflection if the performance is acceptable. I'd use this route if you have or can add an IoC container to the project. You need to make sure your IoC container supports open generics if you go this route, but I'm pretty sure all the major players do.
I am currently testing with:
A SQLConnection which is pointed towards an IB database.
A SQLDataset that has a SQLConnection field set to the one above.
A DatasetProvider that has the SQLDataset in (2) as its Dataset field value.
A ClientDataset, with the ProviderName field pointing to the provider in (3).
I use the following method (borrowed from Alister Christie) to get the data...
function TForm1.GetCurrEmployee(const IEmployeeID: integer): OleVariant;
const
SQLSELEMP = 'SELECT E.* FROM EMPLOYEE E WHERE E.EMPLOYEEID = %s';
begin
MainDM.SQLDataset1.CommandText := Format(SQLSELEMP, [Edit1.Text]);
Result := MainDM.DataSetProvider1.Data;
end;
Which populates the DBGrid with just one record. However, when I manually edit the record, click on Post, then try to commit the changes, using
MainDM.ClientDataset1.ApplyUpdates(0); // <<<<<<
It bombs, with the message "SQLDataset1: Cannot modify a read-only dataset."
I have checked the ReadOnly property of the Provider, and of the ClientDataset, and the SQL has no joins.
What could be causing the error?
It appears that your ClientDataSet.Data property is being populated from the Data property of the DataSetProvider. With the setup you described, you should be able to simply call ClientDataSet.Open, which will get the data from the DataSetProvider.
BTW, the default behavior of the DataSetProvider when you call the ClientDataSet.ApplyUpdates method is to send a SQL query to the connection object, and not the DataSet from which the data was obtained (assuming a homogeneous query). Make sure that your DataSetProvider.ResolveToDataSet property is not set to true.
Finally, on an unrelated note, your code above appears to be open to a SQL injection attack (though I have not tested this). It is safer to use a parameter to define the WHERE clause. If someone enters the following into Edit1 you might be in trouble (assuming the InterBase uses the drop table syntax): 1;drop table employee;
Check the LiveMode property of the TIBDataSet.