webForumDet fria alternativet

EntityMapper hur svårt kan det vara egentligen?

.NET

56 svar · 3 020 visningar · startad av Gladh · sida 2 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 dec. 2005664 inlägg
#21

Jo, givetvis så binder man upp sig på ett eller annat sätt om man vill använda någon slags mapper. Men visst skulle man lika väl kunna ha attribut som säger vilket fällt i klassen som är identifier och sedan låta fältet ha den typ som man brukar ha (int, guid etc.). Sedan i mappern hantera detta.

En annan fråga, varför bör man ha mappningsinformationen i filer istället som attribut?

Medlem sedan maj 20012 812 inlägg
#22

cok skrev:

Men visst skulle man lika väl kunna ha attribut som säger vilket fällt i klassen som är identifier och sedan låta fältet ha den typ som man brukar ha (int, guid etc.). Sedan i mappern hantera detta.

Visst kan du det, det brukar nog vara det vanligast också, men jag valde att göra en egen klass, lite bara för att det var "häftigt" att testa på och se hur det hela skulle fungera. Det är ju knappast en fullblodig entitymapper som jag gjort, utan mer någon för att leka och lära....

cok skrev:

En annan fråga, varför bör man ha mappningsinformationen i filer istället som attribut?

Säg att du har ett fält i din databas som heter L_Name och du i din class har satt attributet [DataField("L_Name")] på en variabel. Om du nu av någon anledning vill ändra namnet i din databas kolumn till LastName, så betyder det att du måste in i ditt projekt och ändra och sedan kompilera om. Och kompilera om är något som man helst inte vill göra.

Säg att du har lagt en version 1.0 i produktion, och håller på med version 2.0, du har nu gjort en massa förändringar i din kod och helt plötsligt så ändra DB:an namnet i databasen från L_Name till LastName, då skall du in och kompilera om din kod, men måste se till så inga förändringar slår igenom. Du har alltså 2 vägar att lösa det på, vilket båda är mindre bra:

1. Skapa en vy i databasen där du skickar tillbaka fältet som L_Name, fast det heter LastName i kolumnen.
2. Plocka fram din version 1.0 från din sourcesafe och hoppas på att allt är korrekt markerat så du får ut exkat rätt version, och där efter ändra till LastName och kompilera om och lägga ner filen i produktion. Och sedan göra samma förändring i din version 2.0.

Jag skulle nog råda dig till alternativ 1, men oftast slutar det med en massa olika vyer och nödlösningar. Alternativ 2 är helst något man inte gör, där jag jobbade innan så hade man produktionssättning 2 gånger om året. Under övrig tid så fick man inte lägga något i produktion om det inte var en ren buggrättning som var allvarlig. Så allt som man kan lösa utan att behöva kompilera och lägga i produktion är att fördra, så konfigfiler är perfekta i det sammahanget. Vi hade till och med gjort en speciell settingsdatabas som vi mappade mot VS2005 settings funktion. Där värden lades i en databas som ändrades från ett webinterface under kontrollerade former. Väldigt smidigt och man slapp ner på serverns xml-filer för att ändra något till applikationen.

- M

Medlem sedan maj 20012 812 inlägg
#23

Så då fortsätter vi med DELETE. Vi vill helt enkelt ta bort en rad från databasen som motsvara vårt objekt.

Nu har vi alla attribute klara och även vår Userklass har allt den behöver så vi skall bara skapa några nya metoder i vår EntityMapper.

public void Delete<ItemType>(ItemType itemType) 
{
    IDbCommand command = null;
    try
    {
        //-- Create information objects
        DataSourceInformation dataSourceInformation = CreateDataSourceInformation<ItemType>();
        DataIdentifierInformation dataIdentifierInformation = CreateDataIdentifierInformation<ItemType>(itemType, null);

        //-- Create Command
        command = CreateDeleteCommand(dataIdentifierInformation, dataSourceInformation);
        int affectedRow = //Här anropar du ditt DAL med command objektet och exekverar det med metoden ExecuteNonQuery() av prestandaskäl... ;)
    }
    finally
    {
        if (command != null)
            command.Dispose();
    }        
}

Här återanvänder vi ju lite gamla metoder så det enda som är nytt här är CreateDeleteStatement() och det är den som bygger ihop vår DELETE sats och den är inte heller speciellt komplicerad.

private IDbCommand CreateDeleteCommand(DataIdentifierInformation dataIdentifierInformation, DataSourceInformation dataSourceInformation)
{
    //-- Checks to see so we have any dataIdentifierfields
    if (dataIdentifierInformation == null)
        throw new NoDataIdentifierInformationDefinedException();

    StringBuilder sb = new StringBuilder();
    string whereStatement = WHERE(dataIdentifierInformation);
    if (string.IsNullOrEmpty(whereStatement))
        throw new Exception();

    sb.Append(DELETE(dataSourceInformation)).Append(whereStatement);

    //-- Create commandobject
    IDbCommand command = CreateSqlCommand();
    command.CommandText = sb.ToString();

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

    //-- Check so we really has a parameter value
    if (command.Parameters.Count == 0)
          throw new Exception();
    //-- returnerar command object.
    return command;
}

Här dök det upp en gammal välkänd metod WHERE(), den är vi lite försiktiga med, för om den skulle råka returnera en tom sträng, så kommer vi ta bort alla rader i databasen och de vill vi inte, så därför kontrollerar vi att den verkligen innehåller något innan vi fortsätter (hängslen och livrem)...

Det fanns en ny metod som vi inte stött på tidigare. DELETE och den bygger faktiskt bara upp vår DELETE sats, och eftersom denna är för en SQL Server så har den ingen * i sin DELETE sats...

private string DELETE(DataSourceInformation dataSourceInformation)
{
	StringBuilder sb = new StringBuilder();
	sb.Append(" DELETE ");

	sb.Append(" FROM [").Append(dataSourceInformation.DataSourceName).Append("] ");

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

Det är det som behövs för att deleta en rad från databasen utifrån ett objekt vi har.

Medlem sedan maj 20012 812 inlägg
#24

Nu är det faktiskt så att i ovanstående exempel så krävde vi att objektet fanns, vilket betyder att om vi står med identifier i handen, så måste vi antingen hämta objektet från databasen och sedan ta bort det ;) eller skapa ett nytt objekt och sätta värdet för identifier och sedan ta bort det objektet. För att göra vårt liv lite enklare så väljer vi den andra metoden men vi låter vår entitymapper lösa problem till oss.

public void Delete<ItemType>(Identifier identifier) 
{
    ItemType itemType = Activator.CreateInstance<ItemType>();
    SetIdentifier<ItemType>(itemType, identifier);

    Delete<ItemType>(itemType);
}

Enkelt va!

Visst skulle det gå att fixa det hela utan att behöv skapa objektet, men för att återanvända så mycket kod som möjligt (förenklar när man ändra saker och ting), så är det en billig kompromiss, men tanke på att de flesta Entitys inte gör något speciellt när de skall instanseras, om det är så att de skall göra en massa tunga saker, så bör man antingen ändra på det, eller se till så att man kan delete datan utan att behöva instansera objekt. Det går genom att leta fram DataIdentifierAttributet, plocka fram fältnamnet och sedan plocka fram DataFieldAttrubutet för detta fält och vips så har du löst det utan att instansera ditt objekt...

Men nu så valde jag att återanvända min kod från den andra Delete funktionen och då blir man tvungen att använda metoden SetIdentifier() som sätter identifier värdet till oss i vår nya instans, och som vanligt så spelar det ingen roll vilken typ vi skickar in, bara vi har satt DataIdentifierAttributet och DataFieldAttributet på fältet i klassen och det är av typen Identifier.

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 Exception("No fields found");

    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 Exception("No identifier type found");
}

Så ja nu har vi bara INSERT och UPDATE kvar, de är lite mer jobb med dem... men håll ut de kommer...

- M

Medlem sedan jan. 20022 440 inlägg
#25

Hmm... Nu är jag ute på djupt vatten igen... sitter och kopierar och klistrar in din kod i ett tomt projekt för att testa hur det fungerar.

Tyvärr får jag inte ihop det riktigt, vet fasen inte var jag ska placera allting i för olika projekt/klassfiler... Ett projekt som ser ut på detta viset blir lite mer abstrakt än att göra som jag gjort tidigare så jag stöter på en del problem här men ska nog lyckas klura ut det :)

Medlem sedan dec. 2005664 inlägg
#26

en fråga. Sitter på mobil just nu så det kan bli lite kryptiskt... Om jag har ett domän objekt "user" och har en beskrivande fil som innehåller info om mappning. Hur når jag denna filen utifrån itemtype, Då denna kan ligga i andra projekt etc. Har ingen hjälp installerad :(

Medlem sedan jan. 20022 440 inlägg
#27

Äsch jag blir tokig! Vilket blir lättast och mest överskådligt sätt att organisera allting? Delete, Select, update och insert eller?

Om jag förstått det hela rätt så är det dödssynd att ha mer än en klass i samma fil?

Medlem sedan maj 20012 812 inlägg
#28

Catz skrev:

Äsch jag blir tokig! Vilket blir lättast och mest överskådligt sätt att organisera allting? Delete, Select, update och insert eller?

Om jag förstått det hela rätt så är det dödssynd att ha mer än en klass i samma fil?

Du lägger alla classer i egna filer i ett projekt (utom user som skall ligga i ett eget assenbly, men här i testet går det bra i samma projekt).
Du lägger sedan alla metoder som vi har skapat i EntityMapper klassen. Ditt projekt som innehåller din User skall ha en referens till projektet med EntityMappern. Sedan har du ytterligare ett projekt (din applikation) som skall ha en referens till både EntityMappern och till User projekten, du kan sedan ifrån din Applikation anropa entitymappern för att fylla din User klass.

- M

Medlem sedan maj 20012 812 inlägg
#29

cok skrev:

Om jag har ett domän objekt "user" och har en beskrivande fil som innehåller info om mappning. Hur når jag denna filen utifrån itemtype, Då denna kan ligga i andra projekt etc. Har ingen hjälp installerad

Mest logiskt är att du har en XML-fil någonstans, och att du skickar med sökvägen till denna XML fil i konstruktorn till din EntityMapper, du låter sedan EntityMappern hämta upp XML-filen parsa igenom den efter information som pasar din typ. du kan ju ha en nod som ser ut så här

<EntityMapperMapper>
  <Types>
    <User>
      <DataSource>
          <Name>tblUser</Name>
      <DataSource>
      <DataField>
         <ClassFieldname>_FirstName</ClassFieldName>
          <DataSourceFieldName>fornamn</dataSourceFieldName>
      </DataField>
    <User>
  </Types>
</EntityMapperMapper>

Sedan får du helt enkelt loppa igenom denna xml-nod för att bygga upp de olika informationsklasserna...

- M

Medlem sedan juni 20003 076 inlägg
#30

Gladh, skulle det vara möjligt att använda din mapper mot Linq to sql?
Så att man kör alla select/inserts/updates/delets därigenom.

Jag tycker Linq är riktigt intressant men tycker att microsofts variant av att designa dbml-filerna inte är bra då alla klasser bakas ihop i en lång fil och man tappar kontrollen på klasserna.

Då vore det ultimata att kunna göra egna klasser som vanligt, mappa dem mot databasen genom din mapper, och sedan använda dem genom linq! :)

Medlem sedan dec. 2005664 inlägg
#31

Gladh skrev:

cok skrev:

Om jag har ett domän objekt "user" och har en beskrivande fil som innehåller info om mappning. Hur når jag denna filen utifrån itemtype, Då denna kan ligga i andra projekt etc. Har ingen hjälp installerad

Mest logiskt är att du har en XML-fil någonstans, och att du skickar med sökvägen till denna XML fil i konstruktorn till din EntityMapper, du låter sedan EntityMappern hämta upp XML-filen parsa igenom den efter information som pasar din typ. du kan ju ha en nod som ser ut så här

<EntityMapperMapper>
  <Types>
    <User>
      <DataSource>
          <Name>tblUser</Name>
      <DataSource>
      <DataField>
         <ClassFieldname>_FirstName</ClassFieldName>
          <DataSourceFieldName>fornamn</dataSourceFieldName>
      </DataField>
    <User>
  </Types>
</EntityMapperMapper>

Sedan får du helt enkelt loppa igenom denna xml-nod för att bygga upp de olika informationsklasserna...

- M

Nja. Jag skulle vilja ha det som i nhibernate, en mappfil per klass där mappfilen ligger i samma katalog som objektfilen. Mappfilen har jag koll på men inte hur man kan få tag på sökvägen till objektfilen.

Medlem sedan jan. 20022 440 inlägg
#32

Då har jag i alla fall fått allting att kompilera ok, då återstår att faktiskt hämta datan från db'n och binda den till tex en gridview. Jag gjorde en SQL Databas för enkelthets skull. Vad jag ska skicka för information i själva anropet?

Jag måste ju ange vilken db, tabell osv som vi ska hämta informationen eller delete:a info infrån.

Medlem sedan dec. 2005664 inlägg
#33

det får du se till att den informationen finns i ditt dal.

Medlem sedan maj 20012 812 inlägg
#34

Jag har lagt det så att jag tar emot en connectionstring i entitymapperns konstruktor, och när jag sedan skapar mitt command objekt så sätter jag connectionstringen där. Så här ser det ut, inte riktigt som i koden ovan eftersom den koden som jag vissat ovan är en förkortning och förenkling av hur det ser ut på riktigt hos mig....

        private IDbCommand CreateSqlCommand()
        {
            _Log.TraceEnter();
            try
            {
                //-- Create commandobject
                IDbCommand command = new SqlCommand();
                command.Connection = new SqlConnection(_ConnectionString);
                command.CommandType = CommandType.Text;

                return command as IDbCommand;
            }
            finally
            {
                _Log.TraceLeave();
            }
        }

Och sedan i mitt dal där jag exekverar mitt command så ser det ut så här för metoden som returnerar en datatable.

        private DataTable ExecuteDataTableResult(params IDbCommand[] commands)
        {
            //-- declare variables
            DataTable dataTable = null;

            //-- Loop through all commands and execute, only return the last
            for (int i = 0; i < commands.Length; i++)
                dataTable = internalExecuteDataTableResult(commands[i]);

            //-- Return datatable
            return dataTable;
        }
        private DataTable internalExecuteDataTableResult(IDbCommand command)
        {
            //-- Declara variables
            DataTable dataTable = null;
            bool connectionAlreadyOpen = command.Connection.State == ConnectionState.Open;

            try
            {
                //-- open the connection if needed.
                if (!connectionAlreadyOpen)
                    command.Connection.Open();

                //-- Fill DataTable with data
                dataTable = FillDataTable(command);
            }
            finally
            {
                //-- Clean up after us
                if (!connectionAlreadyOpen)
                    if (command.Connection != null)
                        command.Connection.Close();
            }
            //-- Return datatable
            return dataTable;
        }
        private DataTable FillDataTable(IDbCommand command)
        {
            //-- Declara variables
            DataTable dataTable = new DataTable();

            if (command.GetType() == typeof(SqlCommand))
            {

                SqlDataAdapter dataAdapter = new SqlDataAdapter(command as SqlCommand);
                try
                {
                    dataAdapter.Fill(dataTable);
                }
                finally
                {
                    if(dataAdapter != null)
                        dataAdapter.Dispose();
                }
            }
            else if (command.GetType() == typeof(OleDbCommand))
            {
                OleDbDataAdapter dataAdapter = new OleDbDataAdapter(command as OleDbCommand);
                try
                {
                    dataAdapter.Fill(dataTable);
                }
                finally
                {
                    if (dataAdapter != null)
                        dataAdapter.Dispose();
                }
            }
            else if (command.GetType() == typeof(System.Data.Odbc.OdbcCommand))
            {
                OdbcDataAdapter dataAdapter = new OdbcDataAdapter(command as OdbcCommand);
                try
                {
                    dataAdapter.Fill(dataTable);
                }
                finally
                {
                    if (dataAdapter != null)
                        dataAdapter.Dispose();
                }
            }
            else 
                throw new CommandTypeNotFoundException(string.Format("Couldn't find the correct base type for the IDbCommand, it's type is {0}", command.GetType()));

            //-- return datatable
            return dataTable;
        }

Men bry dig inte om det för det gör nog inte saken enklare....

- M

Medlem sedan maj 20012 812 inlägg
#35

catz skrev:

Jag måste ju ange vilken db, tabell osv som vi ska hämta informationen eller delete:a info infrån.

Det enda så du behöver ange för EntityMappern är connectionStringen så den vet mot vilken databas den skall gå mot.

Tabell och kolumnnamn är ju information som du har satt på ditt Userobjekt så därför behöver du inte ange det någonstans, det är ju det som är det fina i kråksången...

- M

Medlem sedan aug. 20003 575 inlägg
#36

doggelito skrev:

Gladh, skulle det vara möjligt att använda din mapper mot Linq to sql?
Så att man kör alla select/inserts/updates/delets därigenom.

Jag tycker Linq är riktigt intressant men tycker att microsofts variant av att designa dbml-filerna inte är bra då alla klasser bakas ihop i en lång fil och man tappar kontrollen på klasserna.

Då vore det ultimata att kunna göra egna klasser som vanligt, mappa dem mot databasen genom din mapper, och sedan använda dem genom linq! :)

NHibernate kommer i nästa version att stödja LiNQ, det går redan nu att ladda ner det från deras svn :).

Medlem sedan maj 20012 812 inlägg
#37

doggelito skrev:

Gladh, skulle det vara möjligt att använda din mapper mot Linq to sql?
Så att man kör alla select/inserts/updates/delets därigenom.

Nej inte om du inte bygger om en massa, men du skulle ju kunna använda dig av Linq, alltså hämta all information till en lista och använd sedan Linq för att filtrera den, dock ingen lösning som jag rekomenderar...

- M

Medlem sedan maj 20012 812 inlägg
#38

cok skrev:

Nja. Jag skulle vilja ha det som i nhibernate, en mappfil per klass där mappfilen ligger i samma katalog som objektfilen. Mappfilen har jag koll på men inte hur man kan få tag på sökvägen till objektfilen.

Men hjälp av reflections och Assembly klassen, där någonstans så skall sökvägen för var Assemblyn finns placerad på servern vara lagrard.

typ något sånt här:

user.GetType().Assembly.FilePath()

Eller något, du hittar det säkert när du börjar rota i det...

- M

Medlem sedan aug. 20003 575 inlägg
#39

Gladh skrev:

cok skrev:

Nja. Jag skulle vilja ha det som i nhibernate, en mappfil per klass där mappfilen ligger i samma katalog som objektfilen. Mappfilen har jag koll på men inte hur man kan få tag på sökvägen till objektfilen.

Men hjälp av reflections och Assembly klassen, där någonstans så skall sökvägen för var Assemblyn finns placerad på servern vara lagrard.

typ något sånt här:

user.GetType().Assembly.FilePath()

Eller något, du hittar det säkert när du börjar rota i det...

- M

Det är nog ännu smartare att göra XML filen till embedded i dll:en så slipper man tänka på att en xml fil skall med :). Precis som NHibernate :D.

Man kan ha en och samma fil i NHibernate också, men det blir ju mer uppdelat och snyggare med en fil för varje klass :).

Medlem sedan maj 20012 812 inlägg
#40

nickemannen skrev:

Det är nog ännu smartare att göra XML filen till embedded i dll:en så slipper man tänka på att en xml fil skall med . Precis som NHibernate .

Så länge det inte innebär att man skall kompilera om sin assembly om man byter något i XML-filen, för måste man det, så kan man lika gärna ha attributen...

- M

281 ms totalt · 4 externa anrop · v20260731065814-full.86ec41c2
132 ms — deklarationer (db)
0 ms — hämta statistik (cache)
143 ms — hämta tråd, inlägg och bilagor (db)
133 ms — ändringar (db)