Object Relational Mapping in ASP.NET 2.0
page 5 of 7
by Stephen Rylander
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 34138/ 36

Domain Objects

Part of the elegance of using an ORM product is that you can continue to use the domain (or call them entity) objects that you might design before event deciding on using ORM.  This allows you to keep your custom objects and other steps towards a domain model and OOP principles intact while at the same time simplifying your database access.  See Figure 1 for a part of the domain model used on this project.

Figure 1

In the above figure you can see part of the domain model.  Here, we have objects describing a Listing, Feedback, Owner, Event and an interface for Event named IEventDetail. 

Configuration File

The configuration file is what ties together the database tables and fields with your objects as shown in Listing 1.

Listing 1

<?xml version="1.0" encoding="utf-8" ?>
<mappings version="4.2" defaultNamespace="">
    <entity type="Owner" table="Owner"
    keyMember="Id" keyType="Auto" sortOrder="LastName ASC" autoTrack="false">
    <attribute member="Id" field="OwnerId" alias="Id" />
    <attribute member="FirstName" field="FirstName" alias="FirstName" />
    <attribute member="LastName" field="LastName" alias="LastName" />
    <attribute member="Street" field="Street" alias="Street" />
    <attribute member="Street2" field="Street2" alias="Street2" />
    <attribute member="Number" field="Number" alias="Number" />
    <attribute member="City" field="City" alias="City" />
    <attribute member="State" field="State" alias="State" />
    <attribute member="Postal" field="Postal" alias="Postal" />
    <attribute member="Country" field="Country" alias="Country" />
    <attribute member="Company" field="Company" alias="Company" />
    <attribute member="Email" field="Email" alias="Email" />
    <attribute member="UserName" field="UserName" alias="UserName" />
    <attribute member="Salt" field="Salt" alias="Salt" />
    <attribute member="PasswordEncrypted" field="Password" alias="Password" />
    <attribute member="Role" field="Role" alias="Role" />
    <relation relationship="OneToMany" member="Listings" field="OwnerId"
    type="Listing" alias="Listings" lazyLoad="true" cascadeDelete="true" />
    <relation relationship="OneToMany" member="Events" field="OwnerId"
    type="Event" alias="Events" lazyLoad="true" cascadeDelete="false" />
  </entity>

The configuration file is in XML, so the hierarchy of data is simple to read and just as easy to understand after you use it for a short while.  The entire file is wrapped in the mappings element and each table-to-object relationship is defined in an entity element.  The Listing 1 example above ties the object Owner to the table Owner.  It sets the object key, Id, to the tables' primary key via the keymember attribute.  It then sets the default sort order and a attribute named autoTrack which lets the ORM framework know whether or not to watch this object and perform SQL changes on its own.

Each property of the object is then mapped to a database field via an attribute element and member attribute. 

Notice the element relation.  This provides foreign key relationships to other tables.  In this example the field OwnerId in the table Owner is linked to the table Listing with a synonymous field name.  From here we can set the Listing object to be lazy loaded inside of the Owner object.  So, to summarize this, each object is mapped to a table in the configuration file.  Furthermore, a related object can be lazy loaded as a property in another object via foreign key relations.

ObjectSpace Manager

The ObjectSpace object is part of the WilsonORMapper libraries and is the main coordinator that is used by the developer to access data through the framework.  It is best to only have one instance of the ObjectSpace object in an application.  Doing so will help manage concurrency in the application between the data source and the mapper and secondary; the object loads up the xml configuration file from disk.  So, obviously you do not want to repeatedly read from disk or recreate the object over and over.  See Listing 2 for an example of how to create the object, in our case and by example from Wilson, the object is static and named Manager.

Listing 2

public class Global
{
    private static ObjectSpace _Manager;
    public static ObjectSpace Manager
    {
        get
        {
            if (_Manager == null)
            {
                CreateORMapper();
            }
            return _Manager;
        }
    }
 
    private static void CreateORMapper()
    {
string mappingFile =    System.Web.HttpContext.Current.Server.MapPath("~/trader.config");
      _Manager = new ObjectSpace(mappingFile, ConnectString, Provider.Sql2005);
    }
}

Select

Let us take a look at a method that retrieves data from our source by doing a simple SELECT in Listing 3 below.

Listing 3

public Event GetEvent(int Id)
{
   return (Event)Global.Manager.GetObject(typeof(Event), Id);
}

This method simply returns a filled Event object via the Id passed into the method.  We use the GetObject method on the Manager object, which is of type ObjectSpace, and tell it the type of object to return.  The mapper then knows what table the object type is mapped to and does the translation for us. The GetObject method can also then take in the parameter here, which is simply doing a query via the primary key setup in the configuration file.

Notice here how everything is strongly typed.  The code does not have to know anything about ADO.NET or even the internals of the Event object.  The mapping loads the right result set fields into the created Event object properties and fields. 

On a side note, it is a matter of preference if you map your table fields to properties of the object or fields.  The more draconian OOP bylaws would say that if you’re setting data in an object it should be via properties. But here, the mapper can access everything inside of the object anyway – so, it is a matter of preference.

Returning one object is simple, but the world works in large numbers, so let us look at returning a collection of objects. See Listing 4 below.

Listing 4

public IList GetEvents(string sortOrder)
{
    ObjectQuery oq = new ObjectQuery(typeof(Event), "", sortOrder);
    ObjectSet allEvents = Global.Manager.GetObjectSet(oq);
    return allEvents;
}

The GetObjects method of the Manager object returns a collection object called an ObjectSet, which is part of the WilsonORMapper libraries.  Being a well behaved collection it implements the IList interface.  So our GetEvents above can be typed to IList to add some flexibility. 

Insert, Update and Delete

Doing the other parts of the CRUD cycle (create, update and delete) are very similar.  Listing 5 below shows an insert and Listing 6 shows an update.  These are all working against that same Event type.

 Listing 5

public void Insert(Event theEvent)
{
Global.Manager.StartTracking(theEvent, Wilson.ORMapper.InitialState.Inserted);
Global.Manager.PersistChanges(theEvent, Wilson.ORMapper.PersistDepth.ObjectGraph);
Global.Manager.EndTracking(theEvent);
}

Listing 6

public void Update(Event theEvent)
{
Global.Manager.StartTracking(theEvent, Wilson.ORMapper.InitialState.Updated);
Global.Manager.PersistChanges(theEvent, Wilson.ORMapper.PersistDepth.ObjectGraph);
Global.Manager.EndTracking(theEvent);
}

The basics of this operation are that the methods first call a WilsonORMApper method called StartTracking on the Manager object and then does the persistence.  StartTracking lets the mapper know that this object and its associated row in the database are going to have work done to it.  I am not sure if WilsonORMapper locks the row then or not, but it does add safety to the operation.  Part of StartTracking is telling the mapper if we will be inserting and updating data.  Then we call the PersistChanges method and lastly end our tracking of that object and database row. 

Performing a delete is very similar and can be seen in Listing 7.

Listing 7

public void Delete(Event theEvent)
{
Global.Manager.StartTracking(theEvent, Wilson.ORMapper.InitialState.Unchanged);
Global.Manager.MarkForDeletion(theEvent);
Global.Manager.PersistChanges(theEvent, Wilson.ORMapper.PersistDepth.ObjectGraph);
Global.Manager.EndTracking(theEvent);
}

The only addition with the delete is we call MarkForDeletion which is another trigger letting the mapper know what we are about to do to that object and its data.


View Entire Article

User Comments

No comments posted yet.

Product Spotlight
Product Spotlight 





Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-04-16 2:34:48 PM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search