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