Object persistence in het .NET framework 2.0
Afgelopen oktober werd in Los Angeles de Microsoft Professional Developers Conference gehouden. Tijdens deze conferentie werden ontwikkelaars op de hoogte gebracht van nieuwe technologieën die er aan gaan komen. Een van deze nieuwe technologieën is een onderdeel uit ADO.NET 2.0 genaamd ObjectSpaces.
ObjectSpaces is een alternatieve manier om met datatoegang om te gaan. In plaats van datasets of datareaders is het mogelijk te werken met objecten die je direct kunt wegschrijven in de database. Dit soort technologie is in concept niet nieuw. Sterker nog, in andere ontwikkelomgevingen zoals bijvoorbeeld Java wordt deze technologie al langere tijd toegepast (Entity Beans en JDO).
Microsoft heeft deze technologie, die ook bekend staat als object relational mapping, echter zelf nooit toegepast, omdat het vertalen van objecten naar een relationele opslag en vice-versa een stevige performance impact kan hebben. Dit is ook een probleem geweest in de Java-omgeving, maar ook daar begint deze technologie steeds meer volwassen te worden.
Object relational mapping biedt diverse voordelen t.o.v. de gangbare datatoegang, te weten:
- Het niet meer hoeven schrijven van datatoegangscode zoals SQL of Stored Procedures.
- Het relatief eenvoudig kunnen vertalen van objectmodellen die gebruikt worden tijdens de analyse naar een technische implementatie.
- Het volledig objectgeoriënteerd kunnen programmeren van een applicatie.
In de volgende paragrafen zal ik de basisconcepten uiteenzetten en laten zien hoe je met behulp van ObjectSpaces kunt gaan programmeren.
De basis: een persistent object
Om van een object een persistent object te maken moet je op een of andere manier kunnen aangeven hoe de representatie van een object eruit ziet in een relationeel model.
Laten we eens beginnen met een simpel voorbeeld waarbij we een klasse Persoon hebben met de properties VoorNaam, AchterNaam en GeboorteDatum.
Om het object Persoon te kunnen wegschrijven in een database kunnen we een tabel maken met bijvoorbeeld de naam Persoon die de kolommen Voornaam, Achternaam en Geboortedatum bevat. De vertaling die wordt gemaakt is dan: één object instantie wordt één rij in de tabel.
In dit scenario is er een eenvoudige één op één vertaling te maken tussen het object en de database. Laten we dit eens implementeren in ObjectSpaces.
Om met ObjectSpaces te kunnen werken heb je op dit moment de alfaversie nodig van Visual Studio “Whidbey”. Hierbij wordt ook het nieuwe .NET framework 2.0 geleverd en daarin is onder de namespace “System.Data.ObjectSpaces” de ObjectSpaces technologie terug te vinden.
ObjectSpaces kent als hoofdklasse de klasse ObjectSpace. Deze klasse levert de object–relational-mapping-engine. Deze engine heeft, om de vertaling van object naar database en vice versa te kunnen maken, een aantal definities nodig. Ten eerste moet de objectspace-engine weten hoe de objecten en de database eruit zien. Verder moeten we aangeven hoe we een object naar de database kunnen wegschrijven en hoe we vanuit de data uit de database een object kunnen maken. Hiervoor moeten we een drietal XML-bestanden gaan maken met daarin de definities. De relatie tussen deze XML-bestanden en ObjectSpaces is in figuur 1 weergegeven.

Fig. 1
In het OSD.XML-bestand (Object Schema Definition) staat de XML-beschrijving van de objecten. In het RSD.XML-bestand (Relational Schema Definition) is de beschrijving van het datamodel opgenomen. In het MAP.XML-bestand staat de referentie naar de OSD- en RSD-definities en de mapping die tussen beide moet worden gemaakt. De ObjectSpaces-engine kan dan vervolgens op basis van die informatie de objecten ophalen, aanmaken en wegschrijven in de database.
De code om een persoon object aan te maken en deze in de database weg te schrijven is te zien in listing 1.
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.ObjectSpaces;
namespace Persoon
{
public class Persoon
{
public Persoon(){}
public int Id;
public string VoorNaam ;
public string AchterNnaam;
public System.DateTime GeboorteDatum;
}
public class MainClass
{
static void Main()
{
String connectionString =
"Integrated Security=SSPI;Data Source=.;Initial catalog=ObjectSpaces;"
SqlConnection conn = new SqlConnection ( connectionString );
ObjectSpace os = new ObjectSpace("..\\..\\Map.XML", conn);
Persoon p = new Persoon();
p.VoorNaam = "Marcel";
p.AchterNnaam = "de Vries";
p.GeboorteDatum = new System.DateTime(1972, 1, 4);
os.StartTracking(p, InitialState.Inserted);
os.PersistChanges(p);
}
}
}
Listing 1
Er is een klasse gemaakt die eruit ziet als een willekeurige .NET-klasse. De persistente klasse hoeft niet af te leiden van een speciale basisklasse om te kunnen worden weggeschreven in de database, wat bij de meeste vergelijkbare technologieën wel het geval is. Alle informatie over de mapping en persistentie van de objecten wordt volledig buiten de code gehouden in de drie XML-bestanden.
Er wordt een instantie aangemaakt van de Persoon en hierop worden de gewenste properties gevuld met informatie. We moeten aan de objectspace-instantie, in het geval van een nieuw object dat niet door de ObjectSpaces-engine is opgehaald uit de database, eerst aangeven dat het object er is. Dit doen we door de methode StartTracking() aan te roepen. Vanaf dat moment zal de ObjectSpaces-engine weten dat dit object nieuw is en aan de database zal moeten worden toegevoegd. De ObjectSpaces-engine zal zelf de benodigde SQL hiervoor samenstellen en deze vervolgens naar de database sturen.
Ophalen van objecten met behulp van OPath
Het ophalen van objecten uit de database wordt gedaan met behulp van een nieuwe query-taal genaamd OPath. In OPath kun je een query samenstellen die een beschrijving geeft van de objecten waarin we geïnteresseerd zijn. De object-relational-mapping-engine zal deze query vertalen naar een SQL-statement in de database op basis van de beschikbare informatie uit het mapping-file.
De OPath-syntax is een mix van de syntax die we gebruiken bij het programmeren in C# of VB.NET als het gaat om het aangeven van object-properties. Verder zijn er predikaten en operatoren gedefinieerd die overeenkomen met die in SQL. Denk hierbij aan predikaten als Like, In en IsNull en operatoren als +, -, !, <, >, <>, etc. Houd er rekening mee dat OPath case sensitive is als het gaat om de notatie van de properties die worden gebruikt in de query.
Een OPath query voor het ophalen van alle persoon objecten met de voornaam die begint met de letters “Mar” ziet er als volgt uit: “VoorNaam like ‘Mar%’”
En voor alle personen die op 1-1-1972 zijn geboren, gebruik je: “GeboorteDatum = #1/1/1972#
OPath is een door Microsoft ontwikkelde taal die op dit moment nog in ontwikkeling is.
Het stellen van de query wordt gedaan door op een instantie van objectspace een van de methodes GetObjectReader() of GetObjectSet() aan te roepen. Een ObjectReader is te vergelijken met een Datareader en levert een stream van objecten terug. Op de ObjectReader gebruik je de methode Read() om het volgende object in de stream te selecteren. De property Current bevat dan de referentie naar het object waarin we geïnteresseerd zijn.
De ObjectReader is een forward-only mechanisme om de objecten op te halen uit de database. Een ObjectList daarentegen is te vergelijken met een DataSet. Bij een ObjectList worden alle objecten ineens in het geheugen gelezen en kan daarna met behulp van een iterator de collectie van objecten worden benaderd. Zowel bij het gebruik van een ObjectReader als een ObjectList kunnen veranderingen aan de objecten worden weggeschreven naar de database door de methode PersistChanges() aan te roepen op de objectspace-instantie. Alle veranderingen zullen dan in de database worden weggeschreven.
Als objecten zijn opgehaald met behulp van ObjectSpaces dan hoeft de methode StartTracking() niet te worden aangeroepen, omdat de objecten al bekend zijn bij ObjectSpaces. Indien je een nieuw object aanmaakt en dit aan een reeds opgehaalde ObjectSet toevoegt is het ook niet noodzakelijk om StartTracking te gebruiken. Hoe het ophalen van Objecten uit de database met behulp van een ObjectReader en een ObjectSet er in code er uit ziet, kun je terug vinden in listing 2.
ObjectReader r = os.GetObjectReader(typeof(Persoon), "VoorNaam like 'Mar%'");
while (r.Read())
{
p = (Persoon)r.Current;
Console.WriteLine(p.VoorNaam);
}
ObjectSet s = os.GetObjectSet(typeof(Persoon), "VoorNaam like 'Mar%'");
foreach (Persoon p2 in s)
{
Console.WriteLine(p2.VoorNaam);
}
Listing 2
Object relaties
Als we het hebben over een object model dan hebben we het ook over relaties tussen objecten. Relaties tussen objecten worden geïmplementeerd in de vorm van properties.
Neem ons voorbeeld van Persoon. Deze kan een relatie hebben met bijvoorbeeld een Adres. Dit Adres kun je dan terug vinden als een property van de Persoon met als type Adres. Als een Persoon meerdere adressen kan hebben van een bepaald type, bijvoorbeeld een woonadres en een postadres, dan zal de property worden geïmplementeerd als een collection van adressen. Als er sprake is van een veel-op-veel relatie, dan zal in de adresklasse een collection van personen als property moeten worden opgenomen.
Stel dat we een persoon opvragen uit de database waarbij we ook geïnteresseerd zijn in zijn adresgegevens. Het is dan niet zo efficiënt om eerst een query naar de database te sturen voor de personen en vervolgens een query om de adresgegevens op te gaan halen. Om objecten en hun relaties zo efficiënt mogelijk uit de database op te halen zijn er in ObjectSpaces twee mogelijkheden om een OPath-query uit te voeren. De eerste manier is de manier die we al gezien hebben in voorgaand voorbeeld. Indien de opgehaalde personen een relatie hebben en we deze willen benaderen, dan is er de mogelijkheid dat de ObjectSpaces-engine voor ons automatisch de informatie gaat lezen voor de gerelateerde objecten. Dit principe noemen we “Delay loading”. De voorwaarde om met “Delay loading” te kunnen werken is dat de relaties worden geïmplementeerd met properties die gebruik maken van een ObjectHolder of een ObjectList. De ObjectHolder wordt gebruikt in het geval van een één-op-één relatie en een ObjectList in het geval van een één-op-veel relatie.
De tweede manier is de volledige objecthiërarchie in één keer uit de database te lezen. Dit is mogelijk door bij het stellen van de OPath-query een zogenaamde “Span” op te geven. In een “Span” geef je aan welke gerelateerde objecten ook moeten worden opgehaald. De ObjectSpaces engine zal dan vervolgens de volledige hiërarchie van de objecten in één database-roundtrip ophalen.
Omdat de keuze voor “delay loading” of “span loading” afhankelijk is van de context waarin de objecten worden gebruikt, is hiermee rekening gehouden in de implementatie van ObjectSpaces. Als er een “span” wordt opgegeven bij het uitvoeren van de OPath-query, dan zal de “delay load” niet meer zal plaatsvinden. Op deze manier kan, afhankelijk van de context waarin de objecten worden gebruikt, door de ontwikkelaar zelf worden besloten wat de beste strategie is vanuit het oogpunt performance.
Inheritance
In objectoriëntatie is het mogelijk om inheritance toe te passen. Het principe inheritance kennen we ook wel als we het hebben over logische datamodellen, maar inheritance is niet direct te implementeren in een relationele database. Als we in een objectmodel wel inheritance hebben toegepast, dan willen we dit ook kunnen gebruiken als we gebruik maken van ObjectSpaces. Om met inheritance om te kunnen gaan zal er een mapping moeten worden gemaakt om van een inheritance-hiërarchie een representatie te maken in de relationele database.
Voor het mappen van inheritance is er ruwweg een drietal beproefde methoden:
- Voor iedere klasse in de klasse hiërarchie een tabel maken in de database. Afhankelijk van het type object zal de data in de juiste tabel worden weggeschreven.
- Voor alle types in een hiërarchie één tabel aanmaken in de database waar alle instanties uit de zelfde inheritance-hiërarchie worden weggeschreven. Op basis van een type-indicatie veld wordt dan aangegeven welk object type is opgeslagen in de betreffende rij in de tabel.
- Voor de basisklasse wordt een tabel gemaakt met daarin alle properties als kolom. Voor alle geërfde types wordt een tabel gemaakt waarin alleen properties worden weggeschreven die niet worden geërfd uit de basisklasse. Er is dan een één-op-één relatie tussen de basisklasse tabel en de tabel voor het geërfde type. Bij het wegschrijven van het object zal de data worden opgesplitst in de tabellen op basis van het type.
In figuur 2 zijn de drie mapping methoden weergegeven waarbij de Klant-klasse een inheritance-relatie heeft met de Persoon-klasse. De Klant erft de properties VoorNaam, AchterNaam en GeboorteDatum van de Persoon-klasse en heeft zelf nog twee extra properties: Creditcard en BesteedBedrag.

Fig. 2
In scenario 1 zal iedere instantie van een Persoon worden weggeschreven in de Persoon-tabel en iedere Klant in de Klant-tabel.
In scenario 2 zal iedere instantie van Klant en van Persoon worden weggeschreven in de zelfde tabel, waarbij in het geval van een Persoon voor de velden CreditCard en BesteedBedrag de waarde null wordt ingevuld. Verder wordt een type aanduiding weggeschreven in het Type-veld om te kunnen bepalen van welk object type de rij afkomstig is.
In scenario 3 zal een instantie van Persoon worden weggeschreven in de PersoonBase-tabel. Van een instantie Klant zullen de properties geërfd van de basisklasse in de PersoonBase-tabel worden weggeschreven en de overige properties in de Klant-tabel. Verder wordt een één-op-één relatie gelegd tussen persoon en klant.
IObjectNotification
Als we een object wegschrijven in de database, is er een moment nodig waarop wordt gecontroleerd of de objecten die er zijn wel voldoen aan de binnen de applicatie gestelde regels. Deze regels noemt men vaak bedrijfsregels. Als we gebruik maken van de standaard beschikbare ADO.NET-technologie met DataSets, dan zal veelal ergens in de code door de set worden gelopen om de bedrijfsregels te controleren. Als we echter kijken naar object oriëntatie, dan zou je alle objecten volledig autonoom willen maken met zo min mogelijk kennis van de buitenwereld. Dit houdt dus in, dat je de controle of een object een geldig object is ook wilt leggen binnen dit object. ObjectSpaces biedt een mogelijkheid om de objecten op de hoogte te stellen van relevante momenten in de levenscyclus van het object. Dit is mogelijk door de Interface IObjectNotification op het object te implementeren. Stel dat ons Persoon-object een bedrijfsregel bevat die stelt dat personen die wij registreren in het systeem een minimale leeftijd moeten hebben van 15 jaar. Het is dan mogelijk dit te implementeren door het IObjectNotification interface aan de Persoon-klasse toe te voegen en bijvoorbeeld de bedrijfsregel af te dwingen in de OnUpdating()- en OnCreating()-implementatie van het interface.
Als er in het object een geboortedatum is ingegeven voor de persoon die minder dan 15 jaar in het verleden ligt, dan kunnen we een exception gooien. Deze exception zal dan bij het aanroepen van de PersistChanges() terugkomen en kan dan worden afgehandeld door de aanroepende code. De voorbeeldcode voor het Persoon-object met de implementatie voor de bedrijfsregel is gegeven in listing 3.
using System;
using System.Data.ObjectSpaces;
namespace Persoon
{
public class Persoon : IObjectNotification
{
public Persoon(){}
public int Id;
public string VoorNaam ;
public string AchterNnaam;
public System.DateTime GeboorteDatum;
// Implementatie IObjectNotification
public void OnUpdating()
{
CheckAgeBusinessRule();
}
public void OnCreating()
{
CheckAgeBusinessRule();
}
public void OnCreated(){}
public void OnPersistError(){}
public void OnDeleting(){}
public void OnUpdated(){}
public void OnMaterialized(){}
public void OnDeleted(){}
// Centrale functie voor afdwingen businessrule
private void CheckAgeBusinessRule()
{
if (GeboorteDatum.AddYears(15).Year > DateTime.Now.Year)
{
throw new ApplicationException("Persoon jonger dan 15!");
}
}
}
}
Listing 3
Conclusie
ObjectSpaces is een nieuwe datatoegangstechnologie waarbij het schrijven van applicaties drastisch kan worden versimpeld. Doordat er een heldere vertaling kan worden gemaakt tussen ontwerp en implementatie waarbij object oriëntatie maximaal kan worden gebruikt, zal de ontwikkeltijd voor applicaties sterk verminderen. Daarnaast verwacht ik dat de kosten voor het onderhouden van de applicaties ook lager zullen zijn, doordat er significant minder code hoeft te worden geschreven. Verder hoeft er geen SQL-code meer te worden geschreven en te worden onderhouden.
Aan deze voordelen zit natuurlijk ook een nadeel. Het nadeel is dat de performance van ObjectSpaces altijd minder zal zijn dan het direct werken met de relationele data. Er zal in ieder geval bij het gebruik van ObjectSpaces goed moeten worden nagedacht of het gemak van gebruik en de snelheid waarmee je de applicatie kunt ontwikkelen opweegt tegen de verminderde performance. Ik denk dat Microsoft met deze nieuwe technologie de softwareontwikkelgemeenschap een belangrijke dienst bewijst en dat de ontwikkelaars er een belangrijk nieuw gereedschap bij krijgen voor het ontwikkelen van hun applicaties.
Er valt natuurlijk nog veel meer te vertellen dan in deze introductie aan de orde is gekomen. Denk aan zaken als de mapping-XML-bestanden, object-identity, transacties, complexe relaties, query-caching, integratie met bestaande ADO.Net-code, etc . Ik hoop dat je met deze korte introductie in ieder geval een beeld hebt gekregen wat ObjectSpaces inhoudt en wat het voor jou in de toekomst kan gaan betekenen.