對於多個資料庫表對應一個Model問題的思考

我才是銀古發表於2015-03-03

     最近做專案遇到一個場景,就是客戶要求為其下屬的每一個分支機構建一個表儲存相關資料,而這些表的結構都是一樣的,只是分屬於不同的機構。這個問題抽象一下就是多個資料庫表對應一個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對應多個表的問題,還可以解決表的動態增加問題。
     開源就是這樣,自己動手,豐衣足食!

相關文章