webForumDet fria alternativet

EntityMapper hur svårt kan det vara egentligen?

.NET

56 svar · 3 020 visningar · startad av Gladh · sida 3 av 3

Frågan, av Gladh

Hej alla. Det har diskuteras lite om ORMapper/EntityMapper och hur de fungerar och vad de är bra för. En ORMapper/EntityMapper är inget annat än ett sätt att låta våra objekt persisteras ner i någon sorts datakälla (eller hämtas därifrån), oftas en relationsdatabas (O/R Mapper = object/Relation Mapper). Vad vinner vi på det då? I sin enklaste form så det man vinner är att man på ett enkelt (oc

Läs frågan i sin helhet →
Medlem sedan maj 20012 812 inlägg
#41

Nu kommer sista delen av vår entitymapper / ORMapper. Nämligen att kunna spara ner ett objekt till vår databas.

När det gäller spara delen så finns det ju 2 sätt, och det ena är att göra en INSERT och det andra är att göra en UPDATE. Ett ganska enkelt sätt att kolla vilket man skall göra är att kontrollera om vårt ID har ett värde, alltså om det finns ett värde i vårt IdentifierFält så gör vi en UPDATE och finns det inget värde så gör vi en INSERT. Det har sina nackdelar i bland, men i 99,5% av alla gånger så räcker en kontroll som denna kontroll.

Ett annat kanske lite smartare alternativ är att vi i vår Identifier-klass har chans att markera om vi vill göra en UPDATE eller INSERT med detta objekt. Vilket vi just har möjlighet till med vår Identifier-klass.

Vi börjar här med själva Save-metoden i vår EntityMapper

        public ItemType Save<ItemType>(ItemType itemType)
        {

            //-- Declara variables
            IDbCommand command = null;
            DataTable dataTable = null;

            try
            {
                //-- Create information objects
                DataIdentifierInformation dataIdentifierInformation = CreateDataIdentifierInformation<ItemType>(itemType, null);
                List<DataFieldInformation> dataFieldInformationList = CreateDataFieldInformation<ItemType>(itemType);
                DataSourceInformation dataSourceInformation = CreateDataSourceInformation<ItemType>();
                
                //-- Create and execute the command if this is an INSERT operation against storage.
                if (dataIdentifierInformation.IdentifierSaveAction == IdentifierSaveActionEnum.Insert)
                {
                    //-- Execute command
                    command = CreateInsertCommand(dataFieldInformationList, dataIdentifierInformation, dataSourceInformation);
                    
                    //-- if the identifier is of the uniqueidentifier type, the identifier already has it's value set.
                    if (dataIdentifierInformation.IdentifierType == IdentifierTypeEnum.UniqueIdentifier)
                        YOUR_DAL.ExecuteNoResult(command);
                    //-- If the identifier is of the sequeenstype, we need to read that value from the databas and add it to the itemtype.
                    else 
                    {
                        dataTable = YOUR_DAL.ExecuteDataTableResult(command);
                        if (dataTable != null && !string.IsNullOrEmpty(dataTable.Rows[0][0].ToString()))
                            SetIdentifier<ItemType>(itemType, new Identifier(dataTable.Rows[0][0].ToString()));
                    }
                }
                //-- Execute command if this is only an UPDATE operation angainst storage.
                else
                {
                    command = CreateUpdateCommand(dataFieldInformationList, dataIdentifierInformation, dataSourceInformation);
                    YOUR_DAL.ExecuteNoResult(command);
                }
                return itemType;
            }
            finally
            {
                if (dataTable != null)
                    dataTable.Dispose();
                if (command != null)
                    command.Dispose();
            }
        }

Oj, det så väldigt komplicerat ut helt plötsligt. Men lugn det är det inte. Först så fyller vi våra Informations objekt precis som vi gjort tidigare. Sedan gör vi helt enkelt en kontroll på vår Identifier-objekt om detta objekt skall skall köras som en INSERT eller UPDATE.

Och när det gäller INSERTEN så har vi 2 alternativ, det ena är att vi har en GUID som identifier och då gör vi inget speciellt, det andra är att vi har en LONG/INT som identifier och då måste vi få tillbaka värdet från databasen till vårt objekt. Och det gör vi genom att när vi exekverara INSERT metoden så lägger vi på ett SELECT @@IDENTITY kommando också och hämtar tillbaka detta värde och sedan sätter vårt Identifierfield med detta värde.

- M

Medlem sedan maj 20012 812 inlägg
#42

För att sätta värdet på vårt identifier fält i vår klass så använder vi följande metod.

        private void SetIdentifier<ItemType>(ItemType itemType, object identifier)
        {
            //-- Get all fieldInfors for the ItemType
            FieldInfo[] fieldInfos = itemType.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

            //-- Check so we have some field
            if (fieldInfos == null || fieldInfos.Length == 0)
                throw new NoIdentifierFoundException();

            for (int i = 0; i < fieldInfos.Length; i++)
            {
                if (fieldInfos[i].FieldType == typeof(Identifier))
                {
                    fieldInfos[i].SetValue(itemType, new Identifier(identifier));
                    return;
                }
            }

            //-- Couldn't find any Identifier
            throw new NoIdentifierFoundException();
        }

Nu gäller det bara att kunna skapa de olika SQL Satserna och vi börjar med den enklare UPDATE...

private IDbCommand CreateUpdateCommand(List<DataFieldInformation> dataFieldInformationList, DataIdentifierInformation dataIdentifierInformation, DataSourceInformation dataSourceInformation)
{
    //-- Checks to see so we have any dataIdentifierfields
    if (dataIdentifierInformation == null)
        throw new NoDataIdentifierInformationDefinedException();

    //-- Create commandobject
    IDbCommand command = CreateSqlCommand();
    command.CommandText = UPDATE(dataFieldInformationList, dataSourceInformation) + WHERE(dataIdentifierInformation);

    //-- Create parameterlist
    foreach (IDataParameter parameter in CreateParameterList(dataFieldInformationList))
        command.Parameters.Add(parameter);

    //-- Create parameterlist
    foreach (IDataParameter parameter in CreateParameterList(dataIdentifierInformation))
        command.Parameters.Add(parameter);

    //-- returnerar command object.
    return command;
}

Inget nytt under solen där direkt, ja det skulle vara UPDATE metoden, men de flesta vet nog hur den skall se ut vid detta laget, och om inte så kommer den här...

private string UPDATE(List<DataFieldInformation> dataFieldInformationList, DataSourceInformation dataSourceInformation)
{
    //-- Only have the datafield that has DataFielOperation Write set.
    dataFieldInformationList = dataFieldInformationList.FindAll(
        delegate(DataFieldInformation d) { return ((d.DataFieldOperation & DataFieldOperationEnum.Write) == DataFieldOperationEnum.Write); }
        );

    StringBuilder sb = new StringBuilder();
    sb.Append(" UPDATE ");
    sb.Append(" [").Append(dataSourceInformation.DataSourceName).Append("] ");

    for (int i = 0; i < dataFieldInformationList.Count; i++)
    {
        if (i == 0)
            sb.Append(" SET ");
        else
            sb.Append(",");

        sb.Append("[").Append(dataFieldInformationList[i].DataFieldName).Append("] = ");
        sb.Append("@").Append(dataFieldInformationList[i].ClassFieldName).Append(" ");
    }

    //-- return the UPDATE statement
    return sb.ToString();
}

Så nu kan vi även göra en UPDATE på ett objekt som vi redan har hämtat från databasen, nu fattas bara en sak, att spara ner ett nytt objekt...

- M

Medlem sedan maj 20012 812 inlägg
#43

INSERT koden skiljer sig inte speciellt mycket från den övriga kod som jag visat...

private IDbCommand CreateInsertCommand(List<DataFieldInformation> dataFieldInformationList, DataIdentifierInformation dataIdentifierInformation, DataSourceInformation dataSourceInformation)
{
    //-- Create commandobject
    IDbCommand command = CreateSqlCommand();
    command.CommandText = INSERT(dataFieldInformationList, dataIdentifierInformation, dataSourceInformation);

    //-- Create parameterlist
    foreach (IDataParameter parameter in CreateParameterList(dataFieldInformationList))
        command.Parameters.Add(parameter);

    //-- returnerar command object.
    return command;
}

Vi måste dock göra ett lite tillägg i vår DataFieldInformation klass, och det är att lägga till följande property

        public bool IsIdentifier
        {
            get { return _Type == typeof(Identifier); }
        }

När vi gjort det så kan vi nämligen använda den för att hitta vilket fält i vår DataFieldInformation som är en Identifier...

private string INSERT(List<DataFieldInformation> dataFieldInformationList, DataIdentifierInformation dataIdentifierInformation, DataSourceInformation dataSourceInformation)
{
    //-- If the identifier is of the typ Sequenze it shouldn't be added to the INSERT statement.
    if (dataIdentifierInformation.IdentifierType == IdentifierTypeEnum.Sequenze)
        dataFieldInformationList = dataFieldInformationList.FindAll(
            delegate(DataFieldInformation d) { return !d.IsIdentifier; }
            );

    //-- Only have the datafield that has DataFielOperation Write set.
    dataFieldInformationList = dataFieldInformationList.FindAll(
        delegate(DataFieldInformation d) { return ((d.DataFieldOperation & DataFieldOperationEnum.Write) == DataFieldOperationEnum.Write); }
        );

    StringBuilder sb = new StringBuilder();
    sb.Append(" INSERT INTO ");
    sb.Append(" [").Append(dataSourceInformation.DataSourceName).Append("] (");

    for (int i = 0; i < dataFieldInformationList.Count; i++)
    {
        if (i != 0)
            sb.Append(",");

        sb.Append("[").Append(dataFieldInformationList[i].DataFieldName).Append("]");
    }
    sb.Append(") VALUES(");
    for (int i = 0; i < dataFieldInformationList.Count; i++)
    {
        if (i != 0)
            sb.Append(",");

        sb.Append("@").Append(dataFieldInformationList[i].ClassFieldName);
    }

    sb.Append(");");

    if (dataIdentifierInformation != null && dataIdentifierInformation.IdentifierType == IdentifierTypeEnum.Sequenze)
        sb.Append("SELECT SCOPE_IDENTITY();");

    //-- return the INSERT statement
    return sb.ToString();
}

Här börjar vi med att kontrollera om det är ett ID som är av typen räknare, så fall vill vi inte har med det i vår INSERT statement, och det sista vi gör är att lägga till SELECT SCOPE_IDENTITY() för att få ut detta värde från databasen.

Jag tror jag glömt en metod och det vara CreateParameterList som behövs i både UPDATE och INSERT metoderna för att fylla våra parameter som vi använder i SQL satserna. Här kommer den metoden...

private List<IDataParameter> CreateParameterList(List<DataFieldInformation> dataFieldInformationList)
{
    //-- Create parameter list
    List<IDataParameter> parameterList = new List<IDataParameter>();

    //-- Create a parameter for each datafield;
    foreach (DataFieldInformation dataFieldInformation in dataFieldInformationList)
    {
        SqlParameter parameter = new SqlParameter();
        parameter.IsNullable = true;
        parameter.ParameterName = "@" + dataFieldInformation.ClassFieldName;
        parameter.Direction = ParameterDirection.Input;
        if (dataFieldInformation.Value is Identifier)
        {
            if (((Identifier)dataFieldInformation.Value).Type == typeof(Int64))
                parameter.Value = (long)(Identifier)dataFieldInformation.Value;
            if (((Identifier)dataFieldInformation.Value).Type == typeof(Guid))
                parameter.Value = (Guid)(Identifier)dataFieldInformation.Value;
            if (((Identifier)dataFieldInformation.Value).Type == typeof(String))
                parameter.Value = ((Identifier)dataFieldInformation.Value).ToString();
        }
        else
            parameter.Value = (dataFieldInformation.Value != null ? dataFieldInformation.Value : DBNull.Value);

        //-- Add parameter to list
        parameterList.Add(parameter);
    }
    //-- return the list of parameters
    return parameterList;
}

Så nu skall ni ha fått grundlägganden information för hur man kan skapa sig en väldigt väldigt väldigt enkelt ORMapper/EntityMapper. Det är långt i från något fullgott exempel på hur en ORMapper/EntityMapper bör se ut och bland annat så önskas bättre felhantering och även en del grundläggande funktionallitet så som transactionshantering, med denna EntityMapper så måste du själv hantera om du behöver spara ner några objekt i ett transaktionsscope, det bör en riktig ORMapper hanterar till dig. Dessutom så kanske det är dumt att gör update på värden som inte är ändrade, så en riktigt bra ORMapper kan se vilka värden på objektet som är ändrat och bara göra update på de värden, det kräver dock lite mer intelligenta klasser och antagligen även att de ärver från en speciellt basklass...

Happy coding...

- M

Medlem sedan jan. 2008280 inlägg
#44

Tolka interface

Felpost

Medlem sedan feb. 20002 300 inlägg
#45

Kul att du orkar dela med dig av din entitymapper Gladh! :bire

En fråga angående XML-baserad mappningsinfo kontra attributbaserad:
Förutom fördelen med att undvika omkompilering när man använder XML. Hur påverkas prestandan? Är det någon skillnad mellan en inläsning av XML-filen för varje session kontra att komma åt attributen via reflection? Kan det bli märkbara prestandaförluster vid hård belastning?

Medlem sedan aug. 20003 575 inlägg
#46

Phorpher skrev:

Kul att du orkar dela med dig av din entitymapper Gladh! :bire

En fråga angående XML-baserad mappningsinfo kontra attributbaserad:
Förutom fördelen med att undvika omkompilering när man använder XML. Hur påverkas prestandan? Är det någon skillnad mellan en inläsning av XML-filen för varje session kontra att komma åt attributen via reflection? Kan det bli märkbara prestandaförluster vid hård belastning?

XML-inläsningen bör ju inte ta för lång tid och den görs ju enbart vid applikationsstart. Programet bör ju cacha alla skapade PropertyDescriptors osv så att de inte behöver sökas upp på nytt varje gång.

Reflection är långsammare, men jag tror inte man märker av det förens det är väldigt extremt, och jag tror man har många flaskhalsar att hitta innan man kommer till reflectiondelen.

Medlem sedan maj 20012 812 inlägg
#47

Som nickemannen säger så är reflection långsamt, bland det långsammaste man kan göra i .NET världen. Det vill säga att reflection är egentligen något som man bara skall göra när man måste och helst inte då heller. Men nu förtiden så är prestandan i maskinerna så bra så att man kan ta den prestandaförlusten om det avsevärt förenklar/förkortar utvecklingen vilket det gör med ORMapper.

Precis som Nickemannen skriver så bör man ju bara läsa i XML-filen en gång och sedan behålla den i minnet, en singleton är ju ett utnärkt pattern för det. Om man dessutom lägger en lyssnar på själva XML-filen, så kan man låta den läsa om sig om något ändras i XML-filen under drift, vilket betyder att du inte ens behöver starta om din applikation för att få in förändringarna.

Du kan göra något liknande med attributen att du håller en cach med informationen för varje klass, om du tycket att prestandan lider av att skapa dessa Informationsklasser varje gång, du måste dock använda dig av reflection för att både läsa och skriva data till dina klasser, så du kommer inte ifrån det helt...

- M

Medlem sedan jan. 20022 440 inlägg
#48

Jag undrar lite över följande

//-- Set the value to the entity field
                fieldInfo.SetValue(itemType, dataRow[dataField.DataFieldName] != DBNull.Value ? dataRow[dataField.DataFieldName] : null);

Jag får hela tiden

Visual Studio 2008 skrev:

Objekt av typen System.Int32 kan inte konverteras till typen Map.Identifier.

Medlem sedan maj 20012 812 inlägg
#49

Nu hittar jag inte den koden i inläggen men om det bara står så, så har jag glömt lite kod, för du kan inte bara typomvandla en Int32 till en identifier rakt av så.

Om du lägger till följande kod istället så bör det fungera.

                   //-- Set the value to the entity field
                    if (fieldInfo.FieldType == typeof(Identifier))
                        fieldInfo.SetValue(itemType, (dataRow[dataField.DataFieldName] == DBNull.Value ? null : new Identifier(dataRow[dataField.DataFieldName])));
                    else
                        fieldInfo.SetValue(itemType, dataRow[dataField.DataFieldName] != DBNull.Value ? dataRow[dataField.DataFieldName] : null);

- M

Medlem sedan jan. 20022 440 inlägg
#50

Visual Stuio 2008 skrev:

System.Web.HttpException: Det gick inte att hitta någon fältegenskap med namnet Identifier i den valda datakällan

När jag försöker binda listan med User till en GridView.

Konstigt nog finns Identifier med och den har värden som är av typen Int64. Jag spånar vidare imorn.

Medlem sedan jan. 20022 440 inlägg
#51

Då stötte jag på ett litet problem. Hur definierar jag relationer mellan mina klasser? Nu skulle jag till exempel behöva hämta relaterade poster i min User tabell för nyligen hämtade News rader. Eller alternativt mappa ihop det på något vis.

Jag kan tänka mig en enkel fullösning, att hämta alla poster och sedan leta igenom News-listan efter UserID och då hämta i Userlistan det jag behöver men det känns inte som en bra lösning.

En bra lösning vore ju om man kunde göra en join redan i select-satsen. Det går ju hämta i Users tabellen baserat på Identifier men jag är inte säker på hur jag gör det på "bästa" sätt.

En annan tanke som slog mig vore att använda OnEntityCreated() som du hade kommenterat ut men där är jag ute och seglar på mörka vatten i orkan igen :e

Medlem sedan maj 20012 812 inlägg
#52

Det finns inte inbyggt utan något som du får bygga till själv. Jag kan se 2 olika behov av relationerna. Den första som är lite enklare att hantera är att din klass består av enkelinformation från 2 eller flera tabeller. Alltså du har typ namn i en tabell och adressen i en annan, du har sedan en 1-1 relation mellan denna data. Här kan man hyfsat enkelt lägga till något attribut för att beskriva vilka kolumner i databasen som har relation mellan sig och så bygga en JOIN av dessa.

Den andra så är betydligt mer komplicerad och som man egentligen skulle vilja ha implementerad är när man har 1-många förhållande. Typ en Kund har många order, och en order har många orderrader. Här kan man också ganska enkelt göra beskrivningen mellan relationerna och bygga upp JOIN:en. Problemet blir att mappa datat rätt, för du måste ju hämta all information om kunden och order och orderraden och alla denna information kommer ju komma på en rad där kundinformationen kommer vara likadan för alla poster och där order informationen kommer vara samma för många poster och orderrads informationen kommer vara unik för varje post. Denna data skall sedan mappas ner till 1 kundobjet, några orderobjekt och en massa orderrader och varje orderrad måste placeras på rätt orderobjekt.

Det går givetviss att lösa men kräver en del omprogrammering av hur det fungerar just nu. Så det vanligast är helt enkelt använder lazyloading när man hämtar en kund. Så när man hämtar kunden så hämtar du inte orderna direkt. Utan dessa hämtas först när de efterfrågas, och då hämtar du alla order som har kundid=??, du får tillbaka alla order men inga orderrader och vill du ha orderrader så hämtar du alla orderrader som har orderID=?? osv osv..

- M

Medlem sedan jan. 20022 440 inlägg
#53

Jag har till exempel en nyhets entitet som i sin tur är länkad till users där jag lagrar namn, epost osv. Det jag funderade på är väl egentligen bästa sättet att hantera nyheterna eftersom jag alltid måste ladda relaterade poster i användartabellen.

Jag skulle kunna använda använda lazyloading till detta också men frågan är om jag ska ha det i samma lista det vill säga skapa en newsholder class som hanterar laddning av News med relaterade poster i user tabellen med userid som Identity?

Medlem sedan maj 20012 812 inlägg
#54

catz skrev:

Jag skulle kunna använda använda lazyloading till detta också men frågan är om jag ska ha det i samma lista det vill säga skapa en newsholder class som hanterar laddning av News med relaterade poster i user tabellen med userid som Identity?

Japp du är tvingad att använda LazyLoading om du inte orkar hacka om kodens själv, eftersom du måste specificera för EntityMapperna att datan från databasen skall in i 2 olika objekt, och du måste även berätta vilken data som skall in till vilket objekt. Då detta är en 1-1 relation så är det kanske inte så svårt att göra, eftersom varje rad i databasen kommer motsvara 2 objekt.

Jag skulle helt enkelt ha lazyloading på din user-property i din news-entity.

public class News{
 
 ...
 private Identifier _UserIdentifier;
 private User _User;
 ...

 public User User{
  get{
     if(_User == null && _UserIdentifier != null)
        _User = new EntityMapper().Load<User>(_UserIdentifier);
     return _User;
  }
  set{
    _User = value;
    _UserIdentifier = _User.Identifier;
  }
}

- M

Medlem sedan jan. 20022 440 inlägg
#55

Jag börjar få kläm på det här nu!

Det tändes förresten ett ljus vad det gäller generics under tiden. Alla möjligheter att plocka ut det man vill osv är ju underbart. Då kan jag i princip backa upp detta med Generics inbyggda funktioner för att sortera, filtrera och lägga till lite cache där det är applicerbart. Men stöd av det så gör det mindre att man använder sig av Reflection...

Härligt! Stort tack för starthjälpen (mitt batteri var slut) :e

Medlem sedan dec. 2004736 inlägg
#56

Trillade in i den här tråden nu och antar att det blir en del klippa klistra för att få ihop det här till något fungernade ;) Men det ska nog gå bra!
Vad jag undrar är hur bra det här skulle fungera hos ASP.net, för lösningen är ju smidig. Men det känns som att det kommer ta lite tid att hämta information ur en XML hela tiden. Det kanske blir till att skriva en massa kod för varje klass för att slippa öppna en XML-fil för varje sida som en besökare begär?

Medlem sedan jan. 20022 440 inlägg
#57

cache! :) du behöver väl inte hämta data varje gång något händer?

273 ms totalt · 4 externa anrop · v20260731065814-full.a51de22e
127 ms — deklarationer (db)
0 ms — hämta statistik (cache)
143 ms — hämta tråd, inlägg och bilagor (db)
123 ms — ändringar (db)