您的位置:首页 > 运维架构 > 网站架构

网站动态属性的一个架构

2012-07-21 01:38 211 查看
睡不着,讲讲最近做的一个项目的架构的一部分吧,这是一个项目管理系统,支持动态属性,也就是说一个资料
– 例如“项目”、“任务”就是资料,资料的属性
– 例如“名称”、“时间”都是可以在系统运行时动态增删改的。

本文就讲一讲在.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)
{
var value = Get(propertyName);
if (value != null)
{
date = DateTime.Parse(value);
}
}

return date;
}

protected float GetSingle(string propertyName, float member)
{
if (float.IsNaN(member))
{
var property = this.Property(propertyName, false);
if (property != null)
{
member = Single.Parse(property.Value);
}
}

return member;
}

protected DateTime GetDateTime(string propertyName, DateTime member)
{
if (member == DateTime.MinValue)
{
var value = Get(propertyName);
if (value != null)
{
member = DateTime.Parse(value);
return member;
}
else
{
throw new PropertyNotFoundException(string.Format("在Id为\"{0}\"的对象里找不到名为\"{1}\"的属性!", Id, propertyName));
}
}
else
{
return member;
}
}

public DateTime? ClosedDate
{
get;
set;
}

public DateTime OpenDate
{
get;
set;
}

public DateTime LastModified
{
get;
set;
}

public string Creator
{
get;
set;
}

public string LastModifiedBy
{
get;
set;
}
}属性

1 /// <summary>
2 /// 资料的属性
3 /// </summary>
4 public class Property : ITable
5 {
6 /// <summary>
7 /// 获取和设置资料的值
8 /// </summary>
9 /// <remarks>
/// 对于普通类型,例如float等类型直接就保存其ToString的返回结果
/// 对于复杂类型,则保存其json格式的对象
/// </remarks>
// TODO: 第二版 - 需要考虑国际化情形下,属性有多个值的情形!
public string Value { get; set; }

/// <summary>
/// 获取和设置属性的Id
/// </summary>
public Guid Id { get; set; }

public MetaProperty Meta { get; set; }

/// <summary>
/// 获取和设置该属性对应的元数据Id
/// </summary>
public Guid MetaId { get; set; }

/// <summary>
/// 该属性对应的资料的编号
/// </summary>
public Guid EntityId { get; set; }

/// <summary>
/// 获取和设置该属性所属的资料
/// </summary>
public GenericDynamicPropertiesEntity Entity { get; set; }
}
元数据

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()
{
Id = Guid.NewGuid(),
Title = name,
Type = Default.MetaProperty.Type.Time,
Permission = Default.Permission.Mask
};
}

public static MetaProperty NewUser(string name)
{
return new MetaProperty()
{
Id = Guid.NewGuid(),
Title = name,
Type = Default.MetaProperty.Type.User,
Permission = Default.Permission.Mask
};
}

public static MetaProperty NewUrl(string name)
{
return new UrlMetaProperty()
{
Id = Guid.NewGuid(),
Title = name,
Type = Default.MetaProperty.Type.Url,
Permission = Default.Permission.Mask
};
}

public static MetaProperty NewTag(string name)
{
return new MetaProperty()
{
Id = Guid.NewGuid(),
Title = name,
Type = Default.MetaProperty.Type.Tag,
Permission = Default.Permission.Mask
};
}
}

public class MetaProperties : List<MetaProperty>
{
public MetaProperty Find(string name)
{
return this.SingleOrDefault(p => String.Compare(p.Title, name) == 0);
}
}

维护资料时,使用类似下面的代码就可以给资料创建无限多的属性,可以事先、事后给属性关联元数据,以便定义编辑和显示方式(里面用到一些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("同事"));
rep.MetaSet = metas;

var builder = new ContainerBuilder();
var mocks = new Mockery();
var user = mocks.NewMock<IUser>();
Expect.AtLeastOnce.On(user).GetProperty("Email").Will(Return.Value(DEFAULT_USER_EMAIL));

builder.RegisterInstance(user).As<IUser>();
builder.RegisterInstance(rep).As<IContext>();
var back = IocHelper.Container;
try
{
IocHelper.Container = builder.Build();

var customer = Customer.New<Customer>();
customer.Set("姓名", "XXX");
customer.Set("年龄", 28);
customer.Set("地址", "ZZZZZZZZZZZZZZZZZZZZZZ");

var colleague = Customer.New<Customer>();
colleague.Set("姓名", "YYY");

// 对于稍微复杂一点的对象,我们可以用json对象
customer.Set("同事", Relationship.Colleague(customer, colleague).ToString());

Assert.AreEqual("XXX", customer.Get("姓名"));
}
finally
{
IocHelper.Container = back;
}
}

因为动态属性事先不知道其格式,为了实现搜索功能,无法在编写程序的时候拼接查询用的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("地址", "上海市浦东新区");
context.Customers.Add(customer);
context.SaveChanges();

var result = context.Customers.Query("(AND (姓名='测试简单的条件组合查询')" +
" (年龄 介于 1 到 30)" +
")");
Assert.IsTrue(result.Count() > 0);
var actual = result.First();
Assert.AreEqual("测试简单的条件组合查询", actual.Get("姓名"));
Assert.AreEqual("28", actual.Get("年龄"));
}
}

上面测试用例里的查询语句:

(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语句交给数据库处理成最精简的结果再返回的,但是因为开发时间、以及目标客户的关系,暂时没有花精力做这个优化。

当然上面的查询语句写起来还比较复杂,因此我做了一个界面方便用户编辑查询条件,另外对资料属性的编辑、元数据维护等内容,后面再写文章说,蚊子太多了……
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: