睡不着,讲讲最近做的一个项目的架构的一部分吧,这是一个项目管理系统,支持动态属性,也就是说一个资料
– 例如“项目”、“任务”就是资料,资料的属性
– 例如“名称”、“时间”都是可以在系统运行时动态增删改的。
本文就讲一讲在.NET和SQL 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 {
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
2 {
3
public Guid Id {
get;
set; }
4
5
public
string BelongsToMaterial {
get;
set; }
6
7
public String Title {
get;
set; }
8
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 };
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 }
维护资料时,使用类似下面的代码就可以给资料创建无限多的属性,可以事先、事后给属性关联元数据,以便定义编辑和显示方式(里面用到一些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 测试简单的条件组合查询()
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
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 ,如需转载请自行联系原作者