Lagra serialiserade objekt i databas
Förord
För att ta del av innehållet i den här artikeln bör du ha goda kunskaper i ASP.NET. Vi hoppar över delar som jag förutsätter att ni kan, exempelvis databaskopplingar. Exempelkoderna är skrivna i VB.NET. Ibland kan det vara smidigt att kunna lagra objekt under en längre tid i en extern datakälla. Jag tänkte här visa hur man kan lagra objekt i en databas och hur man gör för att återskapa dem. För att göra det ska vi bekanta oss med begreppet Serialization – något som har blivit mycket enkelt och smidigt i ASP.NET. Oftast kommer man i kontakt med det i samband med att man arbetar med XML och WebServices. Då använder man sig av klassen XMLSerializer. I det här exemplet kommer jag dock inte att använda mig av XML, då detta är något begränsat. Det använder sig av så kallad Shallow Serialization (ytlig) och kommer inte åt underliggande medlemmar i klasstrukturer, exempelvis privata variabler. Exemplet består av två delar; Först skapar vi ett objekt som vi serialiserar och lagrar i en MS SQL-databas. I den andra delen hämtar vi det lagrade objektet och återskapar det genom deserialisation. Jag avslutar med ett litet exempel från mina egna erfanrehter där en sådan här lösning kan vara bra.Importera följande Namespace:
System.IO
System.Runtime.Serialization.Formatters.Binary
Vi börjar med att skapa en enkel klass som får heta Person.
Klassen har två publika egenskaper för namn och ålder och en konstruktor som fyller objektets egenskaper. Något som är viktigt att notera är klassattributet <Serializable()>. Det gör att klassen kan serialiseras. Utan det attributet kommer vi att få ett fel.
Vi instansierar ett objekt av Person-klassen och serialiserar det till en MemoryStream.
Här instansierar vi en ny MemoryStream som vi kommer att fylla det serialiserade objektet i. Vi instansierar också en ny BinaryFormatter vars metod Serialize fyller vårt MemoryStream-objekt. Serialize-metoden tar objektet vi ska fylla som första parameter och det objekt som ska serialiseras som andra parameter.
Notis: I det här exemplet skapar jag den nya klassen direkt i konstruktorn.
När det är gjort så sätter vi positionen för myStream till 0. Detta måste vi göra innan vi sedan ska deserialisera strömmen.
Vi lagrar strömmen binärt i datakällan
Jag går inte in djupare på hur lagringen i databasen går till då jag förutsätter att ni redan behärskar det.
I MS SQL Server ska datatypen i fältet vara Binary och därför ska vi även göra strömmen till en array av typen Byte. Det gör vi med hjälp av metoden ToArray.
Om allt har gått som det ska så ska vi nu ha fyllt ett fält i databasen med det serialiserade objektet. Nu återstår det bara att återskapa det.
Obs: Du bör göra fältet för binary tillräckligt stort i databasen. I annat fall kan strömmen kapas. Resultatet blir att vi får ett felmeddelande där det står att objektet inte kan deserialiseras. Storleken måste du även ange i parametrarna i koden och i den Stored Procedure som används.
Vi hämtar det binära värdet från datakällan
Vi deklarerar en array av typen Byte och fyller den med värdet som vi hämtat från databasen. Därefter instansierar vi ett nytt objekt av typen MemoryStream och skickar med vår btResult-variabel som enda parameter i konstruktorn.
Nu har vi återigen den ström som vi hade tidigare då vi serialiserade objektet. Det enda som återstår nu är att med hjälp av objektet BinaryFormatters metod Deserialize återskapa det ursprungliga objektet. Men eftersom den metoden returnerar ett objekt måste vi konvertera objektet till dess rätta typ, som är Person i det här fallet. Vi ”typkastar” det helt enkelt med CType.
Nu har vi objektet återskapat och vi kan återigen använda dess ursprungliga funktionalitet.
Det finns flera olika situationer som det kan vara bra att spara hela objekt i databasen. Senast jag hade användning av detta var för att lagra objekt som "history" för ett e-handelssystem. E-handelssystemet bygger på Varupaket som består av ett antal subklasser och aggregerade klasser, ex rabatter, varor, produkter, tjänster, attribut etc. Sammansättningen av klasser (och i sin tur även relaterade tabeller) blev i vissa fall ganska komplexa.
Ett problem som uppstår då en kund gör en beställning och butiksinnehavaren därefter går in och modifierar innehållet i varupaketet är att historiken över beställningarna får missvisande information. I stället för att låta alla relaterade tabeller ha kvar dessa poster för historiken var det smidigare att spara dessa objekt som kopior. Samma problematik är det för Kund-objektet, då information även där kan förändras efter beställningen och därmed bli missvisande i betalningshistoriken (en kund kan ändra adress efter en beställning, men vi vill veta vilken adress som fanns vid köptillfället).
Fördelen med detta sättet är enkelheten. Efter behov kan vi helt enkelt hämta hela objektet och lätt avläsa de egenskaper objektet hade vid just den tidpunkten.
Hoppas ni haft skoj!
System.IO
System.Runtime.Serialization.Formatters.Binary
Del I: Serialisering av objekt
Vi börjar med att skapa en enkel klass som får heta Person.
<Serializable()> _
Public Class Person
Private sName As String
Private iAge As Integer
Public Property Name() As String
Get
Return sName
End Get
Set(ByVal Value As String)
sName = Value
End Set
End Property
Public Property Age() As Integer
Get
Return iAge
End Get
Set(ByVal Value As Integer)
iAge = Value
End Set
End Property
Public Sub New(ByVal Name As String, ByVal Age As Integer)
sName = Name
iAge = Age
End Sub
End Class
Klassen har två publika egenskaper för namn och ålder och en konstruktor som fyller objektets egenskaper. Något som är viktigt att notera är klassattributet <Serializable()>. Det gör att klassen kan serialiseras. Utan det attributet kommer vi att få ett fel.
Vi instansierar ett objekt av Person-klassen och serialiserar det till en MemoryStream.
Dim myStream As New MemoryStream()
Dim myFormatter As New BinaryFormatter()
myFormatter.Serialize(myStream, New Person("Bratten", 22))
myStream.Position = 0
Här instansierar vi en ny MemoryStream som vi kommer att fylla det serialiserade objektet i. Vi instansierar också en ny BinaryFormatter vars metod Serialize fyller vårt MemoryStream-objekt. Serialize-metoden tar objektet vi ska fylla som första parameter och det objekt som ska serialiseras som andra parameter.
Notis: I det här exemplet skapar jag den nya klassen direkt i konstruktorn.
När det är gjort så sätter vi positionen för myStream till 0. Detta måste vi göra innan vi sedan ska deserialisera strömmen.
Vi lagrar strömmen binärt i datakällan
Jag går inte in djupare på hur lagringen i databasen går till då jag förutsätter att ni redan behärskar det.
myStream.ToArray()
I MS SQL Server ska datatypen i fältet vara Binary och därför ska vi även göra strömmen till en array av typen Byte. Det gör vi med hjälp av metoden ToArray.
Om allt har gått som det ska så ska vi nu ha fyllt ett fält i databasen med det serialiserade objektet. Nu återstår det bara att återskapa det.
Obs: Du bör göra fältet för binary tillräckligt stort i databasen. I annat fall kan strömmen kapas. Resultatet blir att vi får ett felmeddelande där det står att objektet inte kan deserialiseras. Storleken måste du även ange i parametrarna i koden och i den Stored Procedure som används.
Del II: Deserialisering av objekt
Vi hämtar det binära värdet från datakällan
Dim btResult As Byte() = myCommand.Parameters("@object").Value
Dim myStream As MemoryStream = New MemoryStream(btResult)
Vi deklarerar en array av typen Byte och fyller den med värdet som vi hämtat från databasen. Därefter instansierar vi ett nytt objekt av typen MemoryStream och skickar med vår btResult-variabel som enda parameter i konstruktorn.
Dim myFormatter As New BinaryFormatter()
Dim objPerson As Person = CType(myFormatter.Deserialize(myStream), Person)
Nu har vi återigen den ström som vi hade tidigare då vi serialiserade objektet. Det enda som återstår nu är att med hjälp av objektet BinaryFormatters metod Deserialize återskapa det ursprungliga objektet. Men eftersom den metoden returnerar ett objekt måste vi konvertera objektet till dess rätta typ, som är Person i det här fallet. Vi ”typkastar” det helt enkelt med CType.
Response.Write(“Hej och välkommen tillbaka “ & objPerson.Name & ”!”)
Nu har vi objektet återskapat och vi kan återigen använda dess ursprungliga funktionalitet.
Vad är nyttan med detta?
Det finns flera olika situationer som det kan vara bra att spara hela objekt i databasen. Senast jag hade användning av detta var för att lagra objekt som "history" för ett e-handelssystem. E-handelssystemet bygger på Varupaket som består av ett antal subklasser och aggregerade klasser, ex rabatter, varor, produkter, tjänster, attribut etc. Sammansättningen av klasser (och i sin tur även relaterade tabeller) blev i vissa fall ganska komplexa.Ett problem som uppstår då en kund gör en beställning och butiksinnehavaren därefter går in och modifierar innehållet i varupaketet är att historiken över beställningarna får missvisande information. I stället för att låta alla relaterade tabeller ha kvar dessa poster för historiken var det smidigare att spara dessa objekt som kopior. Samma problematik är det för Kund-objektet, då information även där kan förändras efter beställningen och därmed bli missvisande i betalningshistoriken (en kund kan ändra adress efter en beställning, men vi vill veta vilken adress som fanns vid köptillfället).
Fördelen med detta sättet är enkelheten. Efter behov kan vi helt enkelt hämta hela objektet och lätt avläsa de egenskaper objektet hade vid just den tidpunkten.
Hoppas ni haft skoj!
Patrik Löwendahl
Vad är nyttan? Praktiska exempel på varför man skulle vilja spara objektet i en databas, vad som gör att det väger upp förlusten av relations modellen för datan.
Andreas Brantmo
Jag har lagt till ett exempel på hur man kan ha användning av en sådan lösning. Tack för kommentaren.
Staffan sjöstedt
Bra artikel Andreas, enkel & rätt på. Jag har gjort en liknande lösning med en liten modifikation. I databastabellen för person har jag fält för id, namn, telefon, email etc (sånt som kan vara bra å kunna söka på) men jag har dessutom ett binärt fält där jag sparar en serialiserad HashTable med ytterligare egenskaper som personen kan tänkas ha. Vilka de är vet jag kanske inte just nu men ni hoppa upp å sätt er på att en dag kommer men att behöva ytterligare egenskaper (t ex taxerad inkomst, svarta inkomster, skostorlek ,,, ) Personens data läser jag till ett objekt liknande ”person” i artikeln men den har även metoder för att läsa/skriva till Hashtablen. Det är bara att ösa på och ange nya nyckel-värdepar efter behov och vara säker på att dessa sparas i databasen.
Jörgen Lindroos
Ett exempel till: Ifall man har olika typer av objekt i databasen (t.ex. kunder, leverantörer, råvaror, fakturor etc.) kan det vara bra att ha en funktion som gör att när man tar bort något objekt, läggs det i en sk. papperskorg. Papperskorgen består av en tabell innehållande ett nummer som betecknar typ av objekt, objektets id och objektet som en serialiserad klass. På det sättet kan alla typer av objekt finnas i samma tabell. Frågan är isåfall: Tar det serialiserade objektet mindre plats än en rad i tabellen för den typen av objekt? Jag antar att det gör det.
Andreas Brantmo
Hej Jörgen och Staffan. Tack för era kommentarer. Angående Jörgens fråga om storleken för serialiserade objekt kontra tabeller i databasen så tror jag inte detta ska användas för att vinna utrymme. Ofta när man arbetar med stora system försöker man flytta arkivdata till andra tabeller (gärna även andra databasservrar) för att underlätta urval. Man vill ju helst bara ha relevant och aktuell information i de mest använda delarna. Har man komplexa objekt så resulterar det ofta i många tabeller, och i sin tur mycket relationer, i en normaliserad databas. Fördelen här är att man kan lyfta över en större mängd data och komprimera dess placering. Precis som du föreslog med "papperskorgen".