Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update entities doc #2895

Merged
merged 2 commits into from Feb 24, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 138 additions & 22 deletions docs/zh-Hans/Entities.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,80 @@
## 实体
# 实体

实体是DDD(Domain Driven Design)中核心概念.Eric Evans是这样描述实体的 "一个没有从其属性,而是通过连续性和身份的线索来定义的对象"

实体通常映射到关系型数据库的表中.

### 实体类
## 实体类

实体都继承自`Entity<TKey>`类,如下所示:

```C#
public class Person : Entity<int>
public class Book : Entity<Guid>
{
public string Name { get; set; }

public DateTime CreationTime { get; set; }
public float Price { get; set; }
}
```

> 如果你不想继承基类`Entity<TKey>`,也可以直接实现`IEntity<TKey>`接口

`Entity<TKey>`类只是用给定的主 **键类型** 定义了一个`Id`属性,在上面的示例中是`Guid`类型.可以是其他类型如`string`, `int`, `long`或其他你需要的类型.

### Guid主键的实体

如果你的实体Id类型为 `Guid`,有一些好的实践可以实现:

public Person()
* 创建一个构造函数,获取ID作为参数传递给基类.
* 如果没有为GUID Id斌值,ABP框架会在保存时设置它,但是在将实体保存到数据库之前最好在实体上有一个有效的Id.
* 如果使用带参数的构造函数创建实体,那么还要创建一个 `protected` 构造函数. 当数据库提供程序从数据库读取你的实体时(反序列化时)将使用它.
* 不要使用 `Guid.NewGuid()` 来设置Id! 在创建实体的代码中使用[`IGuidGenerator`服务](Guid-Generation.md)传递Id参数. `IGuidGenerator`经过优化可以产生连续的GUID.这对于关系数据库中的聚集索引非常重要.

示例实体:

````csharp
public class Book : Entity<Guid>
{
public string Name { get; set; }
public float Price { get; set; }
protected Book()
{
}
public Book(Guid id)
: base(id)
{
CreationTime = DateTime.Now;
}
}
```
````

> 如果你不想继承基类`Entity<TKey>`,也可以直接实现`IEntity<TKey>`接口
在[应用服务](Application-Services.md)中使用示例:

`Entity<TKey>`类只是用给定的主 **键类型** 定义了一个`Id`属性,在上面的示例中是`int`类型.可以是其他类型如`string`, `Guid`, `long`或其他你需要的类型.
````csharp
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IRepository<Book> _bookRepository;
public BookAppService(IRepository<Book> bookRepository)
{
_bookRepository = bookRepository;
}
public async Task CreateAsync(CreateBookDto input)
{
await _bookRepository.InsertAsync(
new Book(GuidGenerator.Create())
{
Name = input.Name,
Price = input.Price
}
);
}
}
````

实体类还重写了 **equality** 运算符(==),以方便地检查两个实体是否相等(如果它们是相同的类型并且它们的Id相等,则它们是相等的).
* `BookAppService` 注入图书实体的默认[仓库](Repositories.md),使用`InsertAsync`方法插入 `Book` 到数据库中.
* `GuidGenerator`类型是 `IGuidGenerator`,它是在`ApplicationService`基类中定义的属性. ABP将这样常用属性预注入,所以不需要手动[注入](Dependency-Injection.md).
* 如果您想遵循DDD最佳实践,请参阅下面的*聚合示例*部分.

#### 具有复合键的实体
### 具有复合键的实体

有些实体可能需要 **复合键** .在这种情况下,可以从非泛型`Entity`类派生实体.如:

Expand All @@ -53,30 +99,30 @@ public class UserRole : Entity
}
````

上面的例子中,复合键由`UserId`和`RoleId`组成.在关系数据库中,它是相关表的复合主键.
上面的例子中,复合键由`UserId`和`RoleId`组成.在关系数据库中,它是相关表的复合主键. 具有复合键的实体应当实现上面代码中所示的`GetKeys()`方法.

具有复合键的实体应当实现上面代码中所示的`GetKeys()`方法.
> 你还需要在**对象关系映射**(ORM)中配置实体的键. 参阅[Entity Framework Core](Entity-Framework-Core.md)集成文档查看示例.

你还需要在 **object-to-relational mapping**(ORM)中配置实体的键.
> 需要注意,复合主键实体不可以使用 `IRepository<TEntity, TKey>` 接口,因为它需要一个唯一的Id属性. 但你可以使用 `IRepository<TEntity>`.更多信息请参见[仓储](Repositories.md)的文档.

> 复合主键在仓储中有限制.由于不知道Id属性,所以对于这些实体,不能使用`IRepository<TEntity, TKey>`.但是,可以使用`IRepository<TEntity>`.更多信息请参见[仓储](Repositories.md)的文档.

### 聚合根
## 聚合根

"*聚合是域驱动设计中的一种模式.DDD的聚合是一组可以作为一个单元处理的域对象.例如,订单及订单系列的商品,这些是独立的对象,但将订单(连同订单系列的商品)视为一个聚合通常是很有用的*"( [查看详细介绍](http://martinfowler.com/bliki/DDD_Aggregate.html))

`AggregateRoot`类继承自`Entity`类,所以默认有`Id`这个属性
`AggregateRoot<TKey>`类继承自`Entity<TKey>`类,所以默认有`Id`这个属性

> 值得注意的是 ABP 会默认为聚合根创建仓储,当然,ABP也可以为所有的实体创建仓储,详情参见[仓储](Repositories.md).

ABP不强制你使用聚合根,实际上你可以使用上面定义的`Entity`类,当然,如果你想实现DDD并且创建聚合根,这里有一些最佳实践仅供参考:
ABP不强制你使用聚合根,实际上你可以使用上面定义的`Entity`类,当然,如果你想实现[领域驱动设计](Domain-Driven-Design.md)并且创建聚合根,这里有一些最佳实践仅供参考:

* 聚合根需要维护自身的完整性,所有的实体也是这样.但是聚合根也要维护子实体的完整性.所以,聚合根必须一直有效.
* 使用Id引用聚合根,而不使用导航属性
* 聚合根被视为一个单元.它是作为一个单元检索和更新的.它通常被认为是一个交易边界.
* 不单独修改聚合根中的子实体

#### 聚合根例子
如果你想在应用程序中实现DDD,请参阅[实体设计最佳实践指南](Best-Practices/Entities.md).

### 聚合根例子

这是一个具有子实体集合的聚合根例子:

Expand Down Expand Up @@ -155,6 +201,11 @@ public class OrderLine : Entity
{
Count = newCount;
}

public override object[] GetKeys()
{
return new Object[] {OrderId, ProductId};
}
}
````

Expand All @@ -169,8 +220,73 @@ public class OrderLine : Entity
* `Order.AddProduct`实现了业务规则将商品添加到订单中
* 所有属性都有`protected`的set.这是为了防止实体在实体外部任意改变.因此,在没有向订单中添加新产品的情况下设置 `TotalItemCount`将是危险的.它的值由`AddProduct`方法维护.

ABP不强制你应用任何DDD规则或模式.但是,当你准备应用的DDD规则或模式时候,ABP会让这变的可能而且更简单.文档同样遵循这个原则.
ABP框架不强制你应用任何DDD规则或模式.但是,当你准备应用的DDD规则或模式时候,ABP会让这变的可能而且更简单.文档同样遵循这个原则.

#### 带有组合键的聚合根
### 带有组合键的聚合根

虽然这种聚合根并不常见(也不建议使用),但实际上可以按照与上面提到的跟实体相同的方式定义复合键.在这种情况下,要使用非泛型的`AggregateRoot`基类.

## 基类和接口的审计属性

有一些属性,像`CreationTime`,`CreatorId`,`LastModificationTime`...在所有应用中都很常见. ABP框架提供了一些接口和基类来**标准化**这些属性,并**自动设置它们的值**.

### 审计接口

有很多的审计接口,你可以实现一个你需要的那个.

> 虽然可以手动实现这些接口,但是可以使用下一节中定义的**基类**简化代码.

* `IHasCreationTime` 定义了以下属性:
* `CreationTime`
* `IMayHaveCreator` 定义了以下属性:
* `CreatorId`
* `ICreationAuditedObject` 继承 `IHasCreationTime` 和 `IMayHaveCreator`, 所以它定义了以下属性:
* `CreationTime`
* `CreatorId`
* `IHasModificationTime` 定义了以下属性:
* `LastModificationTime`
* `IModificationAuditedObject` 扩展 `IHasModificationTime` 并添加了 `LastModifierId` 属性. 所以它定义了以下属性:
* `LastModificationTime`
* `LastModifierId`
* `IAuditedObject` 扩展 `ICreationAuditedObject` 和 `IModificationAuditedObject`, 所以它定义了以下属性:
* `CreationTime`
* `CreatorId`
* `LastModificationTime`
* `LastModifierId`
* `ISoftDelete` (参阅 [数据过滤文档](Data-Filtering.md)) 定义了以下属性:
* `IsDeleted`
* `IHasDeletionTime` 扩展 `ISoftDelete` 并添加了 `DeletionTime` 属性. 所以它定义了以下属性:
* `IsDeleted`
* `DeletionTime`
* `IDeletionAuditedObject` 扩展 `IHasDeletionTime` 并添加了 `DeleterId` 属性. 所以它定义了以下属性:
* `IsDeleted`
* `DeletionTime`
* `DeleterId`
* `IFullAuditedObject` 继承 `IAuditedObject` 和 `IDeletionAuditedObject`, 所以它定义了以下属性:
* `CreationTime`
* `CreatorId`
* `LastModificationTime`
* `LastModifierId`
* `IsDeleted`
* `DeletionTime`
* `DeleterId`

当你实现了任意接口,或者从下一节定义的类派生,ABP框架就会尽可能地自动管理这些属性.

> 实现 `ISoftDelete` , `IDeletionAuditedObject` 或 `IFullAuditedObject` 让你的实体**软删除**. 参阅[数据过滤文档](Data-Filtering.md),了解软删除模式.

### 审计基类

虽然可以手动实现以上定义的任何接口,但建议从这里定义的基类继承:

* `CreationAuditedEntity<TKey>` 和 `CreationAuditedAggregateRoot<TKey>` 实现了 `ICreationAuditedObject` 接口.
* `AuditedEntity<TKey>` 和 `AuditedAggregateRoot<TKey>` 实现了 `IAuditedObject` 接口.
* `FullAuditedEntity<TKey>` and `FullAuditedAggregateRoot<TKey>` 实现了 `IFullAuditedObject` 接口.

所有这些基类都有非泛型版本,可以使用 `AuditedEntity` 和 `FullAuditedAggregateRoot` 来支持复合主键;

所有这些基类也有 `... WithUser`,像 `FullAuditedAggregateRootWithUser<TUser>` 和 `FullAuditedAggregateRootWithUser<TKey, TUser>`. 这样就可以将导航属性添加到你的用户实体. 但在聚合根之间添加导航属性不是一个好做法,所以这种用法是不建议的(除非你使用EF Core之类的ORM可以很好地支持这种情况,并且你真的需要它. 请记住这种方法不适用于NoSQL数据库(如MongoDB),你必须真正实现聚合模式).

## 另请参阅

* [实体设计最佳实践指南](Best-Practices/Entities.md)