In the previous post we got our object loading and saving to the DB, and were able to query and get the results either in a TList<TCustomer> or in a dataset. That’s not bad with such little effort.
However, unless our business domain is fully represented by a single class, we need to look at how you deal with multiple classes with relationships between them. So let’s do that.
Note, like last time I’ve done a video version of this post, if you prefer. However, there is a part at the very end of this post, about getting a reference to the currently selected TCustomer from the dataset, that I didn’t cover in the video.
We’re going to create a second business class called TContact, and set it up with the same attributes as TCustomer, so that Aurelius knows that it is a) responsible for managing it and b) that we want to use AutoMapping. It should look like this:
[Entity] [Automapping] TContact = class private FFirstname: string; FLastname: string; FDOB: TDate; FId: Integer; public property Id: Integer read FId write FId; property Firstname: string read FFirstname write FFirstname; property Lastname: string read FLastname write FLastname; property DOB: TDate read FDOB write FDOB; end;
This class is going to be in a 1-to-many relationship with TCustomer, ie. one TCustomer can have many TContact objects, so let’s add a property called Contacts to our TCustomer class, which will be of type TList<TContact>. It should look like this:
[Entity] [Automapping] TCustomer = class private FName: string; FId: Integer; FACN: string; FContacts: TList<TContact>; public property Id: Integer read FId write FId; property Name: string read FName write FName; property ACN : string read FACN write FACN; property Contacts : TList<TContact> read FContacts; constructor Create; destructor Destroy; override; end;
We’ve also added a constructor and destructor to create and free our FContacts TList<TContact>. A few things are worth calling out here:
- Apart from the attributes, there’s nothing particularly specific to Aurelius here. Our TContact and TCustomer class are pretty standard Delphi classes.
- We are not using a TObjectList<TContact>, but instead a TList<TContact>. That might prompt you to ask “Who is freeing the TContact objects inside the TList<TContact>?“. We’re freeing the TList<TContact> itself, but not the TContact objects inside it. Remember in the last post I said that Aurelius manages the lifetime of the objects, so most of the time you don’t have to worry about it? Well, the same applies here. Aurelius will instantiate and free the TContact objects in the list, we just need to worry about the TList<TContact> itself.
Now our object model is a little bit more complex than it was before. Let’s update the code we had for creating some sample object instances. In the OnClick event attached to your Create Objects bottom, update the code to look like the following:
procedure TForm1.Button3Click(Sender: TObject); var LCustomer : TCustomer; LContact : TContact; begin LCustomer := TCustomer.Create; LCustomer.Name := 'Acme Inc'; LContact := TContact.Create; LContact.Firstname := 'Wiley'; LContact.Lastname := 'Coyote'; LContact.DOB := EncodeDate(1960, 1, 1); LCustomer.Contacts.Add(LContact); ObjMgr.Save(LCustomer); end;
Note the highlighted lines I’ve added. I’m creating an instance of TContact, setting its property values and then adding it to the TCustomer’s Contacts list. We still have our existing TCustomer objects in the database, but that’s ok, they’ll just end up with no TContact objects, however I’ve changed the name of this one to Acme Inc just so I can distinguish it from the Acme Pty. Ltd. we created in the last post.
Again, there’s nothing specific to Aurelius here, save the last line where we’re calling Save. Everything else is probably how you’d do it with no persistence.
However, if we tried to run this now, we’d have a problem. Our database only has a Customer table in it, it doesn’t have a Contact table, so it won’t be able to save this object model.
No problem. Aurelius makes this very easy. Just as it was able to inspect our object model and create the necessary tables to persist it initially, it can also compare an object model to some existing tables and figure out what it needs to change to enable persistence. The simplest way to do this is to call UpdateDatabase on our DBManager. This will do the comparison and figure out the changes needed, then go ahead and apply them to the database. If you just want to figure out what changes are needed, without actually making the changes, call VerifyDatabase instead.
So we add another button to our form, set the caption to Update Database, and in the OnClick event put the following code:
procedure TForm1.Button2Click(Sender: TObject); begin dmMain.DBMgr.UpdateDatabase; end;
We’ll need to give Aurelius a little help to understand our schema, by including the relevant unit for the database we’re using. In our case, it’s Interbase, so add the Aurelius.Schema.Interbase unit to your DataModule uses clause. If you forget (as I did in the video), you’ll get a helpful error message.
Now, lets update our UI to interact with this expanded object model. First, lets update our code that retrieved our object based on its ID.
Add a TLabel to your form, and line it up with the one you added earlier. We’re going to display the number of TContact objects that our TCustomer has in this label. Update the code in the OnClick event for the Get By ID button as marked below.
procedure TForm1.Button4Click(Sender: TObject); var LCustomer : TCustomer; begin LCustomer := ObjMgr.Find<TCustomer>(Edit1.Text); if Assigned(LCustomer) then begin Label1.Caption := LCustomer.Name; Label2.Caption := IntToStr(LCustomer.Contacts.Count); end else begin Label1.Caption := 'Not Found'; Label2.Caption := ''; end; end;
That’s pretty straight forward, but what about the dataset stuff?
We currently have a TDBGrid displaying the results of a search. Let’s extend it with a second DBGrid which will display the Contacts belonging to the currently selected Customer. The key thing to remember here is that the Contacts exist as a TList<TContact> inside the TCustomer, so we don’t need to do another query. We just need to somehow connect another Grid up to the Contacts property of the TCustomer.
Right click on AureliusDataset1 (the one that we’re loading with the Criteria from our query) and bring up the Fields Editor. At the moment it only has a persistent field for Name, so let’s add another one for Contacts. Right click in the Fields Editor, select New Field and give it a name of Contacts and a Type of Dataset.
We now have a TDatasetField pointing at our Contacts property for the current TCustomer. Let’s drop down another TDBGrid, TDBDataSource and TAureliusDataset, and connect them together like so:
DBGrid2.DataSource -> DataSource2
DataSource2.DataSet -> AureliusDataset2
AureliusDataset2.DatasetField -> AureliusDataset1Contacts
Note in the last line above, we’re pointing the DatasetField property of AureliusDataset2 to the TDatasetField we just created.
That’s it. Let’s run our app and walk through some of the changes.
First, click the Update Database button. Aurelius will compare the current structure of the database to the structure it needs to persist our business objects, and make any necessary changes. If you refresh the Data Explorer now you should see an additional Contacts table, along with a Foreign Key relationship to the record in the Customer table.
Now click the Create Customers button to create our new Acme Inc customer with a single Contact. If you view the record in the Data Explorer now you should see an entry in the Contacts table, with a link back to the correct Customer record.
Next do a search on some Customer ID’s, trying our original ones without the Contacts and also our new Acme Inc with a single Contact. You should see the correct number of Contacts displayed in our new label.
Last, search by name to find Acme Inc, and select it in the Customer Grid and you should see Wiley Coyote show up in the Contact grid.
We now have two classes in our object model, connected in a one-to-many relationship, and we can load them, save them and display them on the form using the same data aware controls we’re already familiar with.
However, often the question comes up at this point: “How can I get a reference to the currently selected TCustomer from the AureliusDataset?“. Well, I’m glad you asked.
Close the app and add another button to the form (next to the Search button). Set its Caption to Current Name and in its OnClick event put the following code:
procedure TForm1.Button6Click(Sender: TObject); var LCurrent : TCustomer; begin LCurrent := AureliusDataset1.Current<TCustomer>; if Assigned(LCurrent) then ShowMessage(LCurrent.Name); end;
You can see we’re asking AureliusDataset1 for the Current<TCustomer>. Once we check if the result is assigned, we can start referencing it as a TCustomer, calling methods, changing properties, whatever we want.
Now we have a basic app up and running with Aurelius, I want to start diving into how we can exercise more control over how our objects are persisted. But that will be in the next post.
Remember, Code Partners has an exclusive offer on TMS Aurelius for readers of these posts. Click here to find out more.