webForumDet fria alternativet

EntityMapper hur svårt kan det vara egentligen?

56 svar · 3 020 visningar · startad av Gladh

GladhMedlem sedan maj 20012 812 inlägg
#1

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 (och tidsvinnande) sätt kan fylla sina objekt med data och sedan spara dessa igen, utan att behöva tänka på SQL eller vilken datakälla som finns under. I avancerade ORMappers typ som NHibernate så finns det lite mer avancerade funktioner att tillgå, men i botten så är det viktiga att spara/hämta data från en databas till ett objekt/lista av objekt.

Så om du sitter i dag med DataSets eller kanske själv mappar ihop din data från databasen till objekt så skall jag här visa hur du kan bygga dig en väldigt väldigt väldigt enkelt mappningslogik som kan ta in vilken typ som helst och fylla den med data och skicka tillbaka en lista av objekt.

Hänger du med?

- M

ZaimanMedlem sedan dec. 20014 239 inlägg
#2

Spännande.. Följer trådens utveckling. (y)

GladhMedlem sedan maj 20012 812 inlägg
#3

Så för att vi skall få mappningen att fungerar så måste vi veta vilka fält i databasen som skall paras ihop med vilka variabler i vår klass. Detta löser man med metadata, alltså data om datat!!

Det absolut bästa sättet är att man lägger all sån här metadata utanför själv klasserna i en xml-fil, så man kan ändra på detta utan att behöva kompilera om sina klasser, i detta enkla exempel så kommer jag istället visa hur man kan göra det med attribute som man sätter på sina variabler.

För att göra det så måste man börja med att skapa ett attribute som vi kan sätta på alla våra variabler. Här kommer koden för det.

    [Serializable]
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    public class DataFieldAttribute : System.Attribute
    {
        #region -- Declaration
        private string _FieldName;
        private DataFieldOperationEnum _DataFieldOperation = DataFieldOperationEnum.Read | DataFieldOperationEnum.Write;
        #endregion

        #region -- Properties
        public string FieldName
        {
            get { return _FieldName; }
        }
        public DataFieldOperationEnum DataFieldOperation
        {
            get { return _DataFieldOperation; }
        }
        #endregion

        #region -- Constructor
        public DataFieldAttribute(string fieldName)
        {
            _FieldName = fieldName;
        }
        public DataFieldAttribute(string fieldName, DataFieldOperationEnum dataFieldOperation)
        {
            _FieldName = fieldName;
            _DataFieldOperation = dataFieldOperation;
        }
        #endregion
    }

    [Flags()]
    public enum DataFieldOperationEnum
    {
        Read = 1,
        Write = 2
    }

Här har vi nu skapat ett attribut som vi kan sätta på våra variabler i våra klasser som kommer beskriva i vilket fält i databasen som vår information finns tillgänglig, vi kan även sätta om datan bara skall användas vid läsa eller skriv eller båda.

om vi tar en klass så kommer den se ut så här.

public class User{

   [DataField("UserId")]
   public long Identifier;
   [DataField("Fornamn")]
   public string FirstName;
   [DataField("Lastname")]
   public string LastName; 
}

Som ni ser så kan namnet på fältet i databasen heta något helt annat än vad vår variabel heter.

Klart så långt? Bra det kommer mer...

- M

GladhMedlem sedan maj 20012 812 inlägg
#4

Så ja, nu har vi möjlighet att berätta vilka fält i databasen som hör ihop med vilka variabler. En sak att komma ihåg att det inte alls behöver vara något fält i databasen och information kan komma från flera olika tabeller. Det löser vi så fall med Views/SP i databasen och sätter namnet på "fältet i databasen" själv när vi skapar frågan, här kommer ett exempel.

SELECT UserID, FirstName as Fornamn, MiddleName + ' ' + LastName as LastName
From User

Även fast vi i vår User tabell har namnet FirstName på en kolumn så i vår fråga till databasen så ändrar vi namnet på kolumnen som vi returnerar till "Fornamn" vilket matchar det som vi skrev att "kolumnen" skulle heta, dåligt exempel? Javisst, men jag ville visa på möjligheterna...

- M

GladhMedlem sedan maj 20012 812 inlägg
#5

Vi går vidare...

Nu lämnar vi sparande av objekten till databasen en liten stund (det fattas lite för att lösa det) och fokuserar på att mappa data från en datatable till mitt objekt.

Vi skulle klara oss utmärkt med den information som finns som den är i klassen, men då vi även vill kunna spara så är det bra med lite mer information, och det vi vill göra är att samla all information om variablen i en ny klass och användas av denna klass för själva mappning, dessutom så underlättar det om vi bestämer oss för att byta ut våra attribute mot en xml-fil. Vi slipper då ändra så mycket i koden, bara ändra hur våra DataFieldInformations skapas.

För den uppgiften så har jag gjort en DataFieldInformation klass som skall hjälpa oss.

    [Serializable]
    internal class DataFieldInformation
    {
        #region -- Declaration
        private string _DataFieldName;
        private string _ClassFieldName;
        private object _Value;
        private Type _Type;
        private DataFieldOperationEnum _DataFieldOperation;
        #endregion

        #region -- Properties
        public string DataFieldName {
            get { return _DataFieldName; }
        }
        public string ClassFieldName {
            get { return _ClassFieldName; }
        }
        public object Value{
            get { return _Value; }
        }
        public Type Type{
            get { return _Type; }
        }
        public DataFieldOperationEnum DataFieldOperation
        {
            get { return _DataFieldOperation; }
            set { _DataFieldOperation = value; }
        }
        #endregion

        #region -- Constructor
        public DataFieldInformation() { }
        public DataFieldInformation(string dataFieldName, string classFieldName, object value, Type type, DataFieldOperationEnum dataFieldOperationEnum)
        {
            _ClassFieldName = classFieldName;
            _DataFieldName = dataFieldName;
            _Type = type;
            _Value = value;
            _DataFieldOperation = dataFieldOperationEnum;
        }
        #endregion
    }

En kort beskrivning:

DataFieldName = namnet på "kolumnen" i databasen
ClassFieldname = namnet på variablen i klassen
Type = variablens typ
Value = värdet som finns i variablen
DataFieldOperation = Om denna information skall användas vid läsning eller skrivning eller både och.

Vi måste sedan kunna samla ihop denna information på något vis, och det är inte speciellt svårt. Kom nu ihåg att vi vill göra våra metoder så generella som möjligt, så det är Generics som gäller från och med nu...

Här kommer en metod för att samla ihop den information som vi behöver.

        private List<DataFieldInformation> CreateDataFieldInformation<ItemType>(ItemType itemType) 
        {
                //-- Create List
                List<DataFieldInformation> dataFieldInformationList = new List<DataFieldInformation>();

                //-- Get all fieldInfors for the ItemType
                FieldInfo[] fieldInfos = itemType.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

                //-- Loop through all fieldinfos and create dataFieldInformation
                for (int i = 0; i < fieldInfos.Length; i++)
                {
                    //-- Check if this field has an DataFieldAttribute on it
                    DataFieldAttribute[] dataFieldAttributes = (DataFieldAttribute[])fieldInfos[i].GetCustomAttributes(typeof(DataFieldAttribute), true);
                    if (dataFieldAttributes == null || dataFieldAttributes.Length == 0)
                        continue;

                    //-- Create a MapperDataField
                    DataFieldInformation dataFieldInformation = new DataFieldInformation(
                        dataFieldAttributes[0].FieldName
                        , fieldInfos[i].Name
                        , fieldInfos[i].GetValue(itemType)
                        , fieldInfos[i].FieldType
                        , dataFieldAttributes[0].DataFieldOperation);

                    //-- Add to list
                    dataFieldInformationList.Add(dataFieldInformation);
                }

                //-- return datafieldInformationslist
                return dataFieldInformationList;
        }

Det här kommer fungerar bra så länge vi har en instans av vårt "ItemType" men det har vi ju inte när vi skall läsa information till databasen, så vi måste skapa en instans av vårt ItemType och de kan du inte göra med new ItemType() eftersom ItemType skulle kunna vara en valueType istället för en referensType. Hur löser man det då? Då använder man reflextions och Activator.CreateInstance<>()..

        private List<DataFieldInformation> CreateDataFieldInformation<ItemType>() 
        {
            return CreateDataFieldInformation(Activator.CreateInstance<ItemType>();
        }

Så ja, nu fungerar det även om vi inte har en instans av klassen som vi vill fylla med data.

Nu börjar vi komma någonstans.... Gött!

- M

GladhMedlem sedan maj 20012 812 inlägg
#6

Om vi nu fortsätter med själva mappningsdelen, de flesta kan ju hämta data från databasen och få tillbaka den som en datatable, så vi väntar med den delen ett tag...

Vi har redan här kommit halva vägen för att skapa vår generella mappnings rutin. Nu skall vi bara få datat i vår datatable in i vår klass...

Här kommer de 2 metoder som löser det åt oss....

Vi börjar med att se till så vi loopar igenom alla rader i vårt datatable och sedan kallar vi på metoden som mappar datan till vårt objekt.

        private List<ItemType> MapDataTableToList<ItemType>(DataTable dataTable, List<DataFieldInformation> dataFieldInformationList) 
        {
                //-- Create list
                List<ItemType> itemTypeList = new List<ItemType>();

                //-- Create items in list
                foreach (DataRow dataRow in dataTable.Rows)
                    itemTypeList.Add(MapDataRowToItemType<ItemType>(dataTable.Columns, dataRow, dataFieldInformationList));

                //-- return list of items
                return itemTypeList;
        }

Och så den intressant delen

        private ItemType MapDataRowToItemType<ItemType>(DataColumnCollection dataColumnCollection, DataRow dataRow, List<DataFieldInformation> dataFieldInformationList) 
        {
                //-- Create a new ItemType
                ItemType itemType = Activator.CreateInstance<ItemType>();

                //-- Loop through the dataFieldInformation list and get alla datafields
                foreach (DataFieldInformation dataField in dataFieldInformationList)
                {
                    //-- Check so this field as in one of the columns
                    if (!dataColumnCollection.Contains(dataField.DataFieldName))
                        continue;

                    //-- Get field of entity
                    FieldInfo fieldInfo = itemType.GetType().GetField(dataField.ClassFieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

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

                //-- Raise event that entity has been created
                //OnEntityCreated(new EntityCreatedEventArgs(itemType));

                //-- return itemType
                return itemType;
            }
        }

Här är den del som fyller vårt objekt med data från datatablen! Löjligt enkel va! Jag har även visat på att man här kan kasta ett event att objektet är skapat om man nu skulle vilja göra något speciellt precis efter det är skapat...

Nu är det bara dags att sy ihop det hela...

- M

GladhMedlem sedan maj 20012 812 inlägg
#7

Så ja... sista punkten (på ett litet tag iallafall...)

Nu skall vi bara sy ihop det så att vi får ett enkelt sätt att fixa det på.

Vi skapar en klass som vi kallar något fyndigt, EntityMapper Kanske! ;)

public class EntityMapper{

	#region -- Public Methods
	public List<ItemType> MapToEntity<ItemType>(DataTable dataTable)
	{
		//-- Create the DataFieldInformationList
		List<DataFieldInformation> dataFieldInformationList = CreateDataFieldInformation<ItemType>();
		
        //-- Convert dataTable to TSLis<ItemType>
		List<ItemType> itemTypeList = MapDataTableToCollection<ItemType>(dataTable, dataFieldInformationList);		
		
		//-- return itemTypeList
		return itemTypeList;
	}
	#endregion
	
	#region -- Private Methods
	private List<ItemType> MapDataTableToList<ItemType>(DataTable dataTable, List<DataFieldInformation> dataFieldInformationList) {...}
	private ItemType MapDataRowToItemType<ItemType>(DataColumnCollection dataColumnCollection, DataRow dataRow, List<DataFieldInformation> dataFieldInformationList) {...}
	
	private List<DataFieldInformation> CreateDataFieldInformation<ItemType>() {...}
	private List<DataFieldInformation> CreateDataFieldInformation<ItemType>(ItemType itemType) {...}
	#endregion
}

public class DataFieldAttribute : System.Attribute {...}
internal class DataFieldInformation {...}

Här har ni nu världens enklaste och minst funktionella entitymapper. Som kan ta emot en dataTable och mappa den till vilket objekt som helst. Nu är koden inte testad utan mest Copy-pastad så jag hoppas att det fungerar som det skall, annars är det nog inga större problem att fixa till det.

Vi tar en lite paus här och fortsätter efter jul med att vår EntityMapper kan hämta data själv från databasen så vi slipper skicka in ett datatable till den...

Håll ut...

- M

GladhMedlem sedan maj 20012 812 inlägg
#8

Ooppsss...

Jag upptäckte att man inte kan använda default(ItemType) operatorn, utan man måste använd:

Activator.CreateInstance<ItemType>();

istället...

- M

doggelitoMedlem sedan juni 20003 076 inlägg
#9

Väntar med spänning på fortsättningen! :bire

CatZMedlem sedan jan. 20022 440 inlägg
#10

Jo... samma här! Jag prenumrerar på tråden... han sa ju efter jul så vi får la vänta tills efter nyåret kan jag tro :)

GladhMedlem sedan maj 20012 812 inlägg
#11

Då tänkte jag fortsätta med att visa hur vi kan slippa att skapa dess command objekt som behövs, vår låter helt enkelt koden skapa dessa command objekt till oss utifrån vår klass.

För att vi skall kunna vet varifrån som vår klass skall hämta sin data, alltså vilken tabell/view/SP som skall användas, så måste vi beskriva det, återigen så vill jag påpeka att denna information bör ligga i en separat fil och inte i koden som jag visar här.

För att beskriva var ifrån vi skall hämta informationen så skapar vi ett nytt attribut som vi sätter på själva klassen och som vi kallar DataSourceAttribute.

{
    [Serializable]
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class DataSourceAttribute : System.Attribute
    {
        #region -- Declaration
        private string _DataSourceName;
        private DataTargetEnum _DataTarget;
        #endregion

        #region -- Properties
        public string DataSourceName
        {
            get { return _DataSourceName; }
        }
        public DataTargetEnum DataTarget
        {
            get { return _DataTarget; }
        }
        #endregion

        #region -- Constructor
        public DataSourceAttribute(string dataSourceName)
        {
            _DataSourceName = dataSourceName;
            _DataTarget = DataTargetEnum.Table;
        }
        public DataSourceAttribute(string dataSourceName,  DataTargetEnum dataTarget)
        {
            _DataSourceName = dataSourceName;
            _DataTarget = dataTarget;
        }
        #endregion
    }

Med hjälp av detta atributt kan vi nu bygga ut vår user klass lite.

[DataSource("User")]
public class User{

   [DataField("UserId")]
   public long Identifier;
   [DataField("Fornamn")]
   public string FirstName;
   [DataField("Lastname")]
   public string LastName; 
}

Vi har nu beskrivit i klassen att data skall hämtas från tabellen User.

Precis som innan så väljer vi att först spara undan denna information i en speciell "holder" klass så känns det enklare att bygga ut och förändra vår entitymapper senare.

Vi skall alltså spara undan information om vår DataSource i en DataSourceInformation klass.

    [Serializable]
    internal class DataSourceInformation
    {
        #region -- Declaration
        private string _DataSourceName;
        private DataTargetEnum _DataTarget;
        #endregion

        #region -- Properties
        public string DataSourceName
        {
            get { return _DataSourceName; }
        }
        public DataTargetEnum DataSourceType
        {
            get { return _DataTarget; }
        }
        #endregion

        #region -- Constructor
        public DataSourceInformation() { }
        public DataSourceInformation(string dataSourceName, DataTargetEnum dataTarget)
        {
            _DataSourceName = dataSourceName;
            _DataTarget= dataTarget;
        }
        #endregion
    }

Vi har nu en speciell klass som innehåller information om var informationen finns att hämta. Nu skall vi bara fylla den med information också...

private DataSourceInformation CreateDataSourceInformation<ItemType>() 
{
        //-- Get DataSourceAttributes for this class
        DataSourceAttribute[] dataSourceAttributes = (DataSourceAttribute[])typeof(ItemType).GetCustomAttributes(typeof(DataSourceAttribute), true);

        //-- Check so we have an datasourceattribute
        if (dataSourceAttributes == null || dataSourceAttributes.Length == 0)
            throw new Exception();

        //-- Create DataSourceInformation and return it.
        DataSourceInformation dataSourceInformation = new DataSourceInformation(dataSourceAttributes[0].DataSourceName, dataSourceAttributes[0].DataTarget);

        return dataSourceInformation;
}

- M

GladhMedlem sedan maj 20012 812 inlägg
#12

Nu börjar det ta form här. Och vi skapar en Load metod som kan de emot vilket objekt som helst och sedan fylla det med data från databasen.

public List<ItemType> Load<ItemType>()
{
    //-- declare variables
    IDbCommand command = null;
    DataTable dataTable = null;

    try
    {
        //-- Create information objects
        List<DataFieldInformation> dataFieldInformationList = CreateDataFieldInformation<ItemType>();
        DataSourceInformation dataSourceInformation = CreateDataSourceInformation<ItemType>();

       return MapDataTableToCollection<ItemType>(dataTable, dataFieldInformationList);	
    }
    finally
    {
        if (dataTable != null)
            dataTable.Dispose();
        if (command != null)
            command.Dispose();
    }
}

Så det som fattas nu är att skapa vårt command objekt och exekverar det så vi får ett dataTable tillbaka. Det skall vi göra härnäst.

Vi har all information vi behöver för att skapa command objektet. I detta exempel så utgår jag ifrån en SQLServer och att vi kommer att exekvera en SELECT statement, ingen SPså jag kommer alltså skapa ett SqlCommand objekt med commandType = CommandType.Text, men för de som vill så kan man använda sig av information i DataSource för att bygga ihop andra lösningar.

        private IDbCommand CreateSelectCommand(List<DataFieldInformation> dataFieldInformationList, DataSourceInformation dataSourceInformation)
        {
	        //-- Create commandobject
            IDbCommand command = new SqlCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = SELECT(dataFieldInformationList, dataSourceInformation);
            
            //-- returnerar command object.
            return command;
        }

Också koden för att bygga själva SELECT satsen:

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

    StringBuilder sb = new StringBuilder();
    sb.Append(" SELECT ");

    //-- Include all DataField into the select statement
    for (int i = 0; i < dataFieldInformationList.Count; i++)
    {
        if (i != 0)
            sb.Append(", ");
        sb.Append("[").Append(dataSourceInformation.DataSourceName).Append("].");
        sb.Append("[").Append(dataFieldInformationList[i].DataFieldName).Append("]");
    }

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

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

Så nu har vi kod för att kunna skapa en select-sats som läser ut all information från en tabell och gör om det till en lista av objekt. Vi skall bara lägga till den sista kodraderna i Load-metoden.

.
.
.
        //-- Create information objects
        List<DataFieldInformation> dataFieldInformationList = CreateDataFieldInformation<ItemType>();
        DataSourceInformation dataSourceInformation = CreateDataSourceInformation<ItemType>();

       command = CreateSelectCommand(DataFieldInformationList, dataSourceInformation);
        
       dataTable = //-- Här kallar du på din klass som sköter själva exekveringen av command objektet till en datatable, ditt DAL helt enkelt.

       return MapDataTableToCollection<ItemType>(dataTable, dataFieldInformationList);

- M

GladhMedlem sedan maj 20012 812 inlägg
#13

En lite bit på vägen men kanske inte jätte användbart då det inte alltid är så att vi vill ha ut hela tabellen utan bara kanske 1 post eller en lite delmängd. Så vi skapar en ny Load-metod som kan ta emot ett WHERE-statement. För enkelhetens skull så kommer detta bara vara en vanlig textsträng, men egentligen så bör man skapa egna klasser för att hantera detta så man inte kan skicka in vad som helst. Men i detta exempel så skickar vi bara in textsträngen, i vanliga fall så skickar man in det speciella objekt och sedan görs de/dem objektet/objekten om till WHEREStatement. Det enda vi behöver göra om är att vi måste fixa en överlagrade funktion för vår CreateSelectCommand() som tar emot en sträng på slutet...

public List<ItemType> Load<ItemType>(string whereStatement)
{
.
.
.
command = CreateSelectCommand(dataFieldInformationList, dataSourceInformation, whereStatement);
.
.
.
}

Och så blir det till att skapa den överlagrade metoden

        private IDbCommand CreateSelectCommand(List<DataFieldInformation> dataFieldInformationList, DataSourceInformation dataSourceInformation, string whereStatement)
        {
			StringBuilder sb = new StringBuilder();
			
	        //-- Create commandobject
            IDbCommand command = new SqlCommand();
            command.CommandType = CommandType.Text;
            sb.Append(SELECT(dataFieldInformationList, dataSourceInformation));
            
			if(!string.IsNullOrEmpty(whereStatement))
				sb.Append(" WHERE ").Append(whereStatement);            
            
            //-- returnerar command object.
            command.CommandText = sb.ToString();
            return command;
        }

Så ja, nu har vi metoder för att hämta alla objekt från en tabell och dessutom hämta en delmängd objekt från listan.

- M

GladhMedlem sedan maj 20012 812 inlägg
#14

Ofta vill man ju bara hämta ett objekt baserat på dess identifier. För att lösa det så skapar men en ny Load-method och skickar med en identifier och få tillbaka ett objekt. Nu brukar dessutom identifier's vara olika, det kan vara stängar och det kan var longs eller Guid's...

Det måste man ta höjd för när man skall skapa SQL satserna, jag löste det genom att helt enkelt skapa mig en Identifier klass som kan hålla alla dessa olika typer...

	[Serializable]
	public class Identifier : IComparable, ICloneable
    {
        #region -- Declaration
        private string _Identifier;
        private IdentifierSaveActionEnum _IdentifierSaveAction = IdentifierSaveActionEnum.Update;
        [NonSerialized]
		private Type _Type;
		#endregion

		#region -- Properties
        public IdentifierSaveActionEnum IdentifierSaveAction
        {
            get { return _IdentifierSaveAction; }
        }
		public string Value
		{
			get { return _Identifier; }
			set {_Identifier = value; }
		}
        [XmlIgnore]
        public Type Type
        {
            get { return _Type; }
            set { _Type = value; }
        }
		#endregion

		#region -- Overrided methods
		public override string ToString()
		{
			return _Identifier;
		}
		#endregion

		#region -- Operation overload
		public static implicit operator long(Identifier identifier)
		{
			if (identifier == null || identifier._Identifier == null)
				return 0;

			return Int64.Parse(identifier._Identifier);
		}
		public static implicit operator Guid(Identifier identifier)
		{
			if (identifier == null || identifier._Identifier == null)
				return Guid.Empty;

			return new Guid(identifier._Identifier);
		}
		public static implicit operator Identifier(Int32 identifier)
		{
			return new Identifier((Int64)identifier);
		}
		public static implicit operator Identifier(Int64 identifier)
		{
			return new Identifier(identifier);
		}
		public static implicit operator Identifier(Guid identifier)
		{
			return new Identifier(identifier);
		}

        public static bool operator ==(Identifier id1, Identifier id2)
        {
            if (Object.ReferenceEquals(id1, null) && Object.ReferenceEquals(id2, null))
                return true;
            if(Object.ReferenceEquals(id1, null) || Object.ReferenceEquals(id2, null))
                return false;
            return id1.Value == id2.Value;
        }
        public static bool operator !=(Identifier id1, Identifier id2)
        {
            return !(id1 == id2);
        }
		#endregion

		#region -- Private methods
		private object Clone()
		{
			//-- Create a clone of this Identifier;
			Identifier identifier = new Identifier();
			identifier._Identifier = this._Identifier;
			identifier._Type = this._Type;

			return identifier;
		}
        private void SetIdentifier(object identifier)
        {
            this.Value = identifier.ToString();
            this.Type = identifier.GetType();
            
            //-- Get type
            if (this.Type == typeof(Guid))
                this.Type = typeof(Guid);
            else if (this.Type == typeof(Int16) || this.Type == typeof(Int32) || this.Type == typeof(Int64))
                this.Type = typeof(Int64);
            else
            {
                //-- Cant decide by checking type, do trail error
                //-- Check for sequenze identifier
                long tmpIdentifier;
                if (long.TryParse(identifier.ToString(), out tmpIdentifier))
                {
                    this.Type = typeof(Int64);
                    return;
                }

                //-- Check for unique identifier
                try
                {
                    new Guid(identifier.ToString());
                    this.Type = typeof(Guid);
                    return;
                }
                catch (System.Exception exception) { }
                
                //-- Throw exception since we can't convert from this type
                throw new NotASupportedIdentifierTypeException();
            }
            
        }
        private void SetIdentifier(object identifier, IdentifierTypeEnum identifierType)
        {
            //-- Set Type
            switch (identifierType)
            {
                case IdentifierTypeEnum.Sequenze:
                    this.Type = typeof(Int64);
                    break;
                case IdentifierTypeEnum.UniqueIdentifier:
                    this.Type = typeof(Guid);
                    break;
            }

            //-- Set value
            this.Value = (identifier == null ? null : identifier.ToString());
        }
		#endregion

		#region -- Constructor
		public Identifier() { }
		public Identifier(object identifier)
		{
            SetIdentifier(identifier);
		}
        public Identifier(object identifier, IdentifierSaveActionEnum identifierSaveAction)
        {
            _IdentifierSaveAction = identifierSaveAction;
            SetIdentifier(identifier);
        }
		public Identifier(object identifier, IdentifierTypeEnum identifierType)
		{
            SetIdentifier(identifier, identifierType);
		}
        public Identifier(object identifier, IdentifierTypeEnum identifierType, IdentifierSaveActionEnum identifierSaveAction)
        {
            _IdentifierSaveAction = identifierSaveAction;
            SetIdentifier(identifier, identifierType);
        }
        #endregion

		#region -- IComparable Members
		int IComparable.CompareTo(object obj)
		{
			return _Identifier.CompareTo(((Identifier)obj)._Identifier);
		}
		#endregion

		#region -- ICloneable Members
		object ICloneable.Clone()
		{
			return Clone();
		}
		#endregion
	}

    public enum IdentifierSaveActionEnum
    {
        Insert,
        Update
    }

Här finns en lite sak som bör påpekas och det är IdentifierSaveActionEnum. Det kan vara bra att ha möjligheten att skapa identifiern ute på klienten istället för att göra det i databasen, speciellt i situationer när man kanske vill använda asynkrona lösningar där du inte kan vänta på att du får tillbaka ett svar från databasen. Man bör dock inte använda long/int som identifier i databasen så utan GUID's. Så skall du aldrig kunna få 2 likadanan nycklar..

I allafall så behöver vi nu skriva om vår user klass lite.

[DataSource("User")]
public class User{

   [DataField("UserId")]
   public Identifier Identifier;
   [DataField("Fornamn")]
   public string FirstName;
   [DataField("Lastname")]
   public string LastName; 
}

Vi skall även beskriva vilket fält det är som är identifier fältet genom ett attribute.

    [Serializable]
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    public class DataIdentifierAttribute : System.Attribute
    {
        #region -- Declaration
        private IdentifierTypeEnum _IdentfierType;
        #endregion

        #region -- Properties
        public IdentifierTypeEnum IdentifierType
        {
            get { return _IdentfierType; }
        }
        #endregion

        #region -- Constructor
        public DataIdentifierAttribute(IdentifierTypeEnum identifierType)
        {
            _IdentfierType = identifierType;
        }
        #endregion
    }

	public enum IdentifierTypeEnum
	{
		Sequenze,
		UniqueIdentifier
	}

Så vår User klass nu ser ut så här:

[DataSource("User")]
public class User{

   [DataIdentifier(IdentifierTypeEnum.Sequenze)]
   [DataField("UserId")]
   public Identifier Identifier;
   [DataField("Fornamn")]
   public string FirstName;
   [DataField("Lastname")]
   public string LastName; 
}

- M

GladhMedlem sedan maj 20012 812 inlägg
#15

Och som alla andra gånger så skapar vi ett DataIdentifierInformation objekt som kan hålla informationen om vår identifier

    [Serializable]
    internal class DataIdentifierInformation
    {
        #region -- Declaration
        private IdentifierTypeEnum _IdentifierType;
        private IdentifierSaveActionEnum _IdentifierSaveAction;
        private string _DataFieldName;
        private string _ClassName;
        private Type _Type;
        private object _Value;
        #endregion

        #region -- Properties
        public IdentifierTypeEnum IdentifierType
        {
            get { return _IdentifierType; }
        }
        public IdentifierSaveActionEnum IdentifierSaveAction
        {
            get { return _IdentifierSaveAction; }
        }
        public string DataFieldName
        {
            get { return _DataFieldName; }
        }
        public string ClassFieldName
        {
            get { return _ClassName; }
        }
        public Type Type
        {
            get { return _Type; }
        }
        public object Value
        {
            get { return _Value; }
        }
        #endregion

        #region -- Constructor
        public DataIdentifierInformation() { }
        public DataIdentifierInformation(string dataFieldName, string className, IdentifierTypeEnum identifierType, IdentifierSaveActionEnum identifierSaveAction, Type type)
        {
            _DataFieldName = dataFieldName;
            _ClassName = className;
            _IdentifierType = identifierType;
            _IdentifierSaveAction = identifierSaveAction;
            _Type = type;

            if (_IdentifierType == IdentifierTypeEnum.UniqueIdentifier)
                _Value = new Identifier(Guid.NewGuid());
        }
        public DataIdentifierInformation(string dataFieldName, string className, IdentifierTypeEnum identifierType, IdentifierSaveActionEnum identifierSaveAction, Type type, object value)
        {
            _DataFieldName = dataFieldName;
            _ClassName = className;
            _IdentifierType = identifierType;
            _IdentifierSaveAction = identifierSaveAction;
            _Type = type;
            _Value = value;
        }
        #endregion
    }

Och så metoden som plockar fram denna information från vår Userklass.

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

    if (fieldInfos == null || fieldInfos.Length == 0)
        throw new Exception();

    //-- Create object
    DataIdentifierInformation dataIdentifierInformation = null;

    //-- Loop through all fieldinfo and get DataFieldInfo from them
    for (int i = 0; i < fieldInfos.Length; i++)
    {
        //-- Check if this field has an DataFieldAttribute on it
        DataIdentifierAttribute[] dataIdentifierAttribute = (DataIdentifierAttribute[])fieldInfos[i].GetCustomAttributes(typeof(DataIdentifierAttribute), true);
        if (dataIdentifierAttribute == null || dataIdentifierAttribute.Length == 0)
            continue;

        //-- Get the datafield attribute for this field
        DataFieldAttribute[] dataFieldAttributes = (DataFieldAttribute[])fieldInfos[i].GetCustomAttributes(typeof(DataFieldAttribute), true);
        if (dataFieldAttributes == null || dataFieldAttributes.Length == 0)
            throw new Exception("No DataFieldAttirbute defined for memberfield that has DataIdentifierAttribute defined");
        if (dataFieldAttributes.Length > 1)
            throw new Exception("Multiple DataFieldAttirbute defined for memeberfield that has DataIdentifierAttribute");

        //-- Create DataIdentifierInformation
        //-- When we have supplied an identifier that we want to use
        if(identifier != null)
            dataIdentifierInformation = new DataIdentifierInformation(
                    dataFieldAttributes[0].FieldName
                    , fieldInfos[i].Name
                    , dataIdentifierAttribute[0].IdentifierType
                    , identifier.IdentifierSaveAction
                    , identifier.Type
                    , identifier);

        //-- No supplied identifier, but the itemType has an identifier that has a value
        else if (fieldInfos[i].GetValue(itemType) != null)
            dataIdentifierInformation = new DataIdentifierInformation(
                    dataFieldAttributes[0].FieldName
                    , fieldInfos[i].Name
                    , dataIdentifierAttribute[0].IdentifierType
                    , ((Identifier)fieldInfos[i].GetValue(itemType)).IdentifierSaveAction
                    , ((Identifier)fieldInfos[i].GetValue(itemType)).Type
                    , fieldInfos[i].GetValue(itemType));

            //-- No supplied identifier and the itemtype has no value in it's identifier
        else
        {
            dataIdentifierInformation = new DataIdentifierInformation(
                dataFieldAttributes[0].FieldName
                , fieldInfos[i].Name
                , dataIdentifierAttribute[0].IdentifierType
                , IdentifierSaveActionEnum.Insert
                , fieldInfos[i].FieldType);

            //-- if the identifier is uniqueidentifier we need to set the identifiers value 
            if (dataIdentifierInformation.IdentifierType == IdentifierTypeEnum.UniqueIdentifier)
                SetIdentifier<ItemType>(itemType, dataIdentifierInformation.Value);
        }
        break;
    }

    return dataIdentifierInformation;
}

Nu kan vi alltså skapa dataIdentifierInformation från vår klass som vi kan använda när vi vill hämta ut bara ett objekt från databasen...

Så vi skapar ytterligare en LoadMetod

public ItemType Load<ItemType>(Identifier identifier)
{
    //-- declare variables
    IDbCommand command = null;
    DataTable dataTable = null;

    try
    {
        //-- Create information objects
        .
        .
        .
        DataIdentifierInformation dataIdentifierInformation = CreateDataIdentifierInformation<ItemType>(Activator.CreateInstance<ItemType>(), identifier);

        //-- Create Command
        command = CreateSelectCommand(dataFieldInformationList, dataSourceInformation, dataIdentifierInformation);
        .
        .
        .
        
        //-- Convert dataTable to TSLis<ItemType>
        List<ItemType> itemTypeList = MapDataTableToCollection<ItemType>(dataTable, dataFieldInformationList);

        //-- return itemtypes
        if (itemTypeList == null || itemTypeList.Count == 0)
            return default(ItemType);
        else
            return itemTypeList[0];
    }
    finally
    {
        if (dataTable != null)
            dataTable.Dispose();
        if (command != null)
            command.Dispose();
    }
}

Här har vi nu ytterligare en ny CreateSelectCommand Method som vi måste hantera och vi har dessutom lagt till en sista kodnutt som kommer att returnera ett objekt eller default värdet för detta objekt (som nästa alltid kommer att vara null).

Då tittar vi lite på vår CreateSelectCommand(). Vad vi kan göra här är att antigen skapa en ny överlagrad method, eller så utökar vi bara metoden som vi hade innan med whereStatmententet och så skickar vi med NULL som whereStatement så här.

private IDbCommand CreateSelectCommand(List<DataFieldInformation> dataFieldInformationList, DataSourceInformation dataSourceInformation, DataIdentifierInformation dataIdentifierInformation, string whereStatement)
{
    //-- Create commandobject
    StringBuilder sb = new StringBuilder()
    string tmpWhereStatement = string.Empty;
    IDbCommand command = CreateSqlCommand();
    command.CommandText = SELECT(dataFieldInformationList, dataSourceInformation);

    //-- Check if this selectcommand shall have a where statement based on identifier field...
    if (dataIdentifierInformation != null)
    {
        //-- Adds the where sqlstatement to the command text
        tmpWhereStatement = WHERE(dataIdentifierInformation);

        //-- Create parameterlist for dataIdentifierInformation
        foreach (IDataParameter parameter in CreateParameterList(dataIdentifierInformation))
            command.Parameters.Add(parameter);
    }
    
    //-- Check for where statement
    if(!string.IsNullOrEmpty(tmpWhereStatement)
    {
        if(!string.IsNullOrEmpty(whereStatement))
            sb.Append(tmpWhereStatement).Append(" AND ").Append(whereStatement);
        else
            sb.Append(tmpWhereStatement);
    }else
			if(!string.IsNullOrEmpty(whereStatement))
				sb.Append(" WHERE ").Append(whereStatement);   

    //-- returnerar command object.
    command.CommandText = sb.ToString();
    return command;
}

Då fick vi helt plötsligt 2 nya metoder som vi inte har sett innan. WHERE() och CreateParameterList och de ser ut så här.

private string WHERE(DataIdentifierInformation dataIdentifierInformation)
{
    StringBuilder sb = new StringBuilder();
    sb.Append(" WHERE ");

    sb.Append("[").Append(dataIdentifierInformation.DataFieldName).Append("] = ");
    sb.Append("@_").Append(dataIdentifierInformation.ClassFieldName);

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

- M

GladhMedlem sedan maj 20012 812 inlägg
#16
private List<IDataParameter> CreateParameterList(DataIdentifierInformation dataIdentifierInformation)
{
    //-- Create parameter list
    List<IDataParameter> parameterList = new List<IDataParameter>();

    if (dataIdentifierInformation == null)
        return parameterList;

    //-- Create a parameter for each datafield;
    SqlParameter parameter = new SqlParameter();  //-- Denna måste ändras om databasen inte är en SQL databas...
    parameter.IsNullable = true;
    parameter.ParameterName = "@_" + dataIdentifierInformation.ClassFieldName;
    parameter.Direction = ParameterDirection.Input;

    if (((Identifier)dataIdentifierInformation.Value).Type == typeof(Int64))
        parameter.Value = (long)(Identifier)dataIdentifierInformation.Value;
    if (((Identifier)dataIdentifierInformation.Value).Type == typeof(Guid))
        parameter.Value = (Guid)(Identifier)dataIdentifierInformation.Value;
    if (((Identifier)dataIdentifierInformation.Value).Type == typeof(String))
        parameter.Value = ((Identifier)dataIdentifierInformation.Value).ToString();

    //-- Add parameter to list
    parameterList.Add(parameter);

    //-- return the list of parameters
    return parameterList;
}

Det bör vara det hela för att lyckas med att en rad, en delmängd rader eller alla rader i en tabell till en lista av objekt i din applikation...

Lite mer kod och lite rörigare denna gång, men det skall ni nog ordna upp...

- M

CatZMedlem sedan jan. 20022 440 inlägg
#17

Oerhört mycket tack för dina kodexempel och dina enkla men lättbegripliga förklaringar! Visst blr det lite rörigt men äntligen så har man ett kodexempel att studera med kommentarer till. Det borde inte vara särskilt svårt att tillämpa detta rakt av.

Ser fint ut på riktigt och borde gå att återanvända till 100%, det enda jag inte var med på riktigt var det där om guid och varför man skulle använda det. Är det för att det inte ska bli kollisioner mellan ID'n från olika tabeller eller vad skulle det annars kunna vara?

cokMedlem sedan dec. 2005664 inlägg
#18

Är det inte lite dumt att använda en egen klass (Identifier) som "nyckel objekt" på domänklassen? Känns som man knyter upp sig lite i onödan.

GladhMedlem sedan maj 20012 812 inlägg
#19

Catz skrev:

Är det för att det inte ska bli kollisioner mellan ID'n från olika tabeller eller vad skulle det annars kunna vara?

Det finns tillfällen där man måste spara ner information från en klient till en server där klienten kan vara offline, men du måste använda dig av ditt objekts id, direkt fast du ligger offline mot servern. I de tillfällen så skapar du ditt objekt och sätter ID, och för att vara på det säkra att ingen annan skall få samma id så använd man GUID istället för INT.

Ett exempel kan vara om du har ett butikssystem i flera olika butiker och så har du servern stående på kontoret, om en butik tar sin uppkoppling så måste man ju kunna sälja saker ändå, alltså lägger man ner alla avslutade köp på en kö, och när uppkopplingen kommer igång igen kan dessa avslutade köp skickas till servern, det är ju bara det att id från ditt avslutade köp knanske du behöver direkt i ditt kassa system för att länka ihop med något annant objekt.

- M

GladhMedlem sedan maj 20012 812 inlägg
#20

cok skrev:

Är det inte lite dumt att använda en egen klass (Identifier) som "nyckel objekt" på domänklassen? Känns som man knyter upp sig lite i onödan.

Japp man knyter upp sig mot denna EntityMapper, men det är inte så svårt att ändra eftersom du har DataIdentifierAttributet som pekar ut vilket fält som är din nyckel, och du kan relativt enkelt ändra så det är en int eller guid eller vad som helst...

Men frågan är kanske hur ofta du byter entitymapper, och det finns ju ingen standard för de olika entitymapper, så du knappast gå från en entitymapper till en annan utan att ändra rätt så mycket i din kod, så du har redan bundit dig ganska hårt när du valt din entitymapper, och då spelar det inte så stor roll att har ett Identifier fält....

- M

152 ms totalt · 3 externa anrop · v20260731065814-full.beb2e261
135 ms — hämta forumlista (db)
0 ms — hämta statistik (cache)
148 ms — hämta tråd, inlägg och bilagor (db)