65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# 实现 ORM

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (14投票s)

2014年12月4日

GPL3

6分钟阅读

viewsIcon

118624

downloadIcon

1828

从头开始创建对象关系映射

引言

本文旨在向对对象关系映射(ORM)一无所知的人介绍在 .NET Framework 中编写 ORM 的方法。对象关系映射是一种将关系数据库映射到编程语言中的逻辑对象的方式,这有助于减少实现业务流程的代码行数,并为对象提供某种持久性。市面上有很多 ORM 实现,但为什么不自己动手以满足您的需求,而不是使用现成的软件,其中包含您不需要的额外功能,这会带来内存占用和性能的代价。所以我一直在尝试 ORM,并创建了自己的 ORM,但它缺少一样东西——“对象持久性”,我将在稍后实现它,完成后我会发布。

背景

我们如何以一种可以呈现给应用程序对象的方式来表示数据库表和表的关联,或者我们如何通过创建应用程序对象中表示的数据库的逻辑视图来将逻辑对象映射到数据库表?ORM 就是关于这个的。

Using the Code

在我的 ORM 实现的最终结果中有几个类,包括数据库层、DataAccessDataMapperdataStorage 或 Repository,其中最重要的是 Data Models。我无法描述全部代码,但我将重点介绍其中最重要的部分,以提供完整的画面。

首先,我创建了自定义属性,它们的唯一目的是以与数据库中相关表描述类似的方式描述模型,因此每个表都有列名和主键以及与其他表的 1 个或多个关系,而创建的自定义属性描述如下

[DataSource(Name = "Sites_Departments", 
Type = GLOBALS.DataSource.Type.DBTable, AccessMethod = GLOBALS.DataSource.AccessMethod.SingleSource)]
public class SiteDepartment : DataModel
{
     [IsIDField]
     [DbColumn("ID")]
      public int ID { get; set; }
 
     [DbColumn("SiteID")]
      public int SiteID { get; set; }
 
     [DbColumn("DepartmentID")]
      public int DepartmentID { get; set; }
 
     [DataRelation(WithDataModel = typeof(Site), 
     OnDataModelKey = "ID", ThisKey = "SiteID")]
      public Site Site { get; set; }
 
     [DataRelation(WithDataModel = typeof(Department), 
     OnDataModelKey = "ID", ThisKey = "DepartmentID")]
      public Department Department { get; set; }
}
...

如上所示,该类使用数据源进行描述,数据源具有反映表名的名称以及是表还是您原始数据的类型,以及访问类型,它指的是此表是数据库中的单个表,还是有更多类似的表。

数据源名称可以指向 Web 服务 URL 或 CSV 文件,但我尚未实现这部分。

  • IsIDField:描述该字段是否为主键。
  • DBColumn:描述数据库表中列的名称。
  • DataRelation:描述外键关系,需要注意的一点是 WithDataModel 部分应引用现有的类/模型。

如果数据访问类型是分布式的,则需要在名称中设置映射表,该表保存了共享表结构和用法的表的列表,如下所示。

[DataSource(Name = "MonitoringServersInfo", Type = GLOBALS.DataSource.Type.DBTable, 
    AccessMethod = GLOBALS.DataSource.AccessMethod.SingleSource)]

因此,在描述了装饰器之后,应该有一个理解这些装饰器的层,那就是 DataAccess

DataAccess 是一个层,它通过围绕数据源例程创建包装器来将数据源例程抽象化到上层,并将其呈现给 DataMapper,同时还能理解类装饰器。但在那之前,为了让一切看起来更有条理,DataAccess 实现了一个 IDataAccess 接口,该接口看起来如下。

public interface IDataAccess<T> where T : class, new()
{
        /// <summary>
        /// 
        /// </summary>
        /// <param name="dataObject"></param>
        /// <returns></returns>
         int Insert(T dataObject, string dataSourceName = null, 
             GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default);
 
        /// <summary>
        /// Update the data based on a predict expression 
        /// </summary>
        /// <param name="dataObject">Object to be updated</param>
        /// <param name="predicate">Expression<Func<T, 
        /// bool>> predicate specify the expression that should be evaluated</param>
        /// <returns></returns>
         bool Update(T dataObject, string dataSourceName = null, 
              GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default);
 
        /// <summary>
        /// Delete Data from the repository
        /// </summary>
        /// <param name="dataObject">the object you wish to delete</param>
        /// <param name="where">Dictionary<string,object> 
        /// Represents the where part that should be executed</param>
        /// <returns>bool status</returns>
        bool Delete(T dataObject, string dataSourceName = null, 
             GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default);
 
        /// <summary>
        /// 
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        T GetById(long id, string dataSourceName = null, 
          GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default);
 
        /// <summary>
        /// Gets the data from repository 
        /// </summary>
        /// <param name="fields">List<string> 
        /// represents the fields that should be set</param>
        /// <param name="where">Dictionary<string,object> 
        /// Represents the where part that should be executed</param>
        /// <param name="limit">Number of T objects to be populated</param>
        /// <returns>IQueryable<T>  Results</returns>
        IEnumerable<T> Get(Dictionary<string, object>  where, int limit = 25, 
                    string dataSourceName = null, 
                    GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default);
 
        /// <summary>
        /// Gets the data from the repository and filter 
        /// it based on the specified predicate expression
        /// </summary>
        /// <param name="predicate">Expression<Func<T, 
        /// bool>> predicate specify the expression that should be evaluated</param>
        /// <returns>IQueryable<T>  Results</returns>
        IEnumerable<T> Get(Expression<Func<T, bool>> predicate, 
                    string dataSourceName = null, 
                    GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default);
 
        /// <summary>
        /// Get all the data from the Repo
        /// </summary>
        /// <returns></returns>
        IEnumerable<T> GetAll(string dataSourceName = null, 
                    GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default);
 
         #region Native SQL Execute Commands
 
        IEnumerable<T> GetAll(string sql);
        int Insert(string sql); 
        bool Update(string sql); 
        bool Delete(string sql); 
        #endregion
}

正如您所见,IDataAccess 是一个类型化类,它描述了 DataAccess 需要实现的函数,例如 insertupdatedeletegetgetallgetbyid

此接口可以实现为 DB-DataAccess、CSV-DataAccess 和 WS-DataAccess 等。但由于它是类型化的,并且在运行时持有使用它的模型的类型,因此应该有一种方法来提取模型信息。所以我创建了另一个类,它能够做到这一点,并且这个类将从 DataAccess Class 构造函数中调用。该类将解析模型的所有属性,并将它们放入一个模式对象中,该对象将被 DataAccess 读取和理解。

public class DataSourceSchema<T> where T: DataModel, new()
{
        public string DataSourceName { get; set; }
        public Enums.DataSourceType DataSourceType { set; get; }
        public Enums.DataSourceAccessType DataSourceAccessType { get; set; }
 
        public string IDFieldName { set; get; }
 
        public List<DataField> DataFields { get; set; }
  
        /***
        * Private functions.
        */
        /// <summary>
        /// Tries to read the TableName attribute value if it exists; 
        /// if it doesn't it throws and exception
        /// </summary>
        /// <returns>TableName attribute value (string), if exists.</returns>
        private void tryReadDataSourceAttributeValue()
        {
            //Get the table name attribute
            IEnumerable<attribute> dataSourceAtt = 
    typeof(T).GetCustomAttributes(typeof(DataSourceAttribute));

            // This mean that the Class is unstructured Class and 
            // it could be related to table/function or procedure or not.
            if (dataSourceAtt.Count() > 0)
            {
                var dsAttr = ((DataSourceAttribute)dataSourceAtt.First());

                if (dsAttr != null)
                {
                    DataSourceType = dsAttr.Type;
                    DataSourceAccessMethod = dsAttr.AccessMethod;

                    if (false == string.IsNullOrEmpty(dsAttr.Name))
                    {
                        DataSourceName = dsAttr.Name;
                    }
                }
            }
        }
 
        /// <summary>
        /// Tries to read the Class Db Properties, which are the properties marked 
        /// with DbColumn Attribute. It tries to resolve the other attribute values, if they exist, 
        /// otherwise, it assigns the default values.
        /// Write the results to the inner List of DataFields
        /// </summary>
         private void tryReadClassDataFields()
        {
            this.DataFields = new List<DataField>();

            var tableFields = typeof(T)
                .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(property => 
                property.GetCustomAttribute<DbColumnAttribute>() != null)
                .ToList();

            var relationFields = typeof(T)
                .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(property => 
                property.GetCustomAttribute<DataRelationAttribute>() != null)
                .ToList();

            var allClassFields = tableFields.Concat(relationFields).ToList();

            //If no exception was thrown, proceed to processing the class fields
            foreach (var field in allClassFields)
            {
                var newDataField = new DataField();

                newDataField.Name = field.Name;

                if (field.GetCustomAttribute<DbColumnAttribute>() != null)
                {
                    newDataField.TableField = new DbTableField()
                    {
                        ColumnName = field.GetCustomAttribute<DbColumnAttribute>().Name,
                        IsIDField = field.GetCustomAttribute<IsIDFieldAttribute>() 
                        != null ? field.GetCustomAttribute<IsIDFieldattribute>().Status : false,
                        AllowNull = field.GetCustomAttribute<AllowNullAttribute>() 
                        != null ? field.GetCustomAttribute<AllowNullAttribute>().Status : false,
                        AllowIDInsert = field.GetCustomAttribute<AllowIDInsertAttribute>() 
                        != null ? field.GetCustomAttribute<AllowIDInsertAttribute>().Status : false,
                        IsKey = field.GetCustomAttribute<IsKeyAttribute<() 
                        != null ? field.GetCustomAttribute<IsKeyAttribute<().Status : false,
                        FieldType = field.PropertyType
                    };
                }

                if (field.GetCustomAttribute<DataRelationAttribute>() != null)
                {
                    var dataRelationAttribute = field.GetCustomAttribute<DataRelationAttribute>();

                    newDataField.Relation = new DbRelation()
                    {
                        DataField = field.Name,
                        RelationName = dataRelationAttribute.Name,
                        WithDataModel = dataRelationAttribute.WithDataModel,
                        OnDataModelKey = dataRelationAttribute.OnDataModelKey,
                        ThisKey = dataRelationAttribute.ThisKey,
                        RelationType = dataRelationAttribute.RelationType
                    };
                }

                this.DataFields.Add(newDataField);
            }

            //Set the IDFieldName variable to the DbColumn name of the ID.
            if (this.DataFields.Count > 0)
            {
                var field = this.DataFields.Find(item => item.TableField 
                != null && item.TableField.IsIDField == true);

                if (field != null)
                {
                    this.IDFieldName = field.TableField.ColumnName;
                }
            }
        }
 
        public DataSourceSchema()
        {
            try
            {
                tryReadDataSourceAttributeValue();
                tryReadClassDataFields();
            }
            catch (Exception ex)
            {
                throw ex.InnerException;
            }
        }
 
        /***
         * Getters.
         * They support accessing a dynamic version of this object's data
         */
        public string GetDataSourceName()
        {
               return this.DataSourceName;
        }
 
        public GLOBALS.DataSource.Type GetDataSourceType()
        {
               return this.DataSourceType;
        }
 
        public Enums.DataSourceAccessType GetDataSourceAccessType()
        {
               return this.DataSourceAccessType;
        }

        public GLOBALS.DataSource.AccessMethod GetDataSourceAccessMethod()
        {
            return this.DataSourceAccessMethod;
        }
 
        public string GetIDFieldName()
        {
               return this.IDFieldName;
        }
 
        public List<DataField> GetDataFields()
        {
               return this.DataFields;
        } 
}

由于 DataSourceSchema 类是类型化的,它将在运行时提取模型信息/属性,并且类型将从 DataAccess 类传递。

不要跳到 DataAccess,正如之前提到的,DataAccess 实现将充当我之前创建的另一个库的包装器,该库处理数据库的通用 SelectInsertUpdateDelete。您可以编写自己的,这无关紧要,但这个库是我写的,它演变成了能够处理所有类型的 Microsoft SQL 例程,例如从函数中选择,执行存储过程,更新(where)和插入(where),选择(where),以及选择哪些列,返回多少行等等。您可以在项目 zip 文件中找到这个库。它需要一些重构,因为它是这个库的增量开发,但对您而言,它工作得非常好。总之,数据访问是与模型应如何获取、修改、删除数据相关的逻辑表示。您可以将任何非 SQL 数据存储库连接到它,例如 Web 服务、NoSQL、CSV 文件等。

public class DataAccess<T> : IDataAccess<T> where T : DataModel, new()
    {
        private DataSourceSchema<T> Schema;

        private static DBLib DBRoutines = new DBLib();
        private static readonly List<Type> NumericTypes = new List<Type>() 
        { typeof(int), typeof(long), typeof(Int16), typeof(Int32), typeof(Int64) };


        /// <summary>
        /// This is a private function. It is responsible for returning a list of 
        /// the data relations on this data model translated to a list of SqlJoinRelation objects.
        /// </summary>
        /// <returns>List of SqlJoinRelation objects</returns>
        private List<SqlJoinRelation> GetDataRelations()
        {
            //Table Relations Map
            //To be sent to the DB Lib for SQL Query generation
            List<SqlJoinRelation> TableRelationsMap = new List<SqlJoinRelation>();

            //TableRelationsList
            //To be used to looking up the relations and extracting information 
            //from them and copying them into the TableRelationsMap
            List<DbRelation> DbRelationsList = Schema.DataFields.Where
            (field => field.Relation != null).Select<DataField, 
            DbRelation>(field => field.Relation).ToList<DbRelation>();

            //Start processing the list of table relations
            if (DbRelationsList != null && DbRelationsList.Count() > 0)
            {
                //Foreach relation in the relations list, 
                //process it and construct the big TablesRelationsMap
                foreach (var relation in DbRelationsList)
                {
                    //Create a temporary map for this target table relation
                    var joinedTableInfo = new SqlJoinRelation();

                    //Get the data model we're in relation with.
                    Type relationType = relation.WithDataModel;

                    //Build a data source schema for the data model we're in relation with.
                    var generalModelSchemaType = typeof(DataSourceSchema<>);
                    var specialModelSchemaType = generalModelSchemaType.MakeGenericType(relationType);
                    dynamic joinedModelSchema = Activator.CreateInstance(specialModelSchemaType);

                    //Get it's Data Fields.
                    List<DataField> joinedModelFields = joinedModelSchema.GetDataFields();

                    //Get the table column names - exclude the ID field name.
                    List<string> joinedModelTableColumns = joinedModelFields
                        .Where(field => field.TableField != null)
                        .Select<datafield, string="">
                        (field => field.TableField.ColumnName)
                        .ToList<string>();

                    //Get the field that describes the relation key from the target model schema
                    DataField joinedModelKey = joinedModelFields.Find
                    (item => item.TableField != null && item.Name == relation.OnDataModelKey);

                    //Get the field that describes our key on which we are in relation with the target model
                    DataField thisKey = Schema.DataFields.Find
                    (item => item.TableField != null && item.Name == relation.ThisKey);

                    if (thisKey != null && joinedModelKey != null)
                    {
                        //Initialize the temporary map and add it to the original relations map
                        joinedTableInfo.RelationName = relation.RelationName;
                        joinedTableInfo.RelationType = relation.RelationType;
                        joinedTableInfo.MasterTableName = Schema.DataSourceName;
                        joinedTableInfo.MasterTableKey = thisKey.TableField.ColumnName;
                        joinedTableInfo.JoinedTableName = joinedModelSchema.GetDataSourceName();
                        joinedTableInfo.JoinedTableKey = joinedModelKey.TableField.ColumnName;
                        joinedTableInfo.JoinedTableColumns = joinedModelTableColumns;

                        //Add the relation keys to the TableRelationsMap
                        TableRelationsMap.Add(joinedTableInfo);
                    }

                }//end-foreach

            }//end-outer-if

            return TableRelationsMap;
        }

        /**
         * Repository Constructor
         */
        public DataAccess() 
        {
            //Get the Table Name and List of Class Attributes
            try
            {
                //Initialize the schema for the class T
                this.Schema = new DataSourceSchema<T>();
                
                //Check for absent or invalid DataModel 
                //attributes and throw the respective exception if they exist.
                if(string.IsNullOrEmpty(Schema.DataSourceName))
                {
                    throw new NoDataSourceNameException(typeof(T).Name);
                }
                else if(Schema.DataFields.Where
                (item => item.TableField != null).ToList().Count() == 0)
                {
                    throw new NoTableFieldsException(typeof(T).Name);
                }
                else if(string.IsNullOrEmpty(Schema.IDFieldName))
                {
                    throw new NoTableIDFieldException(typeof(T).Name);
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public virtual int Insert(T dataObject, string dataSourceName = null, 
        GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default)
        {
            int rowID = 0;
            string finalDataSourceName = string.Empty;
            Dictionary<string,object> columnsValues = new Dictionary<string,object>();
            

            //
            // Decide the DataSource Name
            if(false == string.IsNullOrEmpty(dataSourceName))
            {
                finalDataSourceName = dataSourceName;
            }
            else if(false == string.IsNullOrEmpty(Schema.DataSourceName))
            {
                finalDataSourceName = Schema.DataSourceName;
            }
            else
            {
                throw new Exception("Insert Error: No Data Source was provided in the " + 
                dataObject.GetType().Name + 
                ". Kindly review the class definition or the data mapper definition.");
            }

            //
            // Process the data object and attempt to insert it into the data source
            if (dataObject != null)
            {
                // Get only the Data Fields from Schema which have TableFields objects
                var objectSchemaFields = Schema.DataFields
                    .Where(field => field.TableField != null)
                    .ToList<DataField>();

                foreach (var field in objectSchemaFields)
                {
                    // Don't insert the ID Field in the Data Source, 
                    // unless it's marked as AllowIDInsert
                    if (field.TableField.IsIDField == true 
                    && field.TableField.AllowIDInsert == false)
                    {
                        continue;
                    }

                    // Get the property value
                    var dataObjectAttr = dataObject.GetType().GetProperty(field.Name);

                    //Continue handling the properties
                    if (field.TableField.AllowNull == false && dataObjectAttr != null)
                    {
                        var dataObjectAttrValue = dataObjectAttr.GetValue(dataObject, null);

                        if (dataObjectAttrValue != null)
                        {
                            //
                            // Only update the int/long values to zeros if they are not foreign keys
                            if (true == NumericTypes.Contains(field.TableField.FieldType))
                            {
                                var value = Convert.ChangeType
                        (dataObjectAttrValue, field.TableField.FieldType);

                                if (Convert.ToInt64(value) <= 0 && field.TableField.IsKey == true)
                                {
                                    //continue;
                                    throw new Exception("The Property " + 
                                    field.TableField.ColumnName + " in the " + 
                                    dataObject.GetType().Name + " Table is a foreign key and 
                                    it is not allowed to be null. Kindly set the property value.");
                                }
                            }
                            
                            columnsValues.Add(field.TableField.ColumnName, 
                            Convert.ChangeType(dataObjectAttrValue, field.TableField.FieldType));
                        }
                        else
                        {
                            throw new Exception("The Property " + 
                            field.TableField.ColumnName + " in the " + 
                            dataObject.GetType().Name + 
                            " Table is not allowed to be null kindly annotate 
                the property with [IsAllowNull]");
                        }
                    }
                    else
                    {
                        var dataObjectAttrValue = dataObjectAttr.GetValue(dataObject, null);

                        if (dataObjectAttrValue != null)
                        {
                            //
                            // Only update the int/long values to zeros if they are not foreign keys
                            if (true == NumericTypes.Contains(field.TableField.FieldType))
                            {
                                var value = Convert.ChangeType
                (dataObjectAttrValue, field.TableField.FieldType);

                                if (Convert.ToInt64(value) <= 0 && field.TableField.IsKey == true)
                                {
                                    continue;
                                }
                            }
                            
                            columnsValues.Add(field.TableField.ColumnName, 
                            Convert.ChangeType(dataObjectAttrValue, field.TableField.FieldType));
                        }
                    }
                    //end-inner-if

                }//end-foreach

                try
                {
                    rowID = DBRoutines.INSERT(tableName: finalDataSourceName, 
                    columnsValues: columnsValues, idFieldName: Schema.IDFieldName);
                }
                catch (Exception ex)
                {
                    throw ex;
                }

            }//end-outer-if

            return rowID;  
        }
        
        public virtual bool Update(T dataObject, string dataSourceName = null, 
        GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default)
        {
            bool status = false;
            string finalDataSourceName = string.Empty;
            Dictionary<string,object> columnsValues = new Dictionary<string,object>();
            Dictionary<string,object> whereConditions = new Dictionary<string,object>();

            //
            // Decide the DataSource Name 
            if (false == string.IsNullOrEmpty(dataSourceName))
            {
                finalDataSourceName = dataSourceName;
            }
            else if (false == string.IsNullOrEmpty(Schema.DataSourceName))
            {
                finalDataSourceName = Schema.DataSourceName;
            }
            else
            {
                throw new Exception("Insert Error: 
                No Data Source was provided in the " + dataObject.GetType().Name + 
                ". Kindly review the class definition or the data mapper definition.");
            }

            //
            // Process the data object and attempt to insert it into the data source
            if (dataObject != null)
            {
                // Get only the Data Fields from Schema which have TableFields objects
                var objectSchemaFields = Schema.DataFields
                    .Where(field => field.TableField != null)
                    .ToList<DataField>();

                foreach (var field in objectSchemaFields)
                {
                    // Get the property value
                    var dataObjectAttr = dataObject.GetType().GetProperty(field.Name);

                    //
                    // Don't update the ID Field in the Data Source, unless it's marked as AllowIDInsert
                    // Add the data object ID into the WHERE CONDITIONS
                    if (field.TableField.IsIDField == true)
                    {
                        var dataObjectAttrValue = dataObjectAttr.GetValue(dataObject, null);

                        //
                        // Put the ID Field in the WHERE CONDITIONS
                        if (dataObjectAttrValue != null)
                        {
                            //
                            // Add the ID Field and Value to the 
                            // Where Conditions if it was not added already!
                            if (false == whereConditions.Keys.Contains(field.TableField.ColumnName))
                            { 
                                whereConditions.Add(field.TableField.ColumnName, 
                                Convert.ChangeType(dataObjectAttrValue, field.TableField.FieldType));
                            }
                        }
                        else
                        {
                            throw new Exception("The Property " + 
                            field.TableField.ColumnName + " in the " + 
                            dataObject.GetType().Name + " Table is not SET! 
                            Kindly please set it to it's original value in order 
                            to decide what data to update accordingly.");
                        }

                        //
                        // DON'T CONTINUE EXECUTION IF THE ID FIELD IS NOT ALLOWED TO BE CHANGED
                        if(false == field.TableField.AllowIDInsert)
                        { 
                            continue;
                        }
                    }

                    // 
                    // Add the data object fields into the COLUMNS-VALUES dictionary
                    // This dictionary contains the values to be updated
                    if (field.TableField.AllowNull == false && dataObjectAttr != null)
                    {
                        var dataObjectAttrValue = dataObjectAttr.GetValue(dataObject, null);

                        if (dataObjectAttrValue != null)
                        {
                            //
                            // Only update the int/long values to zeros if they are not foreign keys
                            if (true == NumericTypes.Contains(field.TableField.FieldType))
                            {
                                var value = Convert.ChangeType
                                    (dataObjectAttrValue, field.TableField.FieldType);

                                if (Convert.ToInt64(value) <= 0 && field.TableField.IsKey == true)
                                {
                                    //continue;
                                    throw new Exception("The Property " + 
                                    field.TableField.ColumnName + " in the " + 
                                    dataObject.GetType().Name + " Table is a foreign key 
                                    and it is not allowed to be null. Kindly set the property value.");
                                }
                            }

                            columnsValues.Add(field.TableField.ColumnName, 
                            Convert.ChangeType(dataObjectAttrValue, field.TableField.FieldType));
                        }
                    }
                    else
                    {
                        var dataObjectAttrValue = dataObjectAttr.GetValue(dataObject, null);

                        if (dataObjectAttrValue != null)
                        {
                            //
                            // Only update the int/long values to zeros if they are not foreign keys
                            if (true == NumericTypes.Contains(field.TableField.FieldType))
                            {
                                var value = Convert.ChangeType
                                 (dataObjectAttrValue, field.TableField.FieldType);

                                if (Convert.ToInt64(value) <= 0 && field.TableField.IsKey == true)
                                {
                                    continue;
                                }
                            }

                            columnsValues.Add(field.TableField.ColumnName, 
                            Convert.ChangeType(dataObjectAttrValue, field.TableField.FieldType));
                        }
                    }
                    //end-inner-if

                }//end-foreach

                try
                {
                    if (0 == whereConditions.Count)
                    {
                        throw new Exception("Update Error: 
                        Cannot update data object unless there is at least one WHERE CONDITION. 
                        Please revise the update procedures on " + dataObject.GetType().Name);
                    }
                    else
                    {
                        status = DBRoutines.UPDATE(tableName: finalDataSourceName, 
                        columnsValues: columnsValues, wherePart: whereConditions);
                    }
                }
                catch (Exception ex)
                {
                    throw ex.InnerException;
                }

            }//end-outer-if

            return status;
        }


        public virtual bool Delete(T dataObject, string dataSourceName = null, 
        GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default)
        {
            long ID = 0;
            string finalDataSourceName = string.Empty;
            Dictionary<string,object> whereConditions = new Dictionary<string,object>();

            DataField IDField;
            string ObjectFieldNameWithIDAttribute = string.Empty;

            //
            // Decide the DataSource Name 
            if (false == string.IsNullOrEmpty(dataSourceName))
            {
                finalDataSourceName = dataSourceName;
            }
            else if (false == string.IsNullOrEmpty(Schema.DataSourceName))
            {
                finalDataSourceName = Schema.DataSourceName;
            }
            else
            {
                throw new Exception("Insert Error: 
                No Data Source was provided in the " + dataObject.GetType().Name + 
                ". Kindly review the class definition or the data mapper definition.");
            }
            
            //
            // Decide the IDField value
            IDField = Schema.DataFields.Find(field => field.TableField != null 
            && field.TableField.IsIDField == true);
            
            if(null == IDField)
            {
                throw new Exception("Delete Error: 
                The Data Model does not have IDField property. 
                Kindly mark the properties of " + typeof(T).Name + " with [IsIDField].");
            }

            //
            // Get the object field that is marked with the IsIDField attribute
            var dataObjectAttr = dataObject.GetType().GetProperty(IDField.Name);

            var dataObjectAttrValue = dataObjectAttr.GetValue(dataObject, null);

            if(dataObjectAttrValue == null)
            {
                throw new Exception("The ID Field's value is to NULL. 
                Kindly set the value of the ID Field for the object of type: " + typeof(T).Name);
            }
            else
            {
                whereConditions.Add(IDField.TableField.ColumnName, 
                Convert.ChangeType(dataObjectAttrValue, IDField.TableField.FieldType));
                return DBRoutines.DELETE
                (tableName: finalDataSourceName, wherePart: whereConditions);

            }//end-inner-if-else
        }


        public virtual T GetById(long id, string dataSourceName = null, 
        GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default)
        {
            DataTable dt = new DataTable();
            string finalDataSourceName = string.Empty;

            int maximumLimit = 1;
            List<string> thisModelTableColumns;
            List<SqlJoinRelation> dataRelations;
            Dictionary<string,object> condition;

            string errorMessage = string.Empty;

            //Get our table columns from the schema
            thisModelTableColumns = Schema.DataFields
                .Where(field => field.TableField != null)
                .Select<DataField, string>(field => field.TableField.ColumnName)
                .ToList<string>();

            //Decide on the Data Source Name
            finalDataSourceName = (string.IsNullOrEmpty(dataSourceName) ? 
            Schema.DataSourceName : dataSourceName);

            //Validate the presence of the ID
            if (id <= 0)
            {
                errorMessage = String.Format("The ID Field is either null or zero. 
                Kindly pass a valid ID. Class name: \"{0}\".", typeof(T).Name);
                throw new Exception(errorMessage);
            }

            //Construct the record ID condition
            condition = new Dictionary<string,object>();
            condition.Add(Schema.IDFieldName, id);

            //Proceed with getting the data
            if (Schema.DataSourceType == GLOBALS.DataSource.Type.DBTable)
            {
                dt = DBRoutines.SELECT(finalDataSourceName, 
                thisModelTableColumns, condition, maximumLimit);
            }

            //It will either return a data table with one row or zero rows
            if (dt.Rows.Count == 0)
            {
                return (T)Activator.CreateInstance(typeof(T));
            }
            else
            {
                return dt.ConvertToList<T>().FirstOrDefault<T>() ?? null;
            }
        }

        public virtual IEnumerable<T> Get(Expression<func<T, 
        bool>> predicate, string dataSourceName = null, 
        GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default)
        {
            DataTable dt = new DataTable();

            if (predicate == null) 
            {
                var errorMessage = string.Format
                ("There is no defined Predicate. {0} ",typeof(T).Name);
                
                throw new Exception(errorMessage);
            }
            else 
            {
                CustomExpressionVisitor ev = new CustomExpressionVisitor();
                
                string whereClause = ev.Translate(predicate);

                if (string.IsNullOrEmpty(dataSourceName))
                {

                    if (string.IsNullOrEmpty(whereClause))
                    {
                        dt = DBRoutines.SELECT(Schema.DataSourceName);
                    }
                    else
                    {
                        dt = DBRoutines.SELECT(Schema.DataSourceName, whereClause);
                    }
                }
                else 
                {
                    if (string.IsNullOrEmpty(whereClause))
                    {
                        dt = DBRoutines.SELECT(dataSourceName);
                    }
                    else
                    {
                        dt = DBRoutines.SELECT(dataSourceName, whereClause);
                    }
                }
            }

            return dt.ConvertToList<T>();
        }


        public virtual IEnumerablet<T> Get(Dictionary<string,object> 
        whereConditions, int limit = 25, string dataSourceName = null, 
        GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default)
        {
            DataTable dt = new DataTable();
            string finalDataSourceName = string.Empty;
            
            List<string> thisModelTableColumns;
            List<SqlJoinRelation> dataRelations;

            string errorMessage = string.Empty;

            //Get our table columns from the schema
            thisModelTableColumns = Schema.DataFields
                .Where(field => field.TableField != null)
                .Select<Datafield, string>(field => field.TableField.ColumnName)
                .ToList<string>();

            //Decide on the Data Source Name
            finalDataSourceName = (string.IsNullOrEmpty(dataSourceName) ? 
            Schema.DataSourceName : dataSourceName);
            
            //Validate the presence of the where conditions
            if (whereConditions == null || whereConditions.Count  == 0)
            {
                errorMessage = String.Format("The \"whereConditions\" 
                parameter is either null or empty. 
                Kindly pass a valid \"whereConditions\" parameter. 
                Class name: \"{0}\".", typeof(T).Name);
                throw new Exception(errorMessage);
            }

            //Proceed with getting the data
            if (Schema.DataSourceType == GLOBALS.DataSource.Type.DBTable)
            {
                dt = DBRoutines.SELECT(finalDataSourceName, 
                thisModelTableColumns, whereConditions, limit);
            }

            return dt.ConvertToList<T>();
        }

        public virtual IEnumerable<T> GetAll(string dataSourceName = null, 
        GLOBALS.DataSource.Type dataSourceType = GLOBALS.DataSource.Type.Default)
        {
            DataTable dt = new DataTable();
            string finalDataSourceName = string.Empty;

            int maximumLimit = 0;
            List<string> thisModelTableColumns;
            Dictionary<string,object> whereConditions = null;
            List<SqlJoinRelation> dataRelations;

            //Get our table columns from the schema
            thisModelTableColumns = Schema.DataFields
                .Where(field => field.TableField != null)
                .Select<DataField, string>(field => field.TableField.ColumnName)
                .ToList<string>();

            //Decide on the Data Source Name
            finalDataSourceName = (string.IsNullOrEmpty(dataSourceName) ? 
            Schema.DataSourceName : dataSourceName);
            
            //Proceed with getting the data
            if (Schema.DataSourceType == GLOBALS.DataSource.Type.DBTable)
            {

                dt = DBRoutines.SELECT
                (finalDataSourceName, thisModelTableColumns, whereConditions, maximumLimit);
            }

            return dt.ConvertToList<T>();
        }

        public virtual IEnumerablet<T> GetAll(string SQL_QUERY)
        {
            DataTable dt = DBRoutines.SELECTFROMSQL(SQL_QUERY);

            return dt.ConvertToList<T>();
        }

        public virtual int Insert(string sql)
        {
            int id = DBRoutines.INSERT(sql);
            
            return id;
        }

        public virtual bool Update(string sql)
        {
            bool status = DBRoutines.UPDATE(sql);

            return status;
        }

        public virtual bool Delete(string sql)
        {
            bool status = DBRoutines.DELETE(sql);

            return status;
        }
    }

从类定义中可以看出,它扩展了 DataModelDataModel 是一个类,它将在稍后处理模型的持久性。由于该类的实现不完整,因此目前我将不包含或讨论它。

DBRoutines 是我之前讨论过的库,它处理所有的 SQL Insert/Update/DeleteSelect

使用 DataAccess,您可以选择发送原生 SQL 命令或发送表达式谓词。

在最后阶段,有 DataMapper,它扩展了 DataAccess<T>,为您提供了添加自己的功能以及 DataAccess 功能的可能性,或者覆盖 DataAccess 函数的行为,如下面的示例。

public class SitesDepartmentsDataMapper : DataAccess<SiteDepartment>
{
        /// <summary>
        /// Given a Site's ID, return the list of its Departments.
        /// </summary>
        /// <param name="SiteID">Site.ID (int)</param>
        /// <returns>List of SiteDepartment objects</returns>
        public List<SiteDepartment> GetDepartmentsForSite(long SiteID)
        {
               Dictionary<string, object> 
               condition = new Dictionary<string,object>();
               condition.Add("SiteID", SiteID);
 
               try
               {
                       return Get(whereConditions: condition, 
                       limit: 0).ToList<SiteDepartment>();
               }
               catch(Exception ex)
               {
                       throw ex.InnerException;
               }
        } 
}

上面的代码只会获取 SiteDepratments 对象,而不会获取 Sites/Departmnets 的关系对象。

要获取与对象的关系对象,您需要像这样调用它。

扩展方法 Include,它接受一个参数数组作为参数,如下所示。请注意,Include 是一个 extensionMethod,您可以在项目下的 *Helpers/DataAccessExtensions* 中找到它。

 return Get(whereConditions: condition, limit: 0).Include
(item=>item.Site,Item=>item.Department).ToList<SitesDepartment>

或者,如果您只想包含站点,则调用方式如下。

 return Get(whereConditions: condition, limit: 0).Include
(item=>item.Site).ToList<SitesDepartment>

最后,将所有 DataMapper 对象添加到 DBStorage,它是一个线程安全的单例类,用于通过集中位置促进对您的对象的访问。

public sealed class DataStorage
{
        /***
         * DataStorage Repositories
         */
        
        public SitesDepartmentsDataMapper Sites = new SitesDepartmentsDataMapper();
        
        /***
         * Singleton implementation with an 
         * attempted thread-safety using double-check locking
         * @source: http://csharpindepth.com/articles/general/singleton.aspx
         */
        // internal datastorage singleton container
        private static DataStorage _instance = null;
 
        // lock for thread-safety laziness
        private static readonly object _mutex = new object();
 
        // empty constructor
        private DataStorage() { }
 
        //The only public method, used to obtain an instance of DataStorage
        public static DataStorage Instance
        {
               get
               {
                       if (_instance == null)
                       {
                               lock(_mutex)
                               {
                                      if (_instance == null)
                                      {
                                              _instance = new DataStorage();
                                      }
                               }
                       }
 
                       return _instance;
               }
        }
}

关注点

我自己在实现 ORM 时遇到的有趣之处在于,我在 .NET 反射和泛型类型中进行了划分,这在一定程度上很有趣,在一定程度上很烦人,因为 C# 是一种强类型语言。

历史

  • 版本 1.0 - > 2014-12-02
使用 C# 实现 ORM - CodeProject - 代码之家
© . All rights reserved.