实体(Row)

优质
小牛编辑
133浏览
2023-12-01

Serenity 实体系统是一个微 ORM,它像 Dapper 那样对 SQL 情有独钟。

不同于完全成熟的ORM,如 NHibernate/Entity Framework,Serenity 提供智能感知地映射和查询数据库所需的最小功能、编译时检查和容易的重构。

Serenity 的实体通常像 XYZRow 这样命名。它们是 Serenity.Data.Row 子类。

让我们来定义一个简单的行类:

  1. using Serenity;
  2. using Serenity.ComponentModel;
  3. using Serenity.Data;
  4. public class SimpleRow : Row
  5. {
  6. public string Name
  7. {
  8. get { return Fields.Name[this]; }
  9. set { Fields.Name[this] = value; }
  10. }
  11. public Int32? Age
  12. {
  13. get { return Fields.Age[this]; }
  14. set { Fields.Age[this] = value; }
  15. }
  16. public static RowFields Fields = new RowFields().Init();
  17. public SimpleRow()
  18. : base(Fields)
  19. {
  20. }
  21. public class RowFields : RowFieldsBase
  22. {
  23. public StringField Name;
  24. public Int32Field Age;
  25. }
  26. }

是的,与一个简单的 POCO 类相比,它看起来有点复杂。它不需要像一些 Orm (实体框架,NHibernate 等)那样使用代理类来让一些功能工作。

这种结构使我们能够用零反射构建查询,在需要时启用 INotifyPropertyChanged 跟踪作业。它也可以与用户自定义的字段一起工作。

行(Row)被序列化为 JSON ,这样就可以从服务器返回。你不需要额外的 POCO/DTO 类,除非你有使用它们的好理由。

让我们先学习行的声明部分。

  1. public class SimpleRow : Row

我们在这里定义了一个名为 SimpleRow 的实体,它可能映射数据库中名为 Simple 的表。

这里的 Row 后缀不是必须的,但通常加上该后缀可避免与其他的类名称发生冲突。

所有的实体类继承自 Serenity.Data.Row 基类。

  1. public string Name
  2. {
  3. get { return Fields.Name[this]; }
  4. set { Fields.Name[this] = value; }
  5. }

我们现在声明第一个属性。此属性映射到数据库的 SimpleName 列。

这里不能使用自动属性(如 get; set;)。必须通过称为 Field 的特定对象来读取和设置字段的值。

Field 对象非常类似于 WPF 的依赖属性。下面是依赖属性的声明示例:

  1. public static readonly DependencyProperty MyCustomProperty =
  2. DependencyProperty.Register("MyCustom", typeof(string), typeof(Window1));
  3. public string MyCustom
  4. {
  5. get { return this.GetValue(MyCustomProperty) as string; }
  6. set { this.SetValue(MyCustomProperty, value); }
  7. }

我们在这里定义了一个静态依赖属性对象(MyCustomProperty),它包含属性元数据可以允许我们通过 GetValueSetValue 方法读取和设置属性的值。依赖属性允许 WPF 提供验证、 数据绑定、 动画及更多的功能。

类似于依赖属性,Field 对象包含列的元数据和一些如任务跟踪(assignment tracking)、不使用表达式树构建的查询、变更通知等辅助功能。

虽然依赖属性被声明为所使用类的静态成员, Field 对象在一个名为 RowFields 的嵌套类中声明。这样可更容易分组和获取引用,而不用添加 FieldProperty 后缀,并从实体中保持清晰的字段声明。

  1. public Int32? Age
  2. {
  3. get { return Fields.Age[this]; }
  4. set { Fields.Age[this] = value; }
  5. }

这是我们的第二个属性: Int32? 类型的 Age

Serenity 实体属性始终是可空的,即使数据库中的列类型不是可空类型。

Serenity 从不会在 null 的位置使用 0。

如果你有使用其他 ORM 的经验,会发现这似乎不合逻辑,但请考虑这种情况:

一个非空字段不可能有 null 值,但如果通过 left/right 联接查询呢?在这种情况下,如果检索到的值是 null 或 0 ,你要怎么处理呢?

引用类型已经是可空类型,所以你不能使用 String?

  1. public static RowFields Fields = new RowFields().Init();

我们注意到,Field 对象在名为 RowFields(通常)嵌套子类中声明。在这里,我们创建它的唯一静态实例。因此,每个行类型(row type)只有一个 RowFields 实例,并且每个行属性(row property)只有一个 Field 实例 。

Init 是初始化 RowFields 成员的扩展方法,它将创建没有显式初始化的 Field 对象。

  1. public SimpleRow()
  2. : base(Fields)
  3. {
  4. }

现在我们定义 SimpleRow 带参数的构造函数。基类 Row 需要一个 RowFields 实例,我们为其传递静态对象 Fields 。因此,行类型(SimpleRow)的所有实例共享单个 RowFields (SimpleRow.RowFields) 实例。这意味着它们共享所有元数据。

  1. public class RowFields : RowFieldsBase
  2. {
  3. public StringField Name;
  4. public Int32Field Age;
  5. }

我们在这里定义包含字段对象的嵌套类。它继承自 Serenity.Data.RowFieldsBaseRowFieldsBase 是一个与 Row 相关的特殊类,包含表的元数据。

我们分别声明一个 StringFieldInt32Field 类型的字段。它们的类型基于其属性的类型,并且必须完全匹配。

它们的名称必须与属性名称匹配,否则你将得到一个初始化错误。

我们没有初始化这些字段对象,因此它们的初始值为 null。

记得我们上面写的 new RowFields().Init(),这是字段对象自动创建的地方。

也可以在 RowFields 构造函数中手动初始化它们,但是除了需要特殊定制,不建议这样做。