最近作項目遇到一個場景,就是客戶要求爲其下屬的每個分支機構建一個表存儲相關數據,而這些表的結構都是同樣的,只是分屬於不一樣的機構。這個問題抽象一下就是多個數據庫表對應一個Model(或者叫實體類)。有了這個問題,我就開始思考在現有的代碼中解決問題,最先數據採集部分是用EF來作數據存儲的,我查了一下,資料並很少,問了一下對EF比較熟悉的朋友,得出的結論是EF實現這個功能比較複雜,不易實現。EF不能實現就要去找其餘的框架,在PDF.NET的討論羣跟你們討論這個問題的時候,@深藍醫生說PDF.NET能夠支持這個,在醫生的指導下,我研究了PDF.NET的源碼,確實能夠實現這個功能。在PDF.NET的源碼中,有一個EntityBase的類,這是全部實體的基礎類,該類裏面有如下兩個方法:sql
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 }
可能看了這麼長的一段源碼有點頭疼,不知道什麼意思,不要緊,咱們只看須要的部分ui
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並不支持元標記,思考了半天,只能用一個折中的辦法,代碼以下:this
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對應多個表的問題,還能夠解決表的動態增長問題。
開源就是這樣,本身動手,豐衣足食!spa