由于公司使用的所有技术都比较倾向于使用原生,不怎么借用其他第三方框架(无论是前端布局,样式,到后台的框架)。公司也算比较小型的没有太大的项目
可以让我们进行团队合作项目,几乎是一人接手一个项目。然后自己从前端到后台的框架搭建,项目制作,到后期发布维护这么一个过程。对于我这种刚刚出来工作的菜鸟来说,
不使用第三方框架就感觉会多出很多冗余的代码,然后不灵活每到新项目来的时候就需要从新根据项目的需求再次搭建,就感觉没必要我们公司是以外包为主,
一些微信端的运营活动,从制作到完成、发布差不多两周作用就需要完成(个别项目除外)。开发项目访问数据库使用的ado .net,用过这玩意的都知道如果不用第三方框架,
就需要在DAL层写大量的SQL语句来进行数据库的操作。虽然不太懂为什么公司不喜欢第三方框架,但毕竟是普通的小员工也只能遵从公司大佬的想法。
然后就出现了自制EF用来减少Sql语句的编写(目前还没实际使用到项目之中,本文章也只是展示目前大概的一个方向,也希望各位博客园的大佬给些意见,不怕被喷只是没有针对技术方面的喷一律不接受,喷也请各位大神用技术来喷我,哈哈哈)
自制框架主要使用的技术:ado .net,反射,自定义属性.....,我还给这个玩意取了一个高端的名字:iShare(装下逼,哈哈哈哈)
下面稍微介绍下这三个东西,用几句简短的话语来介绍下
ado .net:主要用于c#程序访问数据库
Reflection(反射):.Net程序中获取运行时类的信息,让程序员可以在程序运行期间得到类的信息
Attribute(属性):.Net程序运行中可用来获取成员的一些特定行为信息
像我们公司微信运营活动的框架都是基于.net 的MVC,然后三层架构DAL、model,当然现在来说BLL层的一些业务逻辑在Controller(MVC的控制器)中就已经差不多完成了,
就也没必要在分出一个BLL层(也许是因为我们项目都算比较小吧,没必要分的这么细)。我自制的框架与.Net的EF极为相似(也可以说仿制版),
在Model层中对实体需要进行特殊的处理,让model与dal层之间建立起一层约定,这个时候就用到了自定的Attribute(属性),
一直有这么一句话“约定胜于配置”,让model与dal之间建立起约定,按照约定来进行执行操作。
特此我封装了一个特殊的Attribute(HelperProperty),用于约定程序的执行。
自定义属性:HelperProperty
首先来介绍下构造函数与枚举的作用:
1、枚举用来标记该model中的字段主要的作用
Navigation:如果字段上有标记该枚举说明该字段为主键(primary key),获取所有关联该字段的数据
Complex:如果字段上标记有该枚举说明该字段为外键,根据该字段获取他关联表的信息
Ignore:标记该枚举说明在反射过程中不对该字段进行任何操作,直接跳过
2、构造函数:
重载方法一(一个参数):用于特殊情况下,当该表不为主表也不为外键表时使用
重载方法二(两个参数):
PropertyName:用于该字段为外键或者主键时使用(一般存放表名或字段名,根据枚举的值来区分)
EnumValue:它是一个特殊的数组,用来存放该字段的特殊操作标记是否为主表或外键表,存放值为枚举值(PropertyEnum),
使用数组的原因是因为一个字段需要特定的逻辑(特定的逻辑并不是只,它既是外键也是主键,而是说它为一个集合需要它的PropertyName属性,
但不需要它被操作而是被忽略掉)在下方看案例代码在详细介绍。
public class HelperProperty : System.Attribute {
/// <summary>
/// 特定操作
/// </summary>
public enum PropertyEnum
{
//导航属性
Navigation = 1,
//复杂属性
Complex = 2,
//忽略
Ignore = 3
};
/// <summary> /// 表名或特定作用名称 /// </summary> public string _PropertyName; /// <summary> /// 枚举值 /// </summary> public int[] _EnumValue; public HelperProperty(string PropertyName) { _PropertyName = PropertyName; } public HelperProperty(string PropertyName,int[] EnumValue) { _PropertyName = PropertyName; _EnumValue = EnumValue; } }
在介绍完自定义属性后就来一个实际的使用案例,来详细讲解自定义属性的使用,就以班级(Classes)与学生(Students)为例,
显然逻辑就非常简单了,表明该学生属于那个班级,该班级有那些学生。
Classes(班级表)
Id:然每个表都会有一个primary key(主键),这里我使用的是以GUID的格式为主键
ClassesName:班级的名称
Student:跟EF的写法非常相似,在EF中是用virtual虚关键字来标记,用一个studnets类型的集合来存储该班级下的所有学生对象。
[HelperProperty("Student_ClassesId", new int[] { 1,3})]:加上自定的属性,使用的是两个参数的构造函数来初始化,自定义属性的两个字段。
第一个参数:_PropertyName 在这里存放了外键表的表名 + 外键表关联本表的列名
第二个参数:_EnumValue(数组类型) “1”:表示该字段标记为导航属性它为存放主键列信息,"3" : 因为在数据表中没有该列但是它又必须存在,
在进行(insert)数据插入操作时,需要忽略掉该字段不进行操作,就是因为这些特殊的字段的出现才会将枚举值用数组来存放
Students(学生信息表)
Id:主键列
Name:学生姓名
Sex:性别
ClassesId:该学生所属的班级Id
[HelperProperty("Classes", new int[] { 2 })] :参数一:表明该列关联的表名,参数二:表明该列为外键列(复杂属性),
如果字段标有复杂属性,程序则根据该自定义属性的参数一获取关联表的详情信息
Classes:所属的班级详情信息
[HelperProperty("Classes",new int[] { 3 })]:参数一:在这里就为另外一种意义了,上面的ClassesId已经标明为复杂属性,
在这里则为关联(让该字段与上方的字段进行关联,根据ClassesId的属性信息查询出的结果存放在该字段中),
参数二:这个字段在数据表中也不存在则需要忽略掉。
/// <summary> /// 班级信息 /// </summary> public class Classes { /// <summary> /// 班级主键Id /// </summary> public Guid Id { get; set; } /// <summary> /// 班级名称 /// </summary> public string ClassesName { get; set; } /// <summary> /// 该班级下所有的学生信息 /// </summary> [HelperProperty("Student_ClassesId", new int[] { 1,3})] public List<Students> Student { get; set; } } /// <summary> /// 学生信息表 /// </summary> public class Students { /// <summary> /// 学生主键Id /// </summary> public Guid Id { get; set; } /// <summary> /// 学生姓名 /// </summary> public string Name { get; set; } /// <summary> /// 性别 /// </summary> public string Sex { get; set; } /// <summary> /// 所属班级 /// </summary> [HelperProperty("Classes", new int[] { 2 })] public Guid ClassesId { get; set; } /// <summary> /// 所属班级详情信息 /// </summary> [HelperProperty("Classes",new int[] { 3 })] public Classes Classes { get; set; } }
以上就为model层中的约定设置,下面来进行数据访问的封装操作(SqlBasic)。
数据访问类:SqlBasic
在该类中封装了基本的增、删、查、改等操作,减少了一部分的sql语句的编写(暂时没有很好的思路来完成联表查询、特定的条件操作的封装)。
下面就来一一介绍增删查改,以及一些辅助方法。既然是基于反射对类进行方法封装,那就不得不用泛型了,在调用方法后得到操作类,在进行反射得到所有约定信息(自定义属性)。在SqlBasic封装的方法讲解完之后会附加ado .net(增删查改)方法代码
辅助方法一:将字段自定义属性的数组转换为枚举数组,减少多次重复的操作
/// <summary> /// 将字段属性数组转换为枚举数组 /// </summary> /// <param name="arr"></param> /// <returns></returns> public static PropertyEnum[] GetPropertyEnumsByArr(int[] arr) { if (arr.Length == 0) { return null; } //定义枚举数组 PropertyEnum[] enums = new PropertyEnum[arr.Length]; for (int i = 0; i < arr.Length; i++) { //将数组元素依次转换为对应的枚举值 enums[i] = (PropertyEnum)arr[i]; } return enums; }
辅助方法二:Sql参数封装,主要是在数据插入时使用,根据实体对象来进行封装SqlParameter,一般为了防止sql注入等,都会将数据进行参数化封装操作
/// <summary> /// 封装sql参数 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="model">实体对象</param> /// <returns></returns> public static SqlParameter[] SqlParameters<T>(T model) where T : new() { List<SqlParameter> sqls = new List<SqlParameter>();
//根据T反射得到该类的成员信息 foreach (PropertyInfo prop in typeof(T).GetProperties()) {
//检测该字段是否带有定义属性 if (prop.GetCustomAttributes(false).Length > 0) {
//检测该属性是否为HelperProperty
if (prop.GetCustomAttributes(false)[0] is HelperProperty)
{
//将该属性转为helperProprty
HelperProperty helper = (prop.GetCustomAttributes(false)[0]) as HelperProperty;
//将字段标记的int数组转换为枚举数组
PropertyEnum[] enums = GetPropertyEnumsByArr(helper._EnumValue);
//判断该枚举数组中是否存在,忽略值 if (!enums.Contains(PropertyEnum.Ignore)) { //取出与model对应的字段值 sqls.Add(new SqlParameter("@" + prop.Name, prop.GetValue(model))); } } } else { sqls.Add(new SqlParameter("@" + prop.Name, prop.GetValue(model))); } } return sqls.ToArray(); }
Insert(数据插入):根据调用时穿入的实体执行添加操作
/// <summary> /// 数据录入 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="model">数据实体</param> /// <returns></returns> public static int Insert<T>(T model) where T : new() {
//调用参数化封装 SqlParameter[] para = SqlParameters(model);
//根据参数进行sql拼接 string sql = "insert into "+typeof(T).Name+" values("; foreach (SqlParameter parameter in para) { sql+=parameter.ParameterName+","; }
//移除sql语句中的最后一个逗号 sql = sql.Remove(sql.Length - 1, 1) + ")"; //执行添加操作 return SQLHelper.ExcuteNonQuery(sql, para); }
Delete(数据删除):支持多条记录删除,传入需要删除的主键Id
/// <summary> /// 根据Id删除数据(支持多删除) /// </summary> /// <typeparam name="T"></typeparam> /// <param name="Ids">Id数组</param> /// <returns></returns> public static void DeleteInfoById<T>(Guid[] Ids) { string condition = ""; for (int i = 0; i < Ids.Length; i++) { //拼接Id condition += Ids[i] + ","; } //移除最后一个逗号 condition = condition.Remove(condition.Length - 1, 1); string sql = "delete from " + typeof(T).Name + " where ID in (@Id)"; SqlParameter[] parameters = { new SqlParameter("@Id",condition) }; SQLHelper.ExcuteNonQuery(sql, parameters); }
Select(数据查询):
/// <summary> /// 数据查询 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="Total"></param> public static List<T> GetDataSkipTake<T>(string condition) where T : new() { //拼接当前实例表格查询 string sql = "select * from "+typeof(T).Name + "where " + condition; //获取表格数据 List<T> list = DataTableToList<T>(SQLHelper.ExcuteDataTable(sql, null),true) as List<T>; //存储字段关联信息 Dictionary<string, DataRelationHelper> Relation = new Dictionary<string, DataRelationHelper>(); //检测该对象是否存在自定义属性(并且拥有外键关联属性) bool Testing = true; //防止字典中的key值相同 int KeyIndex = 0; foreach (T item in list) { object PrimaryKeyValue = ""; foreach (PropertyInfo prop in typeof(T).GetProperties()) { if (prop.Name == "Id") { //获取对象ID(主键值) PrimaryKeyValue = prop.GetValue(item); } //检测是否存在自定义属性 if (prop.GetCustomAttributes(false).Length > 0) { //自定义属性类型是否为HelperProperty类型 if (prop.GetCustomAttributes(false)[0] is HelperProperty) { HelperProperty helper = (prop.GetCustomAttributes(false)[0]) as HelperProperty; //检测是否有外键关联属性 if (helper._EnumValue.Length!=0) { //将字段自定义属性外键关联转换为枚举数组(一个字段可能存储多个枚举值) PropertyEnum[] property = GetPropertyEnumsByArr(helper._EnumValue); //复杂属性 if (property.Contains(PropertyEnum.Complex)) { //获取关联值 object obj = prop.GetValue(item); //获取关联表名 string FkTabelName = helper._PropertyName; //拼接关联表查询语句 string ComplexSql = "select * from " + FkTabelName + " where Id = @value"; SqlParameter[] para = { new SqlParameter("@value",obj) }; //获取关联表数据 DataTable data = SQLHelper.ExcuteDataTable(ComplexSql, para); //实例化存储关联详情信息 DataRelationHelper DataHelper = new DataRelationHelper(); //赋值关联类型(复杂属性) DataHelper.propertyEnum = PropertyEnum.Complex; //赋值关联数据表 DataHelper.DataTable = data; //存储字典 Relation.Add(FkTabelName+ KeyIndex, DataHelper); } //导航属性 else if (property.Contains(PropertyEnum.Navigation)) { int indexof = helper._PropertyName.IndexOf('_'); //获取关联表名 string FkTabelName = helper._PropertyName.Remove(indexof, helper._PropertyName.Length- indexof); //外键关联字段名 string FkFieldName = helper._PropertyName.Remove(0, indexof+1); //拼接外键查询 string NavigationSql = "select * from " + FkTabelName + " where "+ FkFieldName + " = @value"; SqlParameter[] para = { new SqlParameter("@value",PrimaryKeyValue) }; DataTable data = SQLHelper.ExcuteDataTable(NavigationSql, para); DataRelationHelper DataHelper = new DataRelationHelper(); //赋值关联类型(导航属性) DataHelper.propertyEnum = PropertyEnum.Navigation; DataHelper.DataTable = data; Relation.Add(FkTabelName + KeyIndex, DataHelper); } } } } } //该对象不存在外键关联属性,没必要继续检测 if (!Testing) { break; } KeyIndex++; } //该对象存在外键关联属性 if (Relation.Count != 0) { list = SetRelationModel<T>(Relation, list,KeyIndex); } return list; }
数据查询二次封装,将查询出的结果的所外键关联的数据进行再次查询进行封装一次
/// <summary> /// 第二次分装数据(将关联对象信息赋值对象指定成员) /// </summary> /// <typeparam name="T"></typeparam> /// <param name="Dic">外键关联字段信息<字段名称,(关联属性,DataTable数据)></param> /// <param name="ListModel">第一次分装的数据集合</param> /// <param name="KeyIndex">字典key值下标(防止key值重复)</param> /// <returns></returns> public static List<T> SetRelationModel<T>(Dictionary<string, DataRelationHelper> Dic,List<T> ListModel,int KeyIndex) where T :new() { List<T> list = new List<T>(); for (int i = 0; i < ListModel.Count; i++) { T Tmodel = new T(); foreach (PropertyInfo prop in typeof(T).GetProperties()) { //检测该字段是否存在字典(存在则拥有关联属性) if (Dic.ContainsKey(prop.Name+i)) { //取出字典值 DataRelationHelper data = Dic[prop.Name+i]; //当前字段属性值 Type type = null; //检测是否为复杂属性 if (Dic[prop.Name+i].propertyEnum == PropertyEnum.Complex) { //获取当前字段类型 type = prop.PropertyType; } else { //字段为集合属性(返回泛型类型数组) type = prop.PropertyType.GetGenericArguments()[0]; } //获取方法 MethodInfo DataTableToList = typeof(SqlBasic).GetMethod("DataTableToList"); //指定方法泛型类型 DataTableToList= DataTableToList.MakeGenericMethod(type); object[] obj = new object[2]; //将DataTable行转obj类型 obj[0] = data.DataTable; //检测该属性是否为复杂属性 if (data.propertyEnum == PropertyEnum.Complex) { obj[1] = false; } else if(data.propertyEnum == PropertyEnum.Navigation) { obj[1] = true; } //将反射后的对象信息赋值新对象字段 prop.SetValue(Tmodel, DataTableToList.Invoke(null, obj)); } else { //获取该字段值 object ojb = prop.GetValue(ListModel[i]); //赋值新对象 prop.SetValue(Tmodel, ojb); } } list.Add(Tmodel); } return list; }
调用过这两个查询之后,一个表中的数据关联数据都已经被查询出来了,但是目前没有支持递归深入的查询,
然后就是一个辅助的方法将DataTable转换为list,或者只导出一个成一个model:
/// <summary> /// DataTable 转List集合 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="table">数据表格</param> /// <param name="CheckRelation">返回结果是否为多条(true:实体对象|false:实体list集合 )</param> /// <returns></returns> public static object DataTableToList<T>(DataTable table,bool CheckRelation) where T : new() { List<T> Models = new List<T>(); foreach (DataRow item in table.Rows) { T model = new T(); //得到对象所有属性 foreach (PropertyInfo prop in typeof(T).GetProperties()) { //判断属性是否存在自定义属性 if (prop.GetCustomAttributes(false).Length > 0) { //自定义属性是否为HelperProperty类型 if (prop.GetCustomAttributes(false)[0] is HelperProperty) { //得到自定义属性 HelperProperty helper = prop.GetCustomAttributes(false)[0] as HelperProperty; //将属性自定义int数组成员值转换为枚举数组 PropertyEnum[] property = GetPropertyEnumsByArr(helper._EnumValue); //判断此枚举数组否带忽略 if (!property.Contains(PropertyEnum.Ignore)) { //将该行数据列赋值到对应字段 prop.SetValue(model, item[prop.Name]); } } } else { prop.SetValue(model, item[prop.Name]); } } Models.Add(model); } //返回结果是否为多条 if (CheckRelation) { return Models; } else { return Models.FirstOrDefault(); } }
存储过程的调用:
/// <summary> /// 存储过程执行 /// </summary> /// <param name="strCmd">存储过程名称</param> /// <param name="para">参数(如果有参数为出参,需指定参数类型)</param> /// <param name="OutPutValue">出参</param> public static List<T> ExecProc<T>(string ProcName, SqlParameter[] para,out List<object> OutPutValue) where T :new() { OutPutValue = new List<object>(); //拼接存储过程指令 string strCmd = "exec "+ProcName + GetSqlParameterByStr(para); List<T> TModelList = new List<T>(); using (SqlConnection conn = new SqlConnection(connStr)) { SqlCommand cmd = conn.CreateCommand(); //cmd.CommandType = CommandType.StoredProcedure; cmd.CommandText = strCmd; cmd.Parameters.AddRange(para); SqlDataAdapter da = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); da.Fill(dt); //存储过程表结果 TModelList = DataTableToList<T>(dt); //取出存储过程返回值 foreach (SqlParameter paramenter in cmd.Parameters) { if (paramenter.Direction == ParameterDirection.Output) { OutPutValue.Add(paramenter.Value); } } } return TModelList; } /// <summary> /// 动态参数封装(包括存储过程output指令) /// </summary> /// <param name="sqlParameters"></param> /// <returns></returns> public static string GetSqlParameterByStr(SqlParameter[] sqlParameters) { string cmd = " "; foreach (SqlParameter para in sqlParameters) { if (para.Direction == ParameterDirection.Output) { cmd += para.ParameterName + " output" + ","; } else { cmd += para.ParameterName + ","; } } return cmd.Remove(cmd.Length - 1, 1); }
感慨:
看完之后是不是觉得渣到shi呢,唉。虽然说现在已经存在很多非常棒的开源框架了,我目前制作的一个活动就使用的Dapper,这个框架能实现一些基本的操作,但是在查询方面还是有所欠缺,例如在多表联查的时候还是需要自己来写方法进行联查,反正就这样吧,第一版就这样了。希望自己以后能够把它做好,至少还能真的使用到项目中去。其中有很多东西没有讲的很详细那是因为这个框架是7月份就写好了,只是看见身边跟我同一批毕业工作的朋友最近也开始学博客了,都有自己的东西能够发出来,仔细想想貌自己这一年貌似也学了很多东西,也是时候开始静下心来,把自己学会的东西以博客的形式来表达出来。20岁了,也该给自己定义一大堆的目标了,但还是需要自己一步一个脚印来把这条路走踏实吧。在下篇中我会发布一个成绩查询的项目来演示这个框架,虽然项目黄了。但是让我学会了、微信小程序,理解了IOC、DI注入,自己收获也挺多的。