• 网站动态属性的一个架构


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

    本文就讲一讲在.NETSQL 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 { getprivate set; }
     55 
     56         [DataMember]
     57         public Guid Id { getset; }
     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 { getset; }
    15         
    16         /// <summary>
    17         /// 获取和设置属性的Id
    18         /// </summary>
    19         public Guid Id { getset; }
    20 
    21         public MetaProperty Meta { getset; }
    22 
    23         /// <summary>
    24         /// 获取和设置该属性对应的元数据Id
    25         /// </summary>
    26         public Guid MetaId { getset; }
    27 
    28         /// <summary>
    29         /// 该属性对应的资料的编号
    30         /// </summary>
    31         public Guid EntityId { getset; }
    32 
    33         /// <summary>
    34         /// 获取和设置该属性所属的资料
    35         /// </summary>
    36         public GenericDynamicPropertiesEntity Entity { getset; }
    37 }


    元数据

      1 public class MetaProperty : INamedTable, ISecret
      2     {
      3         public Guid Id { getset; }
      4 
      5         public string BelongsToMaterial { getset; }
      6 
      7         public String Title { getset; }
      8 
      9         public string Type { getset; }
     10 
     11         public string DefaultValue { getprivate set; }
     12 
     13         /// <summary>
     14         /// 获取和设置属性的权限
     15         /// </summary>
     16         public int Permission { getset; }
     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 }

    维护资料时,使用类似下面的代码就可以给资料创建无限多的属性,可以事先、事后给属性关联元数据,以便定义编辑和显示方式(里面用到一些IocMock):

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

    当然上面的查询语句写起来还比较复杂,因此我做了一个界面方便用户编辑查询条件,另外对资料属性的编辑、元数据维护等内容,后面再写文章说,蚊子太多了…… 



     

  • 相关阅读:
    解析数据库xml格式字段
    Spring切面表达式,hibernate,struts
    常见枚举
    经典冒泡排序,九九乘法表,三角形
    拦截器后台安全验证
    Oracle 将表中多条数据同一字段拼成一列显示
    idea 11 控制台日志乱码
    BigDecimal 分转元,元转分
    currtDownLatch可能会出现的三个问题
    CountDownLatch用法
  • 原文地址:https://www.cnblogs.com/killmyday/p/2601900.html
Copyright © 2020-2023  润新知