网站动态属性的一个架构
睡不着,讲讲最近做的一个项目的架构的一部分吧,这是一个项目管理系统,支持动态属性,也就是说一个资料 –例如“项目”、“任务”就是资料,资料的属性 –例如“名称”、“时间”都是可以在系统运行时动态增删改的。 本文就讲一讲在.NET和SQL Server里实现动态属性的方法,虽然演示代码都是C#,但我相信可以很容易的移植到Java中。 首先定义几个名词: 资料–是对于系统最终用户来说其要维护的数据,例如“项目”、“任务”信息等。 属性–即资料的一个方面的数据,或者称作字段,在C#代码里应该就是一个Property。 元数据–是解释属性的方式,有时我也会把它称作元属性。 属性和元数据的关系呢,可以参照Excel的实现来理解,好比说我们在一个单元格里输入了一个数据,实际上我们是输入了一个字符串,假设是“1”,当我们设置Excel使用“数字”格式呈现时,那用户在单元格里实际看到的是“1.0”,当我们设置Excel使用“日期”格式呈现时,那用户在单元格里看到的可能就是“1900-1-1”。这里,字符串“1”就是属性,而元数据实际上就类似Excel里的格式。 对于资料来说,它只保存一个属性列表,而属性有一个外键指向定义其格式的元数据,下面是资料、属性和元数据的C#定义: 资料 1 public classGenericDynamicPropertiesEntity:IDynamicPropertiesTable,ISupportDefaultProperties 2{ 3 publicGenericDynamicPropertiesEntity() 4{ 5Properties= newList<Property>(); 6 this.FillDefaultProperties(); 7} 8 9 public stringGet( stringname) 10{ 11 varproperty= this.Property(name, false); 12 if(property!= null) 13{ 14 returnproperty.Value; 15} 16 else 17{ 18 return null; 19} 20} 21 22 publicPropertyGet(MetaPropertymeta) 23{ 24 varproperty= 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 voidSet( stringname, stringvalue) 35{ 36 varproperty= this.Property(name, true); 37 if(property.Meta.Valid(value)) 38property.Value=value; 39 else 40 throw newInvalidValueException( string.Format( " 字段\"{0}\"的值\"{1}\"无效,字段\"{0}\"的类型是\"{2}\",期望值的格式是\"{3}\" ", 41name,value,property.Meta.Type,property.Meta.ExpectedFormat)); 42} 43 44 public voidSet( stringname, doublevalue) 45{ 46 varproperty= this.Property(name, true); 47 if(property.Meta.Valid(value)) 48property.Value=value.ToString(); 49 else 50 throw newInvalidValueException( string.Format( " 字段\"{0}\"的值\"{1}\"无效,字段\"{0}\"的类型是\"{2}\",期望值的格式是\"{3}\" ", 51name,value,property.Meta.Type,property.Meta.ExpectedFormat)); 52} 53 54 publicList<Property>Properties{ get; private set;} 55 56[DataMember] 57 publicGuidId{ get; set;} 58 59 public staticTNew<T>() whereT:GenericDynamicPropertiesEntity, new() 60{ 61 return newT() 62{ 63Id=Guid.NewGuid() 64}; 65} 66 67 protected voidSetClassValue<T>( stringpropertyName,Tmember,Tvalue) 68{ 69member=value; 70Set(propertyName,value!= null?value.ToJson(): null); 71} 72 73 protected voidSetNullableDateTime<T>( stringpropertyName,T?member,T?value) whereT: struct 74{ 75member=value; 76Set(propertyName,value.HasValue?value.Value.ToString(): null); 77} 78 79 protected voidSetDateTime( stringpropertyName,DateTimemember,DateTimevalue) 80{ 81member=value; 82Set(propertyName,value.ToString()); 83} 84 85 protected voidSetSingle( stringpropertyName, floatmember, floatvalue) 86{ 87member=value; 88Set(propertyName,value); 89} 90 91 protected voidSetPrimeValue<T>( stringpropertyName,Tmember,Tvalue) whereT: struct 92{ 93member=value; 94Set(propertyName,value.ToString()); 95} 96 97 protectedDateTime?GetNullableDateTime( stringpropertyName,DateTime?date) 98{ 99 if(!date.HasValue) 100{ 101 varvalue=Get(propertyName); 102 if(value!= null) 103{ 104date=DateTime.Parse(value); 105} 106} 107 108 returndate; 109} 110 111 protected floatGetSingle( stringpropertyName, floatmember) 112{ 113 if( float.IsNaN(member)) 114{ 115 varproperty= this.Property(propertyName, false); 116 if(property!= null) 117{ 118member=Single.Parse(property.Value); 119} 120} 121 122 returnmember; 123} 124 125 protectedDateTimeGetDateTime( stringpropertyName,DateTimemember) 126{ 127 if(member==DateTime.MinValue) 128{ 129 varvalue=Get(propertyName); 130 if(value!= null) 131{ 132member=DateTime.Parse(value); 133 returnmember; 134} 135 else 136{ 137 throw newPropertyNotFoundException( string.Format( " 在Id为\"{0}\"的对象里找不到名为\"{1}\"的属性! ",Id,propertyName)); 138} 139} 140 else 141{ 142 returnmember; 143} 144} 145 146 publicDateTime?ClosedDate 147{ 148 get; 149 set; 150} 151 152 publicDateTimeOpenDate 153{ 154 get; 155 set; 156} 157 158 publicDateTimeLastModified 159{ 160 get; 161 set; 162} 163 164 public stringCreator 165{ 166 get; 167 set; 168} 169 170 public stringLastModifiedBy 171{ 172 get; 173 set; 174} 175} 属性 1 /// <summary> 2 /// 资料的属性 3 /// </summary> 4 public classProperty:ITable 5{ 6 /// <summary> 7 /// 获取和设置资料的值 8 /// </summary> 9 /// <remarks> 10 /// 对于普通类型,例如float等类型直接就保存其ToString的返回结果 11 /// 对于复杂类型,则保存其json格式的对象 12 /// </remarks> 13 // TODO:第二版-需要考虑国际化情形下,属性有多个值的情形! 14 public stringValue{ get; set;} 15 16 /// <summary> 17 /// 获取和设置属性的Id 18 /// </summary> 19 publicGuidId{ get; set;} 20 21 publicMetaPropertyMeta{ get; set;} 22 23 /// <summary> 24 /// 获取和设置该属性对应的元数据Id 25 /// </summary> 26 publicGuidMetaId{ get; set;} 27 28 /// <summary> 29 /// 该属性对应的资料的编号 30 /// </summary> 31 publicGuidEntityId{ get; set;} 32 33 /// <summary> 34 /// 获取和设置该属性所属的资料 35 /// </summary> 36 publicGenericDynamicPropertiesEntityEntity{ get; set;} 37} 元数据 1 public classMetaProperty:INamedTable,ISecret 2{ 3 publicGuidId{ get; set;} 4 5 public stringBelongsToMaterial{ get; set;} 6 7 publicStringTitle{ get; set;} 8 9 public stringType{ get; set;} 10 11 public stringDefaultValue{ get; private set;} 12 13 /// <summary> 14 /// 获取和设置属性的权限 15 /// </summary> 16 public intPermission{ get; set;} 17 18 public virtual stringExpectedFormat{ get{ return string.Empty;}} 19 20 public virtual boolValid( stringvalue) 21{ 22 return true; 23} 24 25 public virtual boolValid( doublevalue) 26{ 27 return true; 28} 29 30 public staticMetaPropertyNewString( stringname) 31{ 32 return newMetaProperty() 33{ 34Id=Guid.NewGuid(), 35Title=name, 36Type=Default.MetaProperty.Type.String, 37Permission=Default.Permission.Mask 38}; 39} 40 41 public staticMetaPropertyNewNumber( stringname, doubledefaultValue= 0.0) 42{ 43 return newMetaProperty() 44{ 45Id=Guid.NewGuid(), 46Title=name, 47Type=Default.MetaProperty.Type.Number, 48Permission=Default.Permission.Mask, 49DefaultValue=defaultValue.ToString() 50}; 51} 52 53 public staticMetaPropertyNewAddress( stringname) 54{ 55 return newMetaProperty() 56{ 57Id=Guid.NewGuid(), 58Title=name, 59Type=Default.MetaProperty.Type.Address, 60Permission=Default.Permission.Mask 61}; 62} 63 64 public staticMetaPropertyNewRelationship( stringname) 65{ 66 return newMetaProperty() 67{ 68Id=Guid.NewGuid(), 69Title=name, 70Type=Default.MetaProperty.Type.Relationship, 71Permission=Default.Permission.Mask 72}; 73} 74 75 public staticMetaPropertyNewDateTime( stringname) 76{ 77 return newMetaProperty() 78{ 79Id=Guid.NewGuid(), 80Title=name, 81Type=Default.MetaProperty.Type.DateTime, 82Permission=Default.Permission.Mask 83}; 84} 85 86 public staticMetaPropertyNewDate( stringname) 87{ 88 return newMetaProperty() 89{ 90Id=Guid.NewGuid(), 91Title=name, 92Type=Default.MetaProperty.Type.Date, 93Permission=Default.Permission.Mask 94}; 95} 96 97 public staticMetaPropertyNewTime( stringname) 98{ 99 return newMetaProperty() 100{ 101Id=Guid.NewGuid(), 102Title=name, 103Type=Default.MetaProperty.Type.Time, 104Permission=Default.Permission.Mask 105}; 106} 107 108 public staticMetaPropertyNewUser( stringname) 109{ 110 return newMetaProperty() 111{ 112Id=Guid.NewGuid(), 113Title=name, 114Type=Default.MetaProperty.Type.User, 115Permission=Default.Permission.Mask 116}; 117} 118 119 public staticMetaPropertyNewUrl( stringname) 120{ 121 return newUrlMetaProperty() 122{ 123Id=Guid.NewGuid(), 124Title=name, 125Type=Default.MetaProperty.Type.Url, 126Permission=Default.Permission.Mask 127}; 128} 129 130 public staticMetaPropertyNewTag( stringname) 131{ 132 return newMetaProperty() 133{ 134Id=Guid.NewGuid(), 135Title=name, 136Type=Default.MetaProperty.Type.Tag, 137Permission=Default.Permission.Mask 138}; 139} 140} 141 142 public classMetaProperties:List<MetaProperty> 143{ 144 publicMetaPropertyFind( stringname) 145{ 146 return this.SingleOrDefault(p=>String.Compare(p.Title,name)== 0); 147} 148} 维护资料时,使用类似下面的代码就可以给资料创建无限多的属性,可以事先、事后给属性关联元数据,以便定义编辑和显示方式(里面用到一些Ioc和Mock): 1[TestMethod] 2 public void验证客户资料的动态属性的可行性() 3{ 4 varrep= newMemoryContext(); 5MemoryMetaSetmetas= newMemoryMetaSet(); 6metas.Add( typeof(Customer),MetaProperty.NewString( " 姓名 ")); 7metas.Add( typeof(Customer),MetaProperty.NewNumber( " 年龄 ")); 8metas.Add( typeof(Customer),MetaProperty.NewAddress( " 地址 ")); 9metas.Add( typeof(Customer),MetaProperty.NewRelationship( " 同事 ")); 10rep.MetaSet=metas; 11 12 varbuilder= newContainerBuilder(); 13 varmocks= newMockery(); 14 varuser=mocks.NewMock<IUser>(); 15Expect.AtLeastOnce.On(user).GetProperty( " Email ").Will(Return.Value(DEFAULT_USER_EMAIL)); 16 17builder.RegisterInstance(user).As<IUser>(); 18builder.RegisterInstance(rep).As<IContext>(); 19 varback=IocHelper.Container; 20 try 21{ 22IocHelper.Container=builder.Build(); 23 24 varcustomer=Customer.New<Customer>(); 25customer.Set( " 姓名 ", " XXX "); 26customer.Set( " 年龄 ", 28); 27customer.Set( " 地址 ", " ZZZZZZZZZZZZZZZZZZZZZZ "); 28 29 varcolleague=Customer.New<Customer>(); 30colleague.Set( " 姓名 ", " YYY "); 31 32 // 对于稍微复杂一点的对象,我们可以用json对象 33 customer.Set( " 同事 ",Relationship.Colleague(customer,colleague).ToString()); 34 35Assert.AreEqual( " XXX ",customer.Get( " 姓名 ")); 36} 37 finally 38{ 39IocHelper.Container=back; 40} 41} 因为动态属性事先不知道其格式,为了实现搜索功能,无法在编写程序的时候拼接查询用的SQL语句,因此我抽象了一层,定义了一个小的查询语法,写了一个小小的编译器将查询语句转化成SQL语句,实现了对动态属性的查询功能,请看下面的测试用例: 1[TestMethod] 2 public void测试简单的条件组合查询() 3{ 4 using( varcontext=IocHelper.Container.Resolve<IContext>()) 5{ 6 varcustomer=Customer.New<Customer>(); 7customer.Set( " 姓名 ", " 测试简单的条件组合查询 "); 8customer.Set( " 年龄 ", 28); 9customer.Set( " 地址 ", " 上海市浦东新区 "); 10context.Customers.Add(customer); 11context.SaveChanges(); 12 13 varresult=context.Customers.Query( " (AND(姓名='测试简单的条件组合查询') "+ 14 " (年龄介于1到30) "+ 15 " ) "); 16Assert.IsTrue(result.Count()> 0); 17 varactual=result.First(); 18Assert.AreEqual( " 测试简单的条件组合查询 ",actual.Get( " 姓名 ")); 19Assert.AreEqual( " 28 ",actual.Get( " 年龄 ")); 20} 21} 上面测试用例里的查询语句: (AND (姓名='测试简单的条件组合查询') (年龄介于1到30) ) 经过Query函数的编译之后,会转化成下面的两段SQL语句: SELECTe. *,p. * FROMProperties ASp INNER JOINGenericDynamicPropertiesEntities ASe ONp.EntityId =e.Id INNER JOINMetaSet ASm ONp.MetaId =m.Id WHERE(( CASE WHENm.Type = ' 日期时间型 ' AND( CONVERT( datetime,p.Value) =N ' 测试简单的条件组合查询 ') AND(m.Title =N ' 姓名 ') THEN 1 WHENm.Type = ' 字符串 ' AND(p.Value =N ' 测试简单的条件组合查询 ') AND(m.Title =N ' 姓名 ') THEN 1 ELSE 0 END = 1)) SELECTe. *,p. * FROMProperties ASp INNER JOINGenericDynamicPropertiesEntities ASe ONp.EntityId =e.Id INNER JOINMetaSet ASm ONp.MetaId =m.Id WHERE(( CASE WHENm.Type = ' 数字型 ' AND( CONVERT( int,p.Value) BETWEEN 1 AND 30) AND(m.Title =N ' 年龄 ') THEN 1 WHENm.Type = ' 日期时间型 ' AND( CONVERT( datetime,p.Value) BETWEENN ' 1 ' ANDN ' 30 ') AND(m.Title =N ' 年龄 ') THEN 1 ELSE 0 END = 1)) 然后分别执行查询并在业务层将求解查询结果的交集,也许是可以直接生成一条SQL语句交给数据库处理成最精简的结果再返回的,但是因为开发时间、以及目标客户的关系,暂时没有花精力做这个优化。 当然上面的查询语句写起来还比较复杂,因此我做了一个界面方便用户编辑查询条件,另外对资料属性的编辑、元数据维护等内容,后面再写文章说,蚊子太多了…… 本文转自 donjuan 博客园博客,原文链接:http://www.cnblogs.com/killmyday/archive/2012/07/21/2601900.html ,如需转载请自行联系原作者