Mapping to an Existing Database
Download C# sources for this article
Purpose of this Article
This article demonstrates how to use eXpress Persistent Objects for .NET (XPO) in a project that already uses a relational database management system (RDBMS) such as MS Access or SQL Server and standard datasets.
Contents
Some Background on the Impedance Mismatch Problem
An application can be described as a system that manipulates data and is able to store the state of that data somewhere between invocations. It can also be said that the data that is manipulated almost always represents information about real world things like people, employees, and library books. With the prevalence of RDBMSs like Oracle, SQL Server and DB/2 it is clear that the dominant way to store this information is in the uniform tables of an RDBMS. This is clearly efficient for data retrieval and storage – the most important two tasks of a persistence layer. Internally inside an application the only way to deal with such data has traditionally been to use synthetic structures such as arrays to represent that data for manipulation and to bend the remainder of the application around this model. The problem is that not all Library books, or people, or employees have the same uniform interesting pieces of information and so a better way to represent data about things was invented and named OOP – Object Oriented Programming. Objects allow a representation of a thing to not only have information - in many more flexible ways than uniform arrays - but they also carry with them the unique functionality for dealing with that data – allowing Objects to more closely map to the real world things that they abstract. Business Objects are just regular objects where the functionality represents Business rules. With the prevalence of Object Oriented and Object Based languages like C++, C#, Object Pascal and Visual Basic it is clear that Objects are a dominant way of representing data in an application. The problem with a world made up of dominant RDBMSs and dominant OOP Languages is that it has not been easy to map the information in one to the other. Back in the early 90s when OOP languages started to appear, this problem was termed the impedance mismatch (a term borrowed from electrical engineering) and was solved by either making objects for dealing with uniform data (Records) or making completely new types of Storage mechanism called Object Oriented database management systems (OODBMS). Impedance mismatch is the problem that eXpress Persistent Objects was designed to solve, allowing a developer to work directly with Business Objects inside an application and yet still have the data persisted in a conventional uniform RDBMS. Of course, the mention of business objects invariably leads to a question - often framed as: Must the whole application be converted to Business Objects for XPO to be used? The unfortunate reality is that most programmers are not building new systems but maintaining existing systems, and if you are maintaining a database driven system that was built using .NET, then there is a good chance that it is ADO.NET based and uses datasets. As you well know, the problem in this instance is that existing code is most likely working fine and it is unlikely that a developer can justify moving to a totally new architecture - something as fundamentally different as a persistence layer just to accommodate new development. Since this issue is foremost in the minds of those who are considering moving to business objects as the basis of future development, we spent significant energy on addressing it. With XPO, you don’t have to change everything that has already been built just to start using it for new things… You can have both architectures co-exist and be able to pick and choose those parts of your application you wish to migrate over time. The complexity of the migration process depends on functionalities of XPO that you want to implement in your application. Straightforward capabilities such as reading, update, saving and searching data do not require database structure modifications. In this case, all you have to do is to define object model for the persistent classes. Although, additional functionalities introduced by XPO such as optimistic concurrency, persistent objects class inheritance, deferred record deletion and support for simple many-to-many relationships do require changes to the database structure by adding auxiliary database structure elements. In such an instance, you will have to take additional steps to implement these features, explanations of which are beyond the scope of this article. We will examine several data models in the ascending order of model complexity. For now we will concentrate on the One-to-Many data model which is probably the most common.
The One-to-Many Data Model
This is commonly referred to in the RDBMS world as foreign key relationship between tables or in the world of client/server development a master-detail relationship. Let’s assume we have table of customers and each customer has multiple orders.
Customer table: ID - key field Name - Customer’s name
Order table: ID - key field Description - information related to order CustomerID - reference field which provides a link to Customer the Order belongs to.
You need to write one persistent class for each table. In this example we assume that you are using auto-increment key fields to enforce unique row contents. The class structure can be implemented as follows:
|
[OptimisticLocking(false)] public class Customer: XPBaseObject { public Customer(Session session) : base(session) {} public Customer() : base() {} [Key(AutoGenerate = true)] public int ID; [Persistent("FullName")] public string Name; [Aggregated, Association("Customer-Orders", typeof(Order))] public XPCollection Orders { get { return GetCollection("Orders"); } } }
[OptimisticLocking(false)] public class Order: XPBaseObject { public Order(Session session) : base(session) {} public Order() : base() {} [Key(AutoGenerate = true)] public int ID; public string Description; [Association("Customer-Orders")] public Customer Owner; }
<OptimisticLocking(False)> _ Public Class Customer : Inherits XPBaseObject Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New() MyBase.new() End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer <Persistent("FullName")> _ Public Name As String <Aggregated(), Association("Customer-Orders", GetType(Order))> _ Public ReadOnly Property Orders() As XPCollection Get Return GetCollection("Orders") End Get End Property End Class
<OptimisticLocking(False)> _ Public Class Order : Inherits XPBaseObject Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New() MyBase.new() End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer Public Description As String <Association("Customer-Orders")> _ Public Owner As Customer End Class
|
By default, XPO stores the object in the database table with the same name as the class, so Customer objects will be put in the Customer table. You can easily override this behavior by using the “MapTo” attribute. In the example below, the Customer objects could be stored in the Customers table, if necessary:
|
[MapTo("Customers")] public class Customer: XPBaseObject { ... }
<MapTo("Customers")> _ Public Class Customer : Inherits XPBaseObject ... End Class
|
By default, XPO will store data in fields with the same name as the Class property. To specify a custom database field name for a class property, use the “Persistent” attribute. In the example below, the Name property will be stored in the FullName field of the Customer Table.
|
public class Customer: XPBaseObject { [Persistent("FullName")] public string Name; ... }
Public Class Customer : Inherits XPBaseObject <Persistent("FullName")> _ Public Name As String ... End Class
|
To enable cascading deletes of the dependent objects (When a customer is deleted all the related orders for that customer are also deleted), you can specify the “Aggregated” attribute for the corresponding properties (see the Orders collection in the definition of the Customer class above). That is all you need to do. Now you have XPO business objects that map to the underlying data model represented in your RDBMSes tables.
Simple Many-to-Many Data Model
In order to demonstrate a Many-to-Many relationship let us consider a Company-Contacts data model where we are keeping information on say PR Contacts for companies. Now some companies in our list can have one or more internal PR people, and other companies might actually delegate that job to PR agencies that could well represent many of the companies in our list. So each PRContact can be working with many Companies, and each company can have many PRContacts – to model this we might have a Companies Table, a Contacts table, and a third table called PRLinks. The latter is used as an intermediate table in the database. It has one record for each Company-Contacts relationship that contains IDs of corresponding contact and company.
Companies table: ID – key field Name – Company name
Contacts table: ID – key field FullName – Contact’s full name
PRLinks table: ID – key field CompanyID – link to a Company by its key ContactID – link to a Contact by its key
As in the previous example, this one uses auto-increment key fields and custom identification handling but we can still inherit the persistent classes from the XPBaseObject. XPO handles Many-to-Many relationships automatically when it generates its own database schema. But in the case of an existing database, you will need to explicitly define a class for the intermediate link table, and add methods to manipulate this intermediate class. In this example, we added the AddCompany method at the Contact end and the AddPRContact method at the Company end to answer a need.
|
[MapTo("Companies"), OptimisticLocking(false)] public class Company: XPBaseObject { public Company(Session session) : base(session) {} public Company() : base() {} [Key(AutoGenerate = true)] public int ID; [Association("CompanyLink", typeof(PRContactFor)), Aggregated] public XPCollection PRContacts { get { return GetCollection("PRContacts"); } } public string Name; public void AddPRContact(Contact contact) { PRContactFor prContactFor = new PRContactFor(); prContactFor.Company = this; prContactFor.Contact = contact; prContactFor.Contact.Companies.Add(prContactFor); PRContacts.Add(prContactFor); } }
[MapTo("Contacts"), OptimisticLocking(false)] public class Contact: XPBaseObject { public Contact(Session session) : base(session) {} public Contact() : base() {} [Key(AutoGenerate = true)] public int ID; [Association("PRContactLink", typeof(PRContactFor)), Aggregated] public XPCollection Companies { get { return GetCollection("Companies"); } } public string FullName; public void AddCompany(Company company) { PRContactFor prContactFor = new PRContactFor(); prContactFor.Company = company; prContactFor.Contact = this; prContactFor.Company.PRContacts.Add(prContactFor); Companies.Add(prContactFor); } }
[MapTo("PRLinks"), OptimisticLocking(false)] public class PRContactFor: XPBaseObject { public PRContactFor(Session session) : base(session) {} public PRContactFor() : base() {} [Key(AutoGenerate = true)] public int ID; [Persistent("ContactID")] [Association("PRContactLink")] public Contact Contact; [Persistent("CompanyID")] [Association("CompanyLink")] public Company Company; }
<MapTo("Companies"), OptimisticLocking(False)> _ Public Class Company : Inherits XPBaseObject Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New() MyBase.new() End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer <Association("CompanyLink", GetType(PRContactFor)), Aggregated()> _ Public ReadOnly Property PRContacts() As XPCollection Get Return GetCollection("PRContacts") End Get End Property Public Name As String Public Sub AddPRContact(ByVal contact As Contact) Dim prContactFor As PRContactFor = New PRContactFor() prContactFor.Company = Me prContactFor.Contact = contact prContactFor.Contact.Companies.Add(prContactFor) PRContacts.Add(prContactFor) End Sub End Class
<MapTo("Contacts"), OptimisticLocking(False)> _ Public Class Contact : Inherits XPBaseObject Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New() MyBase.new() End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer <Association("PRContactLink", GetType(PRContactFor)), Aggregated()> _ Public ReadOnly Property Companies() As XPCollection Get Return GetCollection("Companies") End Get End Property Public FullName As String Public Sub AddCompany(ByVal company As Company) Dim prContactFor As PRContactFor = New PRContactFor() prContactFor.Company = company prContactFor.Contact = Me prContactFor.Company.PRContacts.Add(prContactFor) Companies.Add(prContactFor) End Sub End Class
<MapTo("PRLinks"), OptimisticLocking(False)> _ Public Class PRContactFor : Inherits XPBaseObject Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New() MyBase.new() End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer <Persistent("ContactID"), Association("PRContactLink")> _ Public Contact As Contact <Persistent("CompanyID"), Association("CompanyLink")> _ Public Company As Company End Class
|
Note that we have marked the associations with the Aggregated attribute to make sure the deletion of a company or a contact results in the deletion of the corresponding PRLink as well. That is all we need to map XPO business objects to the underlying model.
Complex Many-to-Many Data Model
Let us consider a more complex Company-Contacts data model. Assume that Contacts and Companies are stored in the same Subjects table and we distinguish them by SubjectType (0 for Contact, and 1 for Company).
Subjects table: ID – key field Name – Company name or Subject’s full name Email – Subject’s email address WebSite – Company’s web site address SubjectType – issue type (Contact or Company)
PRLinks table: ID – key field CompanyID – link to a Company by its key ContactID – link to a Contact by its key
Database designers sometimes put different types of records into one table when the data for those records has significant similarity and/or they need a universal reference. For this purpose, we will create a class called Subject which is mapped to the Subjects table:
|
[MapTo("PRLinks"), OptimisticLocking(false)] public class PRContactFor: XPBaseObject { public PRContactFor(Session session) : base(session) {} public PRContactFor() : base() {} [Key(AutoGenerate = true)] public int ID; [Persistent("ContactID")] [Association("PRContactLink")] public Subject Contact; [Persistent("CompanyID")] [Association("CompanyLink")] public Subject Company; }
public enum SubjectType {stContact = 0, stCompany = 1}
[MapTo("Subjects"), OptimisticLocking(false)] public class Subject: XPBaseObject { [Persistent] private SubjectType subjectType; public Subject(Session session) : base(session) {} public Subject(SubjectType subjectType) : base() { this.subjectType = subjectType; } [Key(AutoGenerate = true)] public int ID; public string Name; [Association("PRContactLink", typeof(PRContactFor)), Aggregated] public XPCollection Companies { get { return GetCollection("Companies"); } } public string Email; public void AddCompany(Subject company) { if (company.SubjectType != SubjectType.stCompany) { throw new Exception("Only companies are valid here"); } PRContactFor prContactFor = new PRContactFor(); prContactFor.Company = company; prContactFor.Contact = this; prContactFor.Company.PRContacts.Add(prContactFor); Companies.Add(prContactFor); } [Association("CompanyLink", typeof(PRContactFor)), Aggregated] public XPCollection PRContacts { get { return GetCollection("PRContacts"); } } public string WebSite; public void AddPRContact(Subject contact) { if (contact.SubjectType != SubjectType.stContact) { throw new Exception("Only contacts are valid here"); } PRContactFor prContactFor = new PRContactFor(); prContactFor.Company = this; prContactFor.Contact = contact; prContactFor.Contact.Companies.Add(prContactFor); PRContacts.Add(prContactFor); } public SubjectType SubjectType { get { return subjectType; } } }
<MapTo("PRLinks"), OptimisticLocking(False)> _ Public Class PRContactFor : Inherits XPBaseObject Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New() MyBase.new() End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer <Persistent("ContactID"), Association("PRContactLink")> _ Public Contact As Subject <Persistent("CompanyID"), Association("CompanyLink")> _ Public Company As Subject End Class
Public Enum SubjectType stContact = 0 stCompany = 1 End Enum
<MapTo("Subjects"), OptimisticLocking(False)> _ Public Class Subject : Inherits XPBaseObject <Persistent()> _ Private subjectType_ As SubjectType Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New(ByVal subjectType As SubjectType) MyBase.new() subjectType_ = subjectType End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer Public Name As String <Association("PRContactLink", GetType(PRContactFor)), Aggregated()> _ Public ReadOnly Property Companies() As XPCollection Get Return GetCollection("Companies") End Get End Property Public Email As String Public Sub AddCompany(ByVal company As Subject) If company.SubjectType <> SubjectType.stCompany Then Throw New Exception("Only companies are valid here") End If Dim prContactFor As PRContactFor = New PRContactFor() prContactFor.Company = company prContactFor.Contact = Me prContactFor.Company.PRContacts.Add(prContactFor) Companies.Add(prContactFor) End Sub <Association("CompanyLink", GetType(PRContactFor)), Aggregated()> _ Public ReadOnly Property PRContacts() As XPCollection Get Return GetCollection("PRContacts") End Get End Property Public WebSite As String Public Sub AddPRContact(ByVal contact As Subject) If contact.SubjectType <> SubjectType.stContact Then Throw New Exception("Only contacts are valid here") End If Dim prContactFor As PRContactFor = New PRContactFor() prContactFor.Company = Me prContactFor.Contact = contact prContactFor.Contact.Companies.Add(prContactFor) PRContacts.Add(prContactFor) End Sub Public ReadOnly Property SubjectType() As SubjectType Get Return subjectType_ End Get End Property End Class
|
That is all we need to map XPO business objects to the underlying model. In another variant, the data from the Subjects table can be mapped to the Contact and Company objects inherited from the base class called Subject as shown in the following code example:
|
[MapTo("PRLinks")] public class PRContactFor: XPBaseObject { [Key(AutoGenerate = true)] public int ID; [Persistent("ContactID")] [Association("PRContactLink")] public Contact Contact; [Persistent("CompanyID")] [Association("CompanyLink")] public Company Company; }
[MapTo("Subjects")] public class Subject: XPBaseObject { [Key(AutoGenerate = true)] public int ID; public string Name; }
[MapInheritance(MapInheritanceType.ParentTable)] public class Contact: Subject { [Association("PRContactLink", typeof(PRContactFor)), Aggregated] public XPCollection Companies { get { return GetCollection("Companies"); } } public string Email; public void AddCompany(Company company) { PRContactFor prContactFor = new PRContactFor(); prContactFor.Company = company; prContactFor.Contact = this; prContactFor.Company.PRContacts.Add(prContactFor); Companies.Add(prContactFor); } [Persistent] public int SubjectType { get { return 0; } } }
[MapInheritance(MapInheritanceType.ParentTable)] public class Company: Subject { [Association("CompanyLink", typeof(PRContactFor)), Aggregated] public XPCollection PRContacts { get { return GetCollection("PRContacts"); } } public string WebSite; public void AddPRContact(Contact contact) { PRContactFor prContactFor = new PRContactFor(); prContactFor.Company = this; prContactFor.Contact = contact; prContactFor.Contact.Companies.Add(prContactFor); PRContacts.Add(prContactFor); } [Persistent] public int SubjectType { get { return 1; } } }
<MapTo("PRLinks")> _ Public Class PRContactFor : Inherits XPBaseObject <Key(AutoGenerate:=True)> _ Public ID As Integer <Persistent("ContactID"), Association("PRContactLink")> _ Public Contact As Contact <Persistent("CompanyID"), Association("CompanyLink")> _ Public Company As Company End Class
<MapTo("Subjects")> _ Public Class Subject : Inherits XPBaseObject <Key(AutoGenerate:=True)> _ Public ID As Integer Public Name As String End Class
<MapInheritance(MapInheritanceType.ParentTable)> _ Public Class Contact : Inherits Subject <Association("PRContactLink", GetType(PRContactFor)), Aggregated()> _ Public ReadOnly Property Companies() As XPCollection Get Return GetCollection("Companies") End Get End Property Public Email As String Public Sub AddCompany(ByVal company As Company) Dim prContactFor As PRContactFor = New PRContactFor() prContactFor.Company = company prContactFor.Contact = Me prContactFor.Company.PRContacts.Add(prContactFor) Companies.Add(prContactFor) End Sub <Persistent()> _ Public ReadOnly Property SubjectType() As Integer Get Return 0 End Get End Property End Class
<MapInheritance(MapInheritanceType.ParentTable)> _ Public Class Company : Inherits Subject
<Association("CompanyLink", GetType(PRContactFor)), Aggregated()> _ Public ReadOnly Property PRContacts() As XPCollection Get Return GetCollection("PRContacts") End Get End Property Public WebSite As String Public Sub AddPRContact(ByVal contact As Contact) Dim prContactFor As PRContactFor = New PRContactFor() prContactFor.Company = Me prContactFor.Contact = contact prContactFor.Contact.Companies.Add(prContactFor) PRContacts.Add(prContactFor) End Sub <Persistent()> _ Public ReadOnly Property SubjectType() As Integer Get Return 1 End Get End Property End Class
|
This variant is far too complicated as it requires additional database modifications; steps to prepare the existing database and synchronize applications to work with the database plus an in-depth knowledge about the inner XPO system table structure (see details in the help documentation shipped with your version of XPO). We do not recommend using this variant as the previous example is simpler and as effective.
Adjusting the Business Object Model to Work with Several Databases that Have Different Structures
In an ideal world, existing data appears in the task- or case-compliant form and all data sources providing it keep to these rules. Obviously we would not have this situation in the real world as the data sources generally provide data in different forms and data has a distinctive structure. Therefore, we have to provide additional integration logic for each data source. These tedious complexities can be handled by XPO for you. Suppose we have two data sources (databases). The first one has the following structure.
Customer table: ID - key field FullName - Customer’s name
Order table: ID - key field Description - information related to order Owner - reference field which provides a link to Customer the Order belongs to.
While the latter has the following structure.
Customer table: Oid - key field Name - Customer’s name
Order table: Oid - key field Description - information related to order Customer - reference field which provides a link to Customer the Order belongs to.
Data structures have something in common, though the names of the fields and tables differ. XPO can combine these differences in the following data model.
|
[OptimisticLocking(false)] public class Customer: XPBaseObject { public Customer(Session session) : base(session) {} public Customer() : base() {} [Key(AutoGenerate = true)] public int ID; public string Name; [Aggregated, Association("Customer-Orders", typeof(Order))] public XPCollection Orders { get { return GetCollection("Orders"); } } }
[OptimisticLocking(false)] public class Order: XPBaseObject { public Order(Session session) : base(session) {} public Order() : base() {} [Key(AutoGenerate = true)] public int ID; public string Description; [Association("Customer-Orders")] public Customer Owner; }
<OptimisticLocking(False)> _ Public Class Customer : Inherits XPBaseObject Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New() MyBase.new() End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer Public Name As String <Aggregated(), Association("Customer-Orders", GetType(Order))> _ Public ReadOnly Property Orders() As XPCollection Get Return GetCollection("Orders") End Get End Property End Class
<OptimisticLocking(False)> _ Public Class Order : Inherits XPBaseObject Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New() MyBase.new() End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer Public Description As String <Association("Customer-Orders")> _ Public Owner As Customer End Class
|
We have not specified in code into which exact tables and columns the data is saved. Instead, we’ll provide this information in the form of metadata files called Metadata_DB1.xml and Metadata_DB2.xml, one for the first data source
<Model xmlns="http://www.devexpress.com/products/xpo/schemas/1.0/xpometadata.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Class assembly="MappingObjectsModel" type="MappingObjectsModel.Customer"> <Attributes> <OptimisticLocking Enabled="false"/> <MapTo MappingName="Customer"/> </Attributes> <Member name="ID"> <Attributes> <Persistent MapTo="ID"/> </Attributes> </Member> <Member name="Name"> <Attributes> <Persistent MapTo="FullName"/> </Attributes> </Member> </Class> <Class assembly="MappingObjectsModel" type="MappingObjectsModel.Order"> <Attributes> <OptimisticLocking Enabled="false"/> <MapTo MappingName="Order"/> </Attributes> <Member name="ID"> <Attributes> <Persistent MapTo="ID"/> </Attributes> </Member> <Member name="Description"> <Attributes> <Persistent MapTo="Description"/> </Attributes> </Member> <Member name="Owner"> <Attributes> <Persistent MapTo="Owner"/> </Attributes> </Member> </Class> </Model>
...and one for another data source.
<Model xmlns="http://www.devexpress.com/products/xpo/schemas/1.0/xpometadata.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Class assembly="MappingObjectsModel" type="MappingObjectsModel.Customer"> <Attributes> <OptimisticLocking Enabled="false"/> <MapTo MappingName="Customers"/> </Attributes> <Member name="ID"> <Attributes> <Persistent MapTo="Oid"/> </Attributes> </Member> <Member name="Name"> <Attributes> <Persistent MapTo="Name"/> </Attributes> </Member> </Class> <Class assembly="MappingObjectsModel" type="MappingObjectsModel.Order"> <Attributes> <OptimisticLocking Enabled="false" inautolist="true"/> <MapTo MappingName="Orders"/> </Attributes> <Member name="ID"> <Attributes> <Persistent MapTo="Oid"/> </Attributes> </Member> <Member name="Description"> <Attributes> <Persistent MapTo="Description"/> </Attributes> </Member> <Member name="Owner"> <Attributes> <Persistent MapTo="Customer"/> </Attributes> </Member> </Class> </Model>
These files contain only table and column names specific for a given data source. Now we can employ the Metadata_DB1.xml metadata file to access the first database as shown below.
|
session.Dictionary.LoadXmlMetadata(@"Metadata_DB1.xml");
session.Dictionary.LoadXmlMetadata("Metadata_DB1.xml")
|
The LoadXmlMetadata method loads the metadata information from the specified file and applies it for the current object model. Now we can freely address the first data source. Similar steps can be taken to address the second data source. This unified technique lets you access different data sources in the same way as shown above.
Reading Data from A Database View
Generally, whenever it is necessary to restrict the resulting data set or to select only the data matching particular criteria, database views are used. Most database servers let the user add, modify and delete the data from database views. XPO accesses data views in the same way as it does with the tables. For instance, we have a Subjects table which holds two record types, Contact and Company, that are distinguished by the value stored in the SubjectType field.
Subjects table: ID – key field Name – Contact’ or Company’s name Email – Contact’s email WebSite – Company’s web site SubjectType - issue type (Contact or Company)
PRLinks table: ID – key field CompanyID – link to a Company by its key ContactID – link to a Contact by its key
In order to access contact and company information separately, let us create two database views. One to retrieve companies
SELECT Subjects.ID, Subjects.Name, Subjects.WebSite, Subjects.SubjectType FROM Subjects WHERE SubjectType=1;
and one to retrieve contacts.
SELECT Subjects.ID, Subjects.Name, Subjects.Email, Subjects.SubjectType FROM Subjects WHERE SubjectType=0;
The following object model can be exactly mapped to the views.
|
[MapTo("PRLinks"), OptimisticLocking(false)] public class PRContactFor: XPBaseObject { public PRContactFor(Session session) : base(session) {} public PRContactFor() : base() {} [Key(AutoGenerate = true)] public int ID; [Persistent("ContactID")] [Association("PRContactLink")] public Contact Contact; [Persistent("CompanyID")] [Association("CompanyLink")] public Company Company; }
public enum SubjectType {stContact = 0, stCompany = 1}
[MapTo("Companies"), OptimisticLocking(false)] public class Company: XPBaseObject { public Company(Session session) : base(session) {} public Company() : base() {} [Key(AutoGenerate = true)] public int ID; public string Name; [Association("CompanyLink", typeof(PRContactFor)), Aggregated] public XPCollection PRContacts { get { return GetCollection("PRContacts"); } } public string WebSite; public void AddPRContact(Contact contact) { PRContactFor prContactFor = new PRContactFor(); prContactFor.Company = this; prContactFor.Contact = contact; prContactFor.Contact.Companies.Add(prContactFor); PRContacts.Add(prContactFor); } [Persistent] public SubjectType SubjectType { get { return SubjectType.stCompany; } } }
[MapTo("Contacts"), OptimisticLocking(false)] public class Contact: XPBaseObject { public Contact(Session session) : base(session) {} public Contact() : base() {} [Key(AutoGenerate = true)] public int ID; public string Name; [Association("PRContactLink", typeof(PRContactFor)), Aggregated] public XPCollection Companies { get { return GetCollection("Companies"); } } public string Email; public void AddCompany(Company company) { PRContactFor prContactFor = new PRContactFor(); prContactFor.Company = company; prContactFor.Contact = this; prContactFor.Company.PRContacts.Add(prContactFor); Companies.Add(prContactFor); } [Persistent] public SubjectType SubjectType { get { return SubjectType.stContact; } } }
<MapTo("PRLinks"), OptimisticLocking(False)> _ Public Class PRContactFor Inherits XPBaseObject Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New() MyBase.new() End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer <Persistent("ContactID"), Association("PRContactLink")> _ Public Contact As Contact <Persistent("CompanyID"), Association("CompanyLink")> _ Public Company As Company End Class
Public Enum SubjectType stContact = 0 stCompany = 1 End Enum
<MapTo("Companies"), OptimisticLocking(False)> _ Public Class Company Inherits XPBaseObject Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New() MyBase.new() End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer Public Name As String <Association("CompanyLink", GetType(PRContactFor)), Aggregated()> _ Public ReadOnly Property PRContacts() As XPCollection Get Return GetCollection("PRContacts") End Get End Property Public Sub AddPRContact(ByVal contact As Contact) Dim prContactFor As PRContactFor = New PRContactFor() prContactFor.Company = Me prContactFor.Contact = contact prContactFor.Contact.Companies.Add(prContactFor) PRContacts.Add(prContactFor) End Sub Public ReadOnly Property SubjectType() As SubjectType Get Return SubjectType.stCompany End Get End Property End Class
<MapTo("Contacts"), OptimisticLocking(False)> _ Public Class Contact Inherits XPBaseObject Public Sub New(ByVal session As Session) MyBase.New(session) End Sub Public Sub New() MyBase.new() End Sub <Key(AutoGenerate:=True)> _ Public ID As Integer Public Name As String <Association("PRContactLink", GetType(PRContactFor)), Aggregated()> _ Public ReadOnly Property Companies() As XPCollection Get Return GetCollection("Companies") End Get End Property Public Sub AddCompany(ByVal company As Company) Dim prContactFor As PRContactFor = New PRContactFor() prContactFor.Company = company prContactFor.Contact = Me prContactFor.Company.PRContacts.Add(prContactFor) Companies.Add(prContactFor) End Sub Public ReadOnly Property SubjectType() As SubjectType Get Return SubjectType.stContact End Get End Property End Class
|
That is all you have to do to map persistent objects to database views. It clearly resembles the case when data tables were used for mapping.
Custom Identity Values Generators
The examples above assumed that key fields in the database are autogenerated integers (identity in MS SQL or AutoNumber in MS Access). Additionally, XPO automatically supports Guid key fields. If you want to use your own identification scheme, you will have to implement your key generation logic. Let us consider a simple example. Suppose we want to generate an integer type key value for the Customer table and store it in a specific table.
|
[OptimisticLocking(false)] public class Customer: XPBaseObject { [Key] public int ID = -1; [Persistent("FullName")] public string Name; [Aggregated, Association("Customer-Orders", typeof(Order))] public XPCollection Orders { get { return GetCollection("Orders"); } } }
<OptimisticLocking(False)> _ Public Class Customer : Inherits XPBaseObject <Key()> _ Public ID As Integer = -1 <Persistent("FullName")> _ Public Name As String <Aggregated(), Association("Customer-Orders", GetType(Order))> _ Public ReadOnly Property Orders() As XPCollection Get Return GetCollection("Orders") End Get End Property End Class
|
The generator is a class responsible for acquiring or calculating a new key value for a table record via specific method(s). To make key generation algorithms work and to maintain the uniqueness of the key values generated, a persistent storage is used to store the most recently generated key value. We will use the IDGeneratorsTable as a table to store generated key values for all data tables that correspond to the persistent objects, i.e. object tables. The generator class implementation is shown in the following code example:
|
[MapTo("IDGeneratorsTable"), OptimisticLocking(false)] public class IDGeneratorClass : XPBaseObject { public IDGeneratorClass(): this(Session.DefaultSession) {} public IDGeneratorClass(Session session): base(session) {} [Persistent("Key"), Key(AutoGenerate = true)] public int Key; public int LastGeneratedID; public string TableName; }
<MapTo("IDGeneratorsTable"), OptimisticLocking(False)> _ Public Class IDGeneratorClass : Inherits XPBaseObject Public Sub New() MyBase.New() End Sub Public Sub New(ByVal session As Session) MyBase.New(session) End Sub <Persistent("Key"), Key(AutoGenerate:=True)> _ Public Key As Integer Public LastGeneratedID As Integer Public TableName As String End Class
|
Now we can extend the IDGeneratorClass functionality with the GenerateID method. This method receives a name of the object table, generates a unique key value for this table and saves it in the database as the last generated key value:
|
public static int GenerateID(string tableName) { IDGeneratorClass generator; generator = (IDGeneratorClass)generatorSession.FindObject( typeof(IDGeneratorClass), new BinaryOperator("TableName", tableName)); if (generator == null) { generator = new IDGeneratorClass(generatorSession); generator.TableName = tableName; } generator.LastGeneratedID++; generator.Save(); return generator.LastGeneratedID; }
Public Shared Function GenerateID(ByVal tableName As String) As Integer Dim generator As IDGeneratorClass generator = CType(generatorSession.FindObject( _ GetType(IDGeneratorClass), New BinaryOperator("TableName", tableName)), IDGeneratorClass) If generator Is Nothing Then generator = New IDGeneratorClass(generatorSession) generator.TableName = tableName End If generator.LastGeneratedID = generator.LastGeneratedID + 1 generator.Save() Return generator.LastGeneratedID End Function
|
Let us take a closer look at the GenerateID method of the IDGeneratorClass class. First, we try to find a persistent object (it is represented by the record in the IDGeneratorsTable) which contains a key value for the table whose name is passed as the method’s parameter.
|
generator = (IDGeneratorClass)generatorSession.FindObject( typeof(IDGeneratorClass), new BinaryOperator("TableName", tableName));
generator = CType(generatorSession.FindObject( _ GetType(IDGeneratorClass), _ New BinaryOperator("TableName", tableName)), IDGeneratorClass)
|
When the persistent object matching the specified criteria is found, we generate a new key value by incrementing the LastGeneratedID field and saving its value in the IDGeneratorsTable.
|
generator.LastGeneratedID++; generator.Save();
generator.LastGeneratedID = generator.LastGeneratedID + 1 generator.Save()
|
Otherwise, we create an instance of the identity value generator for the object table prior to key value generation.
|
if (generator == null) { generator = new IDGeneratorClass(generatorSession); generator.TableName = tableName; }
If generator Is Nothing Then generator = New IDGeneratorClass(generatorSession) generator.TableName = tableName End If
|
Now we can return the unique key value for the specified table.
|
return generator.LastGeneratedID;
Return generator.LastGeneratedID
|
The Customer and Order persistent objects should initialize their key fields before being saved. The obvious solution is to override their BeforeSave methods as shown below.
|
protected override void BeforeSave() { base.BeforeSave(); if (ID == -1) { ID = IDGeneratorClass.GenerateID(ClassInfo.IdClass.TableName); } }
Protected Overrides Sub BeforeSave() MyBase.BeforeSave() If ID = -1 Then ID = IDGeneratorClass.GenerateID(ClassInfo.IdClass.TableName) End If End Sub
|
To enable the GenerateID method to work properly, we should generate key values in a separate session apart from the main session(s) - one(s) used to retrieve and update the persistent object values. That is why we extend the IDGeneratorClass with two methods that are responsible for the generator session’s initialization and disposal.
|
static Session generatorSession; static void AfterSessionConnected(Session session) { generatorSession = new DevExpress.Xpo.Session(); generatorSession.ConnectionString = Session.DefaultSession.ConnectionString; generatorSession.Connect(); } static void AfterSessionDisconnected(Session session) { generatorSession.Disconnect(); generatorSession = null; }
Private Shared generatorSession As Session Shared Sub AfterSessionConnected(ByVal session As Session) generatorSession = New DevExpress.Xpo.Session() generatorSession.ConnectionString = session.DefaultSession.ConnectionString generatorSession.Connect() End Sub Shared Sub AfterSessionDisconnected(ByVal session As Session) generatorSession.Disconnect() generatorSession = Nothing End Sub
|
The obvious suitable places when these methods should be called are the Session.DefaultSession.AfterConnect and Session.DefaultSession.AfterDisconnect event handlers. We initialize them in the IDGeneratorClass’s static constructor as shown in the following code example.
|
static IDGeneratorClass() { Session.DefaultSession.AfterConnect += new SessionManipulationEventHandler(AfterSessionConnected); Session.DefaultSession.AfterDisconnect += new SessionManipulationEventHandler(AfterSessionDisconnected); if (Session.DefaultSession.IsConnected) { AfterSessionConnected(Session.DefaultSession); } }
Shared Sub New() AddHandler Session.DefaultSession.AfterConnect, _ AddressOf AfterSessionConnected AddHandler Session.DefaultSession.AfterDisconnect, _ AddressOf AfterSessionDisconnected If Session.DefaultSession.IsConnected Then AfterSessionConnected(Session.DefaultSession) End If End Sub
|
That is all you have to do to make your own key or identity values generators.
Notes
Triggers
In the case of MSSQL you can also implement the update routine within the database itself via update and insert triggers which would require no changes to your legacy ADO.NET code. MS Access does not provide trigger support but you could use the Defaults attribute for this.
Multiple Connections
If you plan to implement XPO and ADO.NET functionality within multiple connections, you will have to refresh the collections and datasets in order to make the changes available for both connections.
Conclusion
Moving from ADO.NET development to XPO development does not have to be an “All or Nothing” proposition, you can ease XPO development in gracefully and it can co-exist with all your legacy ADO.NET code as long as you follow these simple precautions. We have only covered the simplest and most common data models here and will in future articles cover more complex structures. Download C# sources for this article To learn more about XPO, please write to us at: info@devexpress.com. To order your copy, visit our online order page.
|