網站動態屬性的一個架構


睡不着,講講最近做的一個項目的架構的一部分吧,這是一個項目管理系統,支持動態屬性,也就是說一個資料
例如“項目”、“任務”就是資料,資料的屬性
例如“名稱”、“時間”都是可以在系統運行時動態增刪改的。

本文就講一講在.NETSQL Server里實現動態屬性的方法,雖然演示代碼都是C#,但我相信可以很容易的移植到Java中。

首先定義幾個名詞:

資料 是對於系統最終用戶來說其要維護的數據,例如“項目”、“任務”信息等。

屬性 即資料的一個方面的數據,或者稱作字段,在C#代碼里應該就是一個Property

元數據 是解釋屬性的方式,有時我也會把它稱作元屬性。

屬性和元數據的關系呢,可以參照Excel的實現來理解,好比說我們在一個單元格里輸入了一個數據,實際上我們是輸入了一個字符串,假設是“1”,當我們設置Excel使用“數字”格式呈現時,那用戶在單元格里實際看到的是“1.0”,當我們設置Excel使用“日期”格式呈現時,那用戶在單元格里看到的可能就是“1900-1-1”。這里,字符串“1”就是屬性,而元數據實際上就類似Excel里的格式。

對於資料來說,它只保存一個屬性列表,而屬性有一個外鍵指向定義其格式的元數據,下面是資料、屬性和元數據的C#定義:

 

資料

 

  1   public  class GenericDynamicPropertiesEntity : IDynamicPropertiesTable, ISupportDefaultProperties
  2     {
  3          public GenericDynamicPropertiesEntity()
  4         {
  5             Properties =  new List<Property>();
  6              this.FillDefaultProperties();
  7         }
  8 
  9          public  string Get( string name)
 10         {
 11              var property =  this.Property(name,  false);
 12              if (property !=  null)
 13             {
 14                  return property.Value;
 15             }
 16              else
 17             {
 18                  return  null;
 19             }
 20         }
 21 
 22          public Property Get(MetaProperty meta)
 23         {
 24              var property =  this.Property(meta.Title,  false);
 25              if (property !=  null)
 26             {
 27                  return  this.Property(meta.Title,  false);
 28             }
 29              else
 30             {
 31                  return  null;
 32             }
 33         }
 34          public  void Set( string name,  string value)
 35         {
 36              var property =  this.Property(name,  true);
 37              if (property.Meta.Valid(value))
 38                 property.Value = value;
 39              else
 40                  throw  new InvalidValueException( string.Format( " 字段\"{0}\"的值\"{1}\"無效,字段\"{0}\"的類型是\"{2}\", 期望值的格式是\"{3}\" ",
 41                     name, value, property.Meta.Type, property.Meta.ExpectedFormat));
 42         }
 43 
 44          public  void Set( string name,  double value)
 45         {
 46              var property =  this.Property(name,  true);
 47              if (property.Meta.Valid(value))
 48                 property.Value = value.ToString();
 49              else
 50                  throw  new InvalidValueException( string.Format( " 字段\"{0}\"的值\"{1}\"無效,字段\"{0}\"的類型是\"{2}\", 期望值的格式是\"{3}\" ",
 51                     name, value, property.Meta.Type, property.Meta.ExpectedFormat));
 52         }
 53 
 54          public List<Property> Properties {  getprivate  set; }
 55 
 56         [DataMember]
 57          public Guid Id {  getset; }
 58 
 59          public  static T New<T>()  where T : GenericDynamicPropertiesEntity,  new()
 60         {
 61              return  new T()
 62             {
 63                 Id = Guid.NewGuid()
 64             };
 65         }
 66 
 67          protected  void SetClassValue<T>( string propertyName, T member, T value)
 68         {
 69             member = value;
 70             Set(propertyName, value !=  null ? value.ToJson() :  null);
 71         }
 72 
 73          protected  void SetNullableDateTime<T>( string propertyName, T? member, T? value)  where T :  struct
 74         {
 75             member = value;
 76             Set(propertyName, value.HasValue ? value.Value.ToString() :  null);
 77         }
 78 
 79          protected  void SetDateTime( string propertyName, DateTime member, DateTime value)
 80         {
 81             member = value;
 82             Set(propertyName, value.ToString());
 83         }
 84 
 85          protected  void SetSingle( string propertyName,  float member,  float value)
 86         {
 87             member = value;
 88             Set(propertyName, value);
 89         }
 90 
 91          protected  void SetPrimeValue<T>( string propertyName, T member, T value)  where T :  struct
 92         {
 93             member = value;
 94             Set(propertyName, value.ToString());
 95         }
 96 
 97          protected DateTime? GetNullableDateTime( string propertyName, DateTime? date)
 98         {
 99              if (!date.HasValue)
100             {
101                  var value = Get(propertyName);
102                  if (value !=  null)
103                 {
104                     date = DateTime.Parse(value);
105                 }
106             }
107 
108              return date;
109         }
110 
111          protected  float GetSingle( string propertyName,  float member)
112         {
113              if ( float.IsNaN(member))
114             {
115                  var property =  this.Property(propertyName,  false);
116                  if (property !=  null)
117                 {
118                     member = Single.Parse(property.Value);
119                 }
120             }
121 
122              return member;
123         }
124 
125          protected DateTime GetDateTime( string propertyName, DateTime member)
126         {
127              if (member == DateTime.MinValue)
128             {
129                  var value = Get(propertyName);
130                  if (value !=  null)
131                 {
132                     member = DateTime.Parse(value);
133                      return member;
134                 }
135                  else
136                 {
137                      throw  new PropertyNotFoundException( string.Format( " 在Id為\"{0}\"的對象里找不到名為\"{1}\"的屬性! ", Id, propertyName));
138                 }
139             }
140              else
141             {
142                  return member;
143             }
144         }
145 
146          public DateTime? ClosedDate
147         {
148              get;
149              set;
150         }
151 
152          public DateTime OpenDate
153         {
154              get;
155              set;
156         }
157 
158          public DateTime LastModified
159         {
160              get;
161              set;
162         }
163 
164          public  string Creator
165         {
166              get;
167              set;
168         }
169 
170          public  string LastModifiedBy
171         {
172              get;
173              set;
174         }
175     }

屬性

 

 1      ///   <summary>
 2       ///  資料的屬性
 3       ///   </summary>
 4       public  class Property : ITable
 5     {
 6          ///   <summary>
 7           ///  獲取和設置資料的值
 8           ///   </summary>
 9           ///   <remarks>
10           ///  對於普通類型,例如float等類型直接就保存其ToString的返回結果
11           ///  對於復雜類型,則保存其json格式的對象
12           ///   </remarks>
13           //  TODO: 第二版 - 需要考慮國際化情形下,屬性有多個值的情形!
14           public  string Value {  getset; }
15         
16          ///   <summary>
17           ///  獲取和設置屬性的Id
18           ///   </summary>
19           public Guid Id {  getset; }
20 
21          public MetaProperty Meta {  getset; }
22 
23          ///   <summary>
24           ///  獲取和設置該屬性對應的元數據Id
25           ///   </summary>
26           public Guid MetaId {  getset; }
27 
28          ///   <summary>
29           ///  該屬性對應的資料的編號
30           ///   </summary>
31           public Guid EntityId {  getset; }
32 
33          ///   <summary>
34           ///  獲取和設置該屬性所屬的資料
35           ///   </summary>
36           public GenericDynamicPropertiesEntity Entity {  getset; }
37 }


元數據

 

  1  public  class MetaProperty : INamedTable, ISecret
  2     {
  3          public Guid Id {  getset; }
  4 
  5          public  string BelongsToMaterial {  getset; }
  6 
  7          public String Title {  getset; }
  8 
  9          public  string Type {  getset; }
 10 
 11          public  string DefaultValue {  getprivate  set; }
 12 
 13          ///   <summary>
 14           ///  獲取和設置屬性的權限
 15           ///   </summary>
 16           public  int Permission {  getset; }
 17 
 18          public  virtual  string ExpectedFormat {  get {  return  string.Empty; } }
 19 
 20          public  virtual  bool Valid( string value)
 21         {
 22              return  true;
 23         }
 24 
 25          public  virtual  bool Valid( double value)
 26         {
 27              return  true;
 28         }
 29 
 30          public  static MetaProperty NewString( string name)
 31         {
 32              return  new MetaProperty()
 33             {
 34                 Id = Guid.NewGuid(),
 35                 Title = name,
 36                 Type = Default.MetaProperty.Type.String,
 37                 Permission = Default.Permission.Mask
 38             };
 39         }
 40 
 41          public  static MetaProperty NewNumber( string name,  double defaultValue =  0.0)
 42         {
 43              return  new MetaProperty()
 44             {
 45                 Id = Guid.NewGuid(),
 46                 Title = name,
 47                 Type = Default.MetaProperty.Type.Number,
 48                 Permission = Default.Permission.Mask,
 49                 DefaultValue = defaultValue.ToString()
 50             };
 51         }
 52 
 53          public  static MetaProperty NewAddress( string name)
 54         {
 55              return  new MetaProperty()
 56             {
 57                 Id = Guid.NewGuid(),
 58                 Title = name,
 59                 Type = Default.MetaProperty.Type.Address,
 60                 Permission = Default.Permission.Mask
 61             };
 62         }
 63 
 64          public  static MetaProperty NewRelationship( string name)
 65         {
 66              return  new MetaProperty()
 67             {
 68                 Id = Guid.NewGuid(),
 69                 Title = name,
 70                 Type = Default.MetaProperty.Type.Relationship,
 71                 Permission = Default.Permission.Mask
 72             };
 73         }
 74 
 75          public  static MetaProperty NewDateTime( string name)
 76         {
 77              return  new MetaProperty()
 78             {
 79                 Id = Guid.NewGuid(),
 80                 Title = name,
 81                 Type = Default.MetaProperty.Type.DateTime,
 82                 Permission = Default.Permission.Mask
 83             };
 84         }
 85 
 86          public  static MetaProperty NewDate( string name)
 87         {
 88              return  new MetaProperty()
 89             {
 90                 Id = Guid.NewGuid(),
 91                 Title = name,
 92                 Type = Default.MetaProperty.Type.Date,
 93                 Permission = Default.Permission.Mask
 94             };
 95         }
 96 
 97          public  static MetaProperty NewTime( string name)
 98         {
 99              return  new MetaProperty()
100             {
101                 Id = Guid.NewGuid(),
102                 Title = name,
103                 Type = Default.MetaProperty.Type.Time,
104                 Permission = Default.Permission.Mask
105             };
106         }
107 
108          public  static MetaProperty NewUser( string name)
109         {
110              return  new MetaProperty()
111             {
112                 Id = Guid.NewGuid(),
113                 Title = name,
114                 Type = Default.MetaProperty.Type.User,
115                 Permission = Default.Permission.Mask
116             };
117         }
118 
119          public  static MetaProperty NewUrl( string name)
120         {
121              return  new UrlMetaProperty()
122             {
123                 Id = Guid.NewGuid(),
124                 Title = name,
125                 Type = Default.MetaProperty.Type.Url,
126                 Permission = Default.Permission.Mask
127             };
128         }
129 
130          public  static MetaProperty NewTag( string name)
131         {
132              return  new MetaProperty()
133             {
134                 Id = Guid.NewGuid(),
135                 Title = name,
136                 Type = Default.MetaProperty.Type.Tag,
137                 Permission = Default.Permission.Mask
138             };
139         }
140     }
141 
142      public  class MetaProperties : List<MetaProperty>
143     {
144          public MetaProperty Find( string name)
145         {
146              return  this.SingleOrDefault(p => String.Compare(p.Title, name) ==  0);
147         }
148 }

 

維護資料時,使用類似下面的代碼就可以給資料創建無限多的屬性,可以事先、事后給屬性關聯元數據,以便定義編輯和顯示方式(里面用到一些IocMock):

 1      [TestMethod]
 2          public  void 驗證客戶資料的動態屬性的可行性()
 3         {
 4              var rep =  new MemoryContext();
 5             MemoryMetaSet metas =  new MemoryMetaSet();
 6             metas.Add( typeof(Customer), MetaProperty.NewString( " 姓名 "));
 7             metas.Add( typeof(Customer), MetaProperty.NewNumber( " 年齡 "));
 8             metas.Add( typeof(Customer), MetaProperty.NewAddress( " 地址 "));
 9             metas.Add( typeof(Customer), MetaProperty.NewRelationship( " 同事 "));
10             rep.MetaSet = metas;
11 
12              var builder =  new ContainerBuilder();
13              var mocks =  new Mockery();
14              var user = mocks.NewMock<IUser>();
15             Expect.AtLeastOnce.On(user).GetProperty( " Email ").Will(Return.Value(DEFAULT_USER_EMAIL));
16 
17             builder.RegisterInstance(user).As<IUser>();
18             builder.RegisterInstance(rep).As<IContext>();
19              var back = IocHelper.Container;
20              try
21             {
22                 IocHelper.Container = builder.Build();
23 
24                  var customer = Customer.New<Customer>();
25                 customer.Set( " 姓名 "" XXX ");
26                 customer.Set( " 年齡 "28);
27                 customer.Set( " 地址 "" ZZZZZZZZZZZZZZZZZZZZZZ ");
28 
29                  var colleague = Customer.New<Customer>();
30                 colleague.Set( " 姓名 "" YYY ");
31 
32                  //  對於稍微復雜一點的對象,我們可以用json對象
33                  customer.Set( " 同事 ", Relationship.Colleague(customer, colleague).ToString());
34 
35                 Assert.AreEqual( " XXX ", customer.Get( " 姓名 "));
36             }
37              finally
38             {
39                 IocHelper.Container = back;
40             }
41         }

 

因為動態屬性事先不知道其格式,為了實現搜索功能,無法在編寫程序的時候拼接查詢用的SQL語句,因此我抽象了一層,定義了一個小的查詢語法,寫了一個小小的編譯器將查詢語句轉化成SQL語句,實現了對動態屬性的查詢功能,請看下面的測試用例:

 

 1         [TestMethod]
 2          public  void 測試簡單的條件組合查詢()
 3         {
 4              using ( var context = IocHelper.Container.Resolve<IContext>())
 5             {
 6                  var customer = Customer.New<Customer>();
 7                 customer.Set( " 姓名 "" 測試簡單的條件組合查詢 ");
 8                 customer.Set( " 年齡 "28);
 9                 customer.Set( " 地址 "" 上海市浦東新區 ");
10                 context.Customers.Add(customer);
11                 context.SaveChanges();
12 
13                  var result = context.Customers.Query( " (AND (姓名='測試簡單的條件組合查詢') " +
14                                                       "      (年齡 介於 1 到 30) " +
15                                                       " ) ");
16                 Assert.IsTrue(result.Count() >  0);
17                  var actual = result.First();
18                 Assert.AreEqual( " 測試簡單的條件組合查詢 ", actual.Get( " 姓名 "));
19                 Assert.AreEqual( " 28 ", actual.Get( " 年齡 "));
20             }
21         }

 

上面測試用例里的查詢語句:

(AND (姓名='測試簡單的條件組合查詢') (年齡 介於 1 30) )

經過Query函數的編譯之后,會轉化成下面的兩段SQL語句:

SELECT e. *, p. *  FROM  Properties  AS p  INNER  JOIN  GenericDynamicPropertiesEntities  AS e  ON p.EntityId  = e.Id  INNER  JOIN  MetaSet  AS m  ON p.MetaId  = m.Id   WHERE  (( CASE   WHEN m.Type  =  ' 日期時間型 '  AND ( CONVERT( datetime, p.Value)  = N ' 測試簡單的條件組合查詢 'AND (m.Title  = N ' 姓名 'THEN  1   WHEN m.Type  =  ' 字符串 '  AND (p.Value  = N ' 測試簡單的條件組合查詢 'AND (m.Title  = N ' 姓名 'THEN  1   ELSE  0  END  =  1))

 

SELECT e. *, p. *  FROM  Properties  AS p  INNER  JOIN  GenericDynamicPropertiesEntities  AS e  ON p.EntityId  = e.Id  INNER  JOIN  MetaSet  AS m  ON p.MetaId  = m.Id   WHERE  (( CASE   WHEN m.Type  =  ' 數字型 '  AND ( CONVERT( int, p.Value)  BETWEEN  1  AND  30AND (m.Title  = N ' 年齡 'THEN  1   WHEN m.Type  =  ' 日期時間型 '  AND ( CONVERT( datetime, p.Value)  BETWEEN N ' 1 '  AND N ' 30 'AND (m.Title  = N ' 年齡 'THEN  1   ELSE  0  END  =  1))

 

然后分別執行查詢並在業務層將求解查詢結果的交集,也許是可以直接生成一條SQL語句交給數據庫處理成最精簡的結果再返回的,但是因為開發時間、以及目標客戶的關系,暫時沒有花精力做這個優化。

當然上面的查詢語句寫起來還比較復雜,因此我做了一個界面方便用戶編輯查詢條件,另外對資料屬性的編輯、元數據維護等內容,后面再寫文章說,蚊子太多了…… 



 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM