- Posted by Jakob Andersen on March 19, 2008
This post is in danish and contains a simple introduction to using ActiveRecord, it is heavily based on the guide found here. If you would like an english version of the content let me know.
Introduktion
Jeg vil i denne blogpost introducere Castle ActiveRecord der er en implementation på .NET platformen af det efterhånden udbredte ActiveRecord designpattern. ActiveRecord er måske mest kendt fra implementationen af det i Ruby on Rails.
Hvad er ActiveRecord?
ActiveRecord er en måde at forsimple din adgang til databaser i et objektorienteret udviklingssprog. Ideen er at vi laver en række klasser der "wrapper" vores database tabeller og en instans af disse klasser repræsenterer en række i selvsamme tabel. Praktisk i C# ville denne implementation foregå ved at vi har en klasse med statiske metoder til at operere på hele tabellen mens vi har instansmetoder og egenskaber der opererer på den enkelte række i tabellen. Et eksempel på dette kunne være en klasse der repræsenterer en blog:
public class Blog{
//Statiske metoder, der opererer på "tabelniveau"
public static List<Blog> FindAll(){..}
public static Blog Find(int identifier){..}
//Instansmetoder der operer på en "rækkeniveau"
public void Update(Blog instance){..}
public void Create(Blog instance){..}
//Data for en enkelt række
public int ID{get;set;}
public string Name{get;set;}
public string Owner{get;set;}
}
Manuel implementering er oftest triviel
Det er relativt simpelt at implementere f.eks. Update og Create ovenstående selv men det er en masse kode man skal skrive som egentlig ikke er særlig udfordrende, de ting man skal håndtere er f.eks.:
- Håndtering af databaseforbindelser
- Opbygning af hhv. UPDATE og INSERT statement som oftest er trivielle
- Håndtere parametre for at undgå SQL injection og herigennem håndtere forskelle mellem typer i .NET og på ens database
- Fejlhåndtering som f.eks. check på om det rent faktisk lykkedes at opdatere eller om ingen rækker blev modificeret
Når vi snakker om FindAll og Find metoderne i ovenstående klasse begynder der straks at komme nogle flere problemer til end ovenstående, nu skal vi have vores typesvage resultatsæt fra databasen i form af eksempelvis en DataReader ført over i en typestærk repræsentation i form af vores Blog klasse, det vil typisk resultere i kode som nedenstående:
public static List<Blog> FindAll(){
List<blog> blogs = new List<Blog>();
SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["BlogEngineConnection"]);
SqlCommand comm = conn.CreateCommand("SELECT ID, Name, Owner FROM Blogs");
conn.Open();
DataReader blogReader = comm.ExecuteReader();
while(blogReader.Read()){
Blog b = new Blog();
b.ID = blogReader.GetInt32(blogReader.GetOrdinal("ID"));
b.Name = blogReader.GetString(blogReader.GetOrdinal("Name"));
b.Owner = blogReader.GetString(blogReader.GetOrdinal("Owner"));
blogs.Add(b); }
comm.Dispose();
conn.Close();
conn.Dispose();
}
Koden bliver kun mere besværlig hvis vi vil have relationer mellem vores objekter f.eks. vores Owner egenskab på Blog klassen repræsenteret som en instans af et brugerobjekt eller en collection af Post objekter på selvsamme klasse. Og hvis vi ikke vil indlæse en collection med det samme men først når de bliver brugt? Det er her Castle ActiveRecord kommer på banen, hvis vi fortæller lidt om vores databasestruktur til Castle forærer det os tilgengæld en masse funktionalitet som ovenstående.
Klasser som ActiveRecords
Jeg vil i denne og de næste artikler benytte en datamodel til et blogsystem som eksempel. Hvis du vil følge med selv undervejs kan du hente seneste version af de binære filer du skal bruge på Castle projektets hjemmeside. Det er værd at bemærke at der findes værktøjer til at hjælpe dig med at generere dine ActiveRecord klasser som f.eks. ActiveWriter men da min holdning er det er vigtigt at forstå hvad der sker før vi benytter kodegenerering gør vi det i hånden her.
Når vi skal forklare ActiveRecord at en klasse repræsenterer en tabel i databasen gør vi det ved at dekorere klassen med en attribut kaldet ActiveRecord. Den simpleste udgave af dette kunne se sådan her ud.
[ActiveRecord]
public class Blog
{
//..
}
Castle ActiveRecord benytter sig af en regelen "convention over configuration". I praksis betyder det at Castle ActiveRecord vil bruge konventioner til at udføre den arbejde. I ovenstående vil det betyde at Castle vil antage at vores tabel i databasen har samme navn som vores klasse. Selvfølgelig er det muligt at specificere dette selv.
[ActiveRecord("Blogs")]
public class Blog
{
//..
}
I ovenstående kan vi se hvordan vi angiver et tabelnavn ved at sende en streng som parameter til vores ActiveRecord attribut.
Primærnøgler og egenskaber
Når vi skal angive kolonner og eventuelt deres navne gør vi det også ved hjælp af forskellige attributter så hele vores blogklasse ville se ud som nedenstående:
[ActiveRecord("Blogs")]
public class Blog : ActiveRecordBase<Blog>
{
[PrimaryKey]
public int ID{
get;
set;
}
[Property]
public string Name
{
get;
set;
}
[Property]
public string Owner
{
get;
set;
}
}
PrimaryKey attributten fortæller at denne egenskab er vores primær nøgle i tabellen mens Property attributterne blot refererer til normale kolonner i tabellen. Alle de brugte Attributter i ovenstående er i Castle.ActiveRecord namespacet, så det skal inkluderes i dine filer.
Bemærk også at klassen nedarver fra ActiveRecordBase<T> dette er ikke en nødvendighed, men det er den letteste måde at få vores ActiveRecord hurtigt op at køre.
Konfiguration
Næste skridt er at konfigurere ActiveRecord så den ved hvilken database vi bruger og hvordan der skal forbindes til den. Denne konfiguration kan vi ligge i en XML fil, nedenstående er et eksempel på sådan en konfiguration:
<?xml version="1.0" encoding="utf-8" ?>
<activerecord>
<config>
<add key="hibernate.connection.driver_class" value="NHibernate.Driver.SqlClientDriver" />
<add key="hibernate.dialect" value="NHibernate.Dialect.MsSql2005Dialect" />
<add key="hibernate.connection.provider" value="NHibernate.Connection.DriverConnectionProvider" />
<add key="hibernate.connection.connection_string" value="Data Source=.\SQLEXPRESS;Initial Catalog=BlogEngine;Integrated Security=SSPI" />
</config>
</activerecord>
Den første værdi bestemmer hvilken driver der skal bruges til at forbinde til databaseserveren, den anden parameter specificerer dialekten, den bestemmer hvordan den SQL der bliver produceret for dig ser ud og hvilken database den skal være kompatibel med, der findes dialekter til bl.a. Firebird, Access, SQL Server 2000, SQL Server 2005, Oracle og en del flere. Det betyder også du kan skifte database relativt let så længe du kun tilgår den igennem ActiveRecord. Den tredje egenskab connection.provider bestemmer hvordan connections skal håndteres internt og skal stort set aldrig være andet end ovenstående. Og den sidste egenskab er connectionstring til din database.
Hvis man har erfaring med nHibernate vil man se at dette er de samme parametre man bruger til at nHibernate's konfiguration, det skyldes at Castle ActiveRecord faktisk benytter nHibernate.
Nu mangler vi bare at fortælle ActiveRecord hvor konfigurationen er, det gør vi ved at bruge ActiveRecords XmlConfigurationSource som findes i Castle.ActiveRecord.Framework.Config namespacet og derefter fortæller vi hvilke klasser vi har der er ActiveRecords:
XmlConfigurationSource source = new XmlConfigurationSource("ActiveRecordConfig.xml");
ActiveRecordStarter.Initialize(source, typeof(Blog));
Det er værd at bemærke at Initialize har flere overloads så du kan også bede den om selv at finde alle klasser markeret med ActiveRecord så du ikke selv skal sidde og opdatere en liste med alle dine typer, det kald kunne f.eks. se sådan ud ActiveRecordStarter.Initialize(typeof(Blog).Assembly, source);
Automatisk oprettelse af databasestruktur
Jeg har ikke fortalt dig hvordan du opretter dine tabeller i databasen endnu, og det er fordi du rent faktisk ikke behøver at oprette dem selv, ActiveRecord kan oprette databasestrukturen udfra de informationer du har givet den om mapping og ved at bruge Reflection på dine typer, så hvis vi kalder ActiveRecordStarter.CreateSchema() vil vi få oprettet tabeller i den database vi har specificeret i konfigurationen. Du kan også få slettet strukturen igen ved at kalde DropSchema() metoden.
Oprettelse af objekter
Efter konfiguration og oprettelse af databasestruktur vil vi kunne skrive vores kode sådan her:
Blog b = new Blog();
b.Name = "Responding to change";
b.Owner = "Jakob Andersen";
b.Create();
Hvis vi undersøger hvad ovenstående kode sender til databasen ser vi nedenstående:
INSERT INTO Blogs (Name, Owner) VALUES (@p0, @p1); select SCOPE_IDENTITY(); @p0 = 'Responding to change', @p1 = 'Jakob Andersen'
ActiveRecord generer altså vores INSERT statement for os. Og bedst af alt, vi kan tilføje en egenskab til vores klasse uden at skulle røre ved noget datalag kode. Den skal blot oprettes i databasen og markeres med en attribut i vores ActiveRecord klasse.Udover at oprette nye objekter kan vi selvfølgelig også ændre i objekter vi har hentet ud vha. ActiveRecord. Det foregår ved at kalde Save metoden på objektet
Simple relationer mellem ActiveRecord klasser
Nu kan vi begynde at tilføje flere ActiveRecord klasser eksempelvis Blogger og Post,men hvad med relationerne mellem klasserne, ideelt set ville vi gerne have det til at se ud som nedenstående:
Det er let at gøre dette med ActiveRecord f.eks. kan vi modificere vores Owner egenskab på Blog klassen til at være af typen Blogger istedet for en streng, hvis vi skal gøre det opretter vi Blogger som ActiveRecord og vi kan nu modificere vores Blog klasses Owner egenskab til:
[BelongsTo]
public Blogger Owner{get;set;}
Istedet for at markere den med Property attrbutten specificerer vi nu en BelongTo attribut på, det instruerer ActiveRecord i at denne kolonne i databasen indeholder en fremmednøgle og denne skal omsættes til et objekt af typen Blogger. I den ande ende af relationen på vores Blogger klasse kan vi finde ud af hvilke blogs en Blogger ejer ved at lave en collection på Blogger objektet som nedenstående:
[HasMany]
public IList<Blog> Blogs
{
get;
set;
}
Her benytter vi HasMany attributten den fortæller at en Blogger kan have mange Blogs, den ved at den skal hente data om hvilke Blogs det er i den anden ende af relationen, dvs. at dette er primærnøgle tabellen og fremmednøglen ligger i Blogs tabellen.
Mange til mange relationer
Hvis vi skal introducere mange-til-mange relationer kan det gøres ved at introducere at man kan sætte et Tag på en Post, et Tag kan være på mange Post's og en Post kan have mange Tags. Når vi skal specificere dette i vores ActiveRecord klasser bruger vi attributten HasAndBelongsToMany. Da databasen her har brug for at oprette en ekstra tabel skal vi specificere nogle informationer i attributten om hvad denne tabel skal hedde og dens kolonnenavne, de to egenskaber med attributter i hhv. Tag og Post klasserne vil se sådan ud:
[ActiveRecord]
public clas Tag : ActiveRecordBase<Tag>{
.....
[HasAndBelongsToMany(Table = "PostTags", ColumnKey = "TagID", ColumnRef = "PostID", Inverse = true)]
public IList<Post> Posts
{
get;
set;
}
]
public class Post : ActiveRecordBase<Post>{
....
[HasAndBelongsToMany(Table = "PostTags", ColumnKey = "PostID", ColumnRef = "TagID")]
public IList<Tag> Tags{
get;
set;
}
}
Vi specificerer navnet på tabellen der skal indeholde mange-til-mange relationen samt hvad nøglen er for denne del af relationen og hvad fremmednøglen er. Derudover udvælger vi en ende af relationen som vi arbejder på når vi tilføjer og fjerner elementer fra kolonnen, dette specificeres med Inverse = true i attributtens parametre. I vores tilfælde er det Post der er den styrende ende af relationen. Det vil sige vi tilføjer et Tag til en Post og ikke omvendt.
Persistering af relaterede objekter
Nu når vi har indført relationer i vores datamodel er der en vigtig ting at nævne, nemlig cascades. Vi kan specificere på vores relationer om de skal gemmes når vi gemmer den instans de er en del af. Hvis vi ikke specificerer noget vil vi skulle manuelt kalde Save på samtlige objekter i vores objektgraf. Hvis vi specificerer en cascade kan vi nøjes med kun at gøre det på et enkelt objekt. Et eksempel kunne være at vi ville gemme vores Blogger når vi glemte Blog, det kan specificeres sådan her:
[BelongsTo(Cascade = CascadeEnum.SaveUpdate)]
public Blogger Owner { get; set; }
Udover SaveUpdate, er der cascades Delete, All og None. Bemærkt at None er default.
Specielt med lange tekster
Der er en enkelt ting du vil se i kildekoden jeg ikke har nævnt, en længde på Text egenskaben på Post klassen, dette er for at tvinge databasen til at lave en kolonne der kan indeholde arbitrær lang tekst, som f.eks. på SQL Server en TEXT type fremfor en varchar type. I samme åndedræt skal det siges at ActiveRecord gætter på dine strenglængder (dvs de altid ender på 255) medmindre du specificerer dem, så specificer dem hvis du kan
Næste gang
Det var en hurtig opsætning af en række mappede typer i Castle ActiveRecord. jeg vil anbefale dig at downloade kildekoden, kigge på den og undersøge de metoder der er på dine klasser som de har nedarvet fra ActiveRecordBase<T>. I den næste post vil jeg kigge på hvordan vi kan forespørge på vores data, både med Find og FindAll metoderne men også mere komplekst. Derudover vil jeg kigge på hvordan vi kan sikre os at kun det data vi har brug for bliver hentet
VS2008 solution: ActiveRecordBlogSample.zip
UPDATE: Se mig kode ovenstående i et screencast