天天看點

網站動态屬性的一個架構

睡不着,講講最近做的一個項目的架構的一部分吧,這是一個項目管理系統,支援動态屬性,也就是說一個資料

– 例如“項目”、“任務”就是資料,資料的屬性

– 例如“名稱”、“時間”都是可以在系統運作時動态增删改的。

本文就講一講在.NET和SQL Server裡實作動态屬性的方法,雖然示範代碼都是C#,但我相信可以很容易的移植到Java中。

首先定義幾個名詞:

資料 – 是對于系統最終使用者來說其要維護的資料,例如“項目”、“任務”資訊等。

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

中繼資料 – 是解釋屬性的方式,有時我也會把它稱作元屬性。

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

對于資料來說,它隻儲存一個屬性清單,而屬性有一個外鍵指向定義其格式的中繼資料,下面是資料、屬性和中繼資料的C#定義:

資料

<a></a>

  1  public class GenericDynamicPropertiesEntity : IDynamicPropertiesTable, ISupportDefaultProperties

  2     {

  3         public GenericDynamicPropertiesEntity()

  4         {

  5             Properties = new List&lt;Property&gt;();

  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&lt;Property&gt; Properties { get; private set; }

 55 

 56         [DataMember]

 57         public Guid Id { get; set; }

 58 

 59         public static T New&lt;T&gt;() where T : GenericDynamicPropertiesEntity, new()

 60         {

 61             return new T()

 62             {

 63                 Id = Guid.NewGuid()

 64             };

 65         }

 66 

 67         protected void SetClassValue&lt;T&gt;(string propertyName, T member, T value)

 68         {

 69             member = value;

 70             Set(propertyName, value != null ? value.ToJson() : null);

 71         }

 72 

 73         protected void SetNullableDateTime&lt;T&gt;(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&lt;T&gt;(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     /// &lt;summary&gt;

 2     /// 資料的屬性

 3     /// &lt;/summary&gt;

 4     public class Property : ITable

 5     {

 6         /// &lt;summary&gt;

 7         /// 擷取和設定資料的值

 8         /// &lt;/summary&gt;

 9         /// &lt;remarks&gt;

10         /// 對于普通類型,例如float等類型直接就儲存其ToString的傳回結果

11         /// 對于複雜類型,則儲存其json格式的對象

12         /// &lt;/remarks&gt;

13         // TODO: 第二版 - 需要考慮國際化情形下,屬性有多個值的情形!

14         public string Value { get; set; }

15         

16         /// &lt;summary&gt;

17         /// 擷取和設定屬性的Id

18         /// &lt;/summary&gt;

19         public Guid Id { get; set; }

20 

21         public MetaProperty Meta { get; set; }

22 

23         /// &lt;summary&gt;

24         /// 擷取和設定該屬性對應的中繼資料Id

25         /// &lt;/summary&gt;

26         public Guid MetaId { get; set; }

27 

28         /// &lt;summary&gt;

29         /// 該屬性對應的資料的編号

30         /// &lt;/summary&gt;

31         public Guid EntityId { get; set; }

32 

33         /// &lt;summary&gt;

34         /// 擷取和設定該屬性所屬的資料

35         /// &lt;/summary&gt;

36         public GenericDynamicPropertiesEntity Entity { get; set; }

37 }

中繼資料

  1 public class MetaProperty : INamedTable, ISecret

  3         public Guid Id { get; set; }

  4 

  5         public string BelongsToMaterial { get; set; }

  6 

  7         public String Title { get; set; }

  9         public string Type { get; set; }

 10 

 11         public string DefaultValue { get; private set; }

 12 

 13         /// &lt;summary&gt;

 14         /// 擷取和設定屬性的權限

 15         /// &lt;/summary&gt;

 16         public int Permission { get; set; }

 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             };

 97         public static MetaProperty NewTime(string name)

 99             return new MetaProperty()

101                 Id = Guid.NewGuid(),

102                 Title = name,

103                 Type = Default.MetaProperty.Type.Time,

104                 Permission = Default.Permission.Mask

105             };

106         }

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&lt;MetaProperty&gt;

143     {

144         public MetaProperty Find(string name)

145         {

146             return this.SingleOrDefault(p =&gt; String.Compare(p.Title, name) == 0);

147         }

148 }

維護資料時,使用類似下面的代碼就可以給資料建立無限多的屬性,可以事先、事後給屬性關聯中繼資料,以便定義編輯和顯示方式(裡面用到一些Ioc和Mock):

 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&lt;IUser&gt;();

15             Expect.AtLeastOnce.On(user).GetProperty("Email").Will(Return.Value(DEFAULT_USER_EMAIL));

16 

17             builder.RegisterInstance(user).As&lt;IUser&gt;();

18             builder.RegisterInstance(rep).As&lt;IContext&gt;();

19             var back = IocHelper.Container;

20             try

21             {

22                 IocHelper.Container = builder.Build();

23 

24                 var customer = Customer.New&lt;Customer&gt;();

25                 customer.Set("姓名", "XXX");

26                 customer.Set("年齡", 28);

27                 customer.Set("位址", "ZZZZZZZZZZZZZZZZZZZZZZ");

28 

29                 var colleague = Customer.New&lt;Customer&gt;();

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 測試簡單的條件組合查詢()

 4             using (var context = IocHelper.Container.Resolve&lt;IContext&gt;())

 5             {

 6                 var customer = Customer.New&lt;Customer&gt;();

 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() &gt; 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 30) AND (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語句交給資料庫處理成最精簡的結果再傳回的,但是因為開發時間、以及目标客戶的關系,暫時沒有花精力做這個優化。

當然上面的查詢語句寫起來還比較複雜,是以我做了一個界面友善使用者編輯查詢條件,另外對資料屬性的編輯、中繼資料維護等内容,後面再寫文章說,蚊子太多了…… 

本文轉自 donjuan 部落格園部落格,原文連結:http://www.cnblogs.com/killmyday/archive/2012/07/21/2601900.html   ,如需轉載請自行聯系原作者