对于多个数据库表对应一个Model问题的思考

     最近做项目遇到一个场景,就是客户要求为其下属的每一个分支机构建一个表存储相关数据,而这些表的结构都是一样的,只是分属于不同的机构。这个问题抽象一下就是多个数据库表对应一个Model(或者叫实体类)。有了这个问题,我就开始思考在现有的代码中解决问题,最早数据采集部分是用EF来做数据存储的,我查了一下,资料并不多,问了一下对EF比较熟悉的朋友,得出的结论是EF实现这个功能比较复杂,不易实现。EF不能实现就要去找其他的框架,在PDF.NET的讨论群跟大家讨论这个问题的时候,@深蓝医生说PDF.NET可以支持这个,在医生的指导下,我研究了PDF.NET的源码,确实可以实现这个功能。在PDF.NET的源码中,有一个EntityBase的类,这是所有实体的基础类,该类里面有以下两个方法:

 1          /// <summary>
 2         /// 将实体类的表名称映射到一个新的表名称
 3         /// </summary>
 4         /// <param name="newTableName">新的表名称</param>
 5         /// <returns>是否成功</returns>
 6         public bool MapNewTableName(string newTableName)
 7         {
 8             if (EntityMap == EntityMapType.Table)
 9             {
10                 this.TableName = newTableName;
11                 return true;
12             }
13             return false;
14         }
15 
16         /// <summary>
17         /// 获取表名称。如果实体类有分表策略,那么请重写该方法
18         /// </summary>
19         /// <returns></returns>
20         public virtual string GetTableName()
21         {
22             return _tableName; ;
23         }

     看到这两个方法,大家应该就基本明白了,有了这两个方法就可以很方便的根据需要将同一个实体也就是Model指向不同的表。如果对PDF.NET不了解可能看着比较糊涂,我这里简单的解释一下,在PDF.NET中,实体的就像一个个的表结构,而这个表结构具体属于哪个真实的表是需要通过EntityBase这个基础类提供的TableName属性来设置的,而PDF.NET又支持将实体类通过自己特有的OQL方式拼写成SQL语句再执行,所以,在执行SQL之前,我们可以很方便的通过修改实体类的TableName属性让我们的SQL语句最终指向不同的表,是不是很简单?
     另外,对于一个项目来说,能做到一个Model对应多个表还不够,因为在实际情况下,你是无法预知会有多少表的,即便你已经知道这些表对应的Model只有一个,随着业务的开展,表也在增加。那怎么解决这个问题呢?有了表对应的Model,那用什么方式来动态增加表呢?目前最常用的就是CodeFirst的方式,还好最新版的PDF.NET已经开始支持CodeFirst的方式,不过,我要用的时候发现还不能支持Postgresql的CodeFirst方式,主要问题是主键的自增,大家都知道,Postgresql并不像SQL Server那样原生支持自增主键,要实现Postgresql的自增主键一般是借助于序列,在数据库中新建一个序列,然后自增主键取值于这个序列,思路比较清晰,直接动手改源码

 1 /// <summary>
 2         /// 获取创建表的命令脚本
 3         /// </summary>
 4         public string CreateTableCommand
 5         {
 6             get {
 7                 if (_createTableCommand == null)
 8                 {
 9                     string script = @"
10 CREATE TABLE @TABLENAME(
11 @FIELDS
12 )
13                     ";
14 
15                     if (this.currDb.CurrentDBMSType == PWMIS.Common.DBMSType.PostgreSQL && !string.IsNullOrEmpty(currEntity.IdentityName))
16                     {
17                         string seq =
18                             "CREATE SEQUENCE " + currEntity.TableName + "_" + currEntity.IdentityName + "_" + "seq INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1;";
19 
20                         script = seq + script;
21                     }
22 
23                     var entityFields = EntityFieldsCache.Item(this.currEntity.GetType());
24                     string fieldsText = "";
25                     foreach (string field in this.currEntity.PropertyNames)
26                     {
27                         string columnScript =entityFields.CreateTableColumnScript(this.currDb as AdoHelper, this.currEntity, field);
28                         fieldsText = fieldsText + "," + columnScript+"\r\n";
29                     }
30                     string tableName =this.currDb.GetPreparedSQL("["+ currTableName+"]");
31                     _createTableCommand = script.Replace("@TABLENAME", tableName).Replace("@FIELDS", fieldsText.Substring(1));
32                 }
33                 return _createTableCommand;
34             }
35         }

     我在建表之前,先新建一个序列,新建的表的自增主键引用这个序列即可。
     在修改源码的过程中,我发现了一个问题,如果实体中字段的类型为String,它在表中可能对应char,varchar或者text,怎么解决这个问题呢?思考无果后,我想到EF中对这个的支持很好,那EF中是怎么解决这个问题的呢,翻了半天代码,终于找到了相应的源码,贴出来看看:

  1 // Npgsql.NpgsqlMigrationSqlGenerator
  2 private void AppendColumnType(ColumnModel column, StringBuilder sql, bool setSerial)
  3 {
  4     switch (column.Type)
  5     {
  6     case PrimitiveTypeKind.Binary:
  7         sql.Append("bytea");
  8         return;
  9     case PrimitiveTypeKind.Boolean:
 10         sql.Append("boolean");
 11         return;
 12     case PrimitiveTypeKind.Byte:
 13     case PrimitiveTypeKind.SByte:
 14     case PrimitiveTypeKind.Int16:
 15         if (setSerial)
 16         {
 17             sql.Append(column.IsIdentity ? "serial2" : "int2");
 18             return;
 19         }
 20         sql.Append("int2");
 21         return;
 22     case PrimitiveTypeKind.DateTime:
 23     {
 24         byte? precision = column.Precision;
 25         if ((precision.HasValue ? new int?((int)precision.GetValueOrDefault()) : null).HasValue)
 26         {
 27             sql.Append("timestamp(" + column.Precision + ")");
 28             return;
 29         }
 30         sql.Append("timestamp");
 31         return;
 32     }
 33     case PrimitiveTypeKind.Decimal:
 34     {
 35         byte? precision2 = column.Precision;
 36         if (!(precision2.HasValue ? new int?((int)precision2.GetValueOrDefault()) : null).HasValue)
 37         {
 38             byte? scale = column.Scale;
 39             if (!(scale.HasValue ? new int?((int)scale.GetValueOrDefault()) : null).HasValue)
 40             {
 41                 sql.Append("numeric");
 42                 return;
 43             }
 44         }
 45         sql.Append("numeric(");
 46         sql.Append(column.Precision ?? 19);
 47         sql.Append(',');
 48         sql.Append(column.Scale ?? 4);
 49         sql.Append(')');
 50         return;
 51     }
 52     case PrimitiveTypeKind.Double:
 53         sql.Append("float8");
 54         return;
 55     case PrimitiveTypeKind.Guid:
 56         sql.Append("uuid");
 57         return;
 58     case PrimitiveTypeKind.Single:
 59         sql.Append("float4");
 60         return;
 61     case PrimitiveTypeKind.Int32:
 62         if (setSerial)
 63         {
 64             sql.Append(column.IsIdentity ? "serial4" : "int4");
 65             return;
 66         }
 67         sql.Append("int4");
 68         return;
 69     case PrimitiveTypeKind.Int64:
 70         if (setSerial)
 71         {
 72             sql.Append(column.IsIdentity ? "serial8" : "int8");
 73             return;
 74         }
 75         sql.Append("int8");
 76         return;
 77     case PrimitiveTypeKind.String:
 78         if (column.IsFixedLength.HasValue && column.IsFixedLength.Value && column.MaxLength.HasValue)
 79         {
 80             sql.AppendFormat("char({0})", column.MaxLength.Value);
 81             return;
 82         }
 83         if (column.MaxLength.HasValue)
 84         {
 85             sql.AppendFormat("varchar({0})", column.MaxLength);
 86             return;
 87         }
 88         sql.Append("text");
 89         return;
 90     case PrimitiveTypeKind.Time:
 91     {
 92         byte? precision3 = column.Precision;
 93         if ((precision3.HasValue ? new int?((int)precision3.GetValueOrDefault()) : null).HasValue)
 94         {
 95             sql.Append("interval(");
 96             sql.Append(column.Precision);
 97             sql.Append(')');
 98             return;
 99         }
100         sql.Append("interval");
101         return;
102     }
103     case PrimitiveTypeKind.DateTimeOffset:
104     {
105         byte? precision4 = column.Precision;
106         if ((precision4.HasValue ? new int?((int)precision4.GetValueOrDefault()) : null).HasValue)
107         {
108             sql.Append("timestamptz(");
109             sql.Append(column.Precision);
110             sql.Append(')');
111             return;
112         }
113         sql.Append("timestamptz");
114         return;
115     }
116     case PrimitiveTypeKind.Geometry:
117         sql.Append("point");
118         return;
119     default:
120         throw new ArgumentException("Unhandled column type:" + column.Type);
121     }
122 }

     可能看了这么长的一段源码有点头疼,不知道什么意思,没关系,我们只看需要的部分

 1                 case PrimitiveTypeKind.String:
 2         if (column.IsFixedLength.HasValue && column.IsFixedLength.Value && column.MaxLength.HasValue)
 3         {
 4             sql.AppendFormat("char({0})", column.MaxLength.Value);
 5             return;
 6         }
 7         if (column.MaxLength.HasValue)
 8         {
 9             sql.AppendFormat("varchar({0})", column.MaxLength);
10             return;
11         }
12         sql.Append("text");
13         return;

     很明显这一段的功能是区分char,varchar和text,怎么区分的呢?IsFixedLength,MaxLength是不是很熟悉,对了,这就是EF实体类中字段上的元标记,可惜PDF.NET并不支持元标记,思考了半天,只能用一个折中的办法,代码如下:

 1             if (t == typeof(string))
 2             {
 3                 int length = entity.GetStringFieldSize(field);
 4                 if (length == -1) //实体类未定义属性字段的长度
 5                 {
 6                     string fieldType = "text";
 7                     if (db is SqlServer) //此处要求SqlServer 2005以上,SqlServer2000 不支持
 8                         fieldType = "varchar(max)";
 9                     temp = temp + "[" + field + "] "+fieldType;
10                 }
11                 else
12                 {
13                     temp = temp + "[" + field + "] varchar" + "(" + length + ")";
14                 }
15             }

     PDF.NET虽然不支持元标记,但是它支持给字符串类型的字段设置字段最大长度,所以,这里的解决办法就是如果用户设置了字段长度就用varchar(n)的方式建表,如果没有设置就用text或者varcahr(max)建表。
     说到这里,PDF.NET不光可以解决我的一个Model对应多个表的问题,还可以解决表的动态增加问题。
     开源就是这样,自己动手,丰衣足食!

posted @ 2015-03-03 22:34  我才是银古  阅读(2905)  评论(8编辑  收藏  举报