睡不着,講講最近做的一個項目的架構的一部分吧,這是一個項目管理系統,支援動态屬性,也就是說一個資料
– 例如“項目”、“任務”就是資料,資料的屬性
– 例如“名稱”、“時間”都是可以在系統運作時動态增删改的。
本文就講一講在.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<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 { get; private set; }
55
56 [DataMember]
57 public Guid Id { get; set; }
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 { get; set; }
15
16 /// <summary>
17 /// 擷取和設定屬性的Id
18 /// </summary>
19 public Guid Id { get; set; }
20
21 public MetaProperty Meta { get; set; }
22
23 /// <summary>
24 /// 擷取和設定該屬性對應的中繼資料Id
25 /// </summary>
26 public Guid MetaId { get; set; }
27
28 /// <summary>
29 /// 該屬性對應的資料的編号
30 /// </summary>
31 public Guid EntityId { get; set; }
32
33 /// <summary>
34 /// 擷取和設定該屬性所屬的資料
35 /// </summary>
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 /// <summary>
14 /// 擷取和設定屬性的權限
15 /// </summary>
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<MetaProperty>
143 {
144 public MetaProperty Find(string name)
145 {
146 return this.SingleOrDefault(p => 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<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 測試簡單的條件組合查詢()
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 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 ,如需轉載請自行聯系原作者