Browse Source

Update the many-to-many relationship article

pull/10509/head
Engincan VESKE 5 years ago
parent
commit
c2d869549a
  1. 153
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/POST.md
  2. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/demo.png
  3. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-entity-structure.png
  4. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-file-structure.png

153
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/POST.md

@ -13,11 +13,18 @@ When we've examined the ER Diagram, we can see the one-to-many relationship betw
### Source Code ### Source Code
You can find the source code of the application at https://github.com/EngincanV/ABP-Many-to-Many-Relationship-Demo . You can find the source code of the application at https://github.com/EngincanV/ABP-Many-to-Many-Relationship-Demo .
### Screenshot of The Final Application
At the end of this article, we will have created an application as in the below image.
![Book Homepage](./demo.png)
## Creating the Solution ## Creating the Solution
In this article, I will create a new startup template with EF Core as a database provider and MVC for UI framework. In this article, I will create a new startup template with EF Core as a database provider and MVC for UI framework.
* We can create a new startup template by using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): * We can create a new startup template by using the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI):
```bash ```bash
abp new BookStore -t app --version 5.0.0-beta.2 abp new BookStore -t app --version 5.0.0-beta.2
@ -33,7 +40,7 @@ Let's start with creating our Domain Entities.
We can create a folder-structure under the `BookStore.Domain` project like in the below image. We can create a folder-structure under the `BookStore.Domain` project like in the below image.
![Domain-Layer-Folder-Structure](./domain-entity-structure.png) ![Domain-Layer-Folder-Structure](./domain-file-structure.png)
Open the entity classes and add the following codes to each of these classes. Open the entity classes and add the following codes to each of these classes.
@ -86,6 +93,10 @@ namespace BookStore.Authors
```csharp ```csharp
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.Domain.Entities.Auditing;
namespace BookStore.Books namespace BookStore.Books
@ -94,70 +105,94 @@ namespace BookStore.Books
{ {
public Guid AuthorId { get; set; } public Guid AuthorId { get; set; }
public string Name { get; set; } public string Name { get; private set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; } public DateTime PublishDate { get; set; }
public float Price { get; set; } public float Price { get; set; }
/* This constructor is for deserialization / ORM purpose */ public ICollection<BookCategory> Categories { get; private set; }
private Book() private Book()
{ {
} }
public Book(Guid id, Guid authorId, string name, BookType type, DateTime publishDate, float price) public Book(Guid id, Guid authorId, string name, DateTime publishDate, float price)
: base(id) : base(id)
{ {
AuthorId = authorId; AuthorId = authorId;
Name = name; SetName(name);
Type = type;
PublishDate = publishDate; PublishDate = publishDate;
Price = price; Price = price;
Categories = new Collection<BookCategory>();
} }
}
}
```
> We'll create the `BookType` enum later in this step. public void SetName(string name)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name), BookConsts.MaxNameLength);
}
* **Category.cs** public void AddCategory(Guid categoryId)
{
Check.NotNull(categoryId, nameof(categoryId));
if (IsInCategory(categoryId))
{
return;
}
Categories.Add(new BookCategory(bookId: Id, categoryId));
}
```csharp public void RemoveCategory(Guid categoryId)
using System; {
using Volo.Abp; Check.NotNull(categoryId, nameof(categoryId));
using Volo.Abp.Domain.Entities.Auditing;
namespace BookStore.Categories if (!IsInCategory(categoryId))
{ {
public class Category : AuditedAggregateRoot<Guid> return;
{ }
public string Name { get; private set; }
public bool IsActive { get; set; } Categories.RemoveAll(x => x.CategoryId == categoryId);
}
/* This constructor is for deserialization / ORM purpose */ public void RemoveAllCategoriesExceptGivenIds(List<Guid> categoryIds)
private Category()
{ {
Check.NotNullOrEmpty(categoryIds, nameof(categoryIds));
Categories.RemoveAll(x => !categoryIds.Contains(x.CategoryId));
} }
public Category(Guid id, string name, bool isActive = true) : base(id) public void RemoveAllCategories()
{ {
SetName(name); Categories.RemoveAll(x => x.BookId == Id);
IsActive = isActive;
} }
public Category SetName(string name) private bool IsInCategory(Guid categoryId)
{ {
Name = Check.NotNullOrWhiteSpace(name, nameof(name), BookConsts.MaxNameLength); return Categories.Any(x => x.CategoryId == categoryId);
return this;
} }
} }
} }
``` ```
> We'll create the `BookConsts` class later in this step. * In our scenerio, a book can have more than one category and a category can have more
than one book so it's obvious to we need to establish many-to-many relationship between them.
* For achieve this we will create an **join entity** named `BookCategory`, and this class
will simply have variables named `BookId` and `CategoryId`.
* To manage this **join entity**, we can add it as sub-collection to the **Book** entity, as we do above. We add this sub-collection
to **Book** class instead of **Category** class, because a book can have tens or mostly hundreds of category in it but in other
view a category can have more than hundred (or even way much) book inside of it.
* It is a significant performance problem to load thousands of items whenever you query a category. Therefore it's make much sense to add that sub-collection to `Book` entity. (Don't forget: **An aggregate (with the root entity and sub-collections) should
be serializable and transferrable on the wire as a single unit.**)
* Notice that, `BookCategory` is not an **Aggregate Root** so we are not violating the one of the base rule about Aggregate Root (Rule: "Reference Other Aggregates Only by ID").
> We'll create the `BookCategory` and `BookConsts` classes later in this step.
* **BookCategory.cs** * **BookCategory.cs**
@ -173,6 +208,7 @@ namespace BookStore.Books
public Guid CategoryId { get; protected set; } public Guid CategoryId { get; protected set; }
/* This constructor is for deserialization / ORM purpose */
private BookCategory() private BookCategory()
{ {
} }
@ -191,27 +227,52 @@ namespace BookStore.Books
} }
``` ```
Here, as you can notice we've defined the `BookCategory` as the **Join Table** for our many-to-many relationship and ensure the required properties (BookId and CategoryId) must be set to create this object. * Here, as you can notice we've defined the `BookCategory` as the **Join Table/Entity** for our many-to-many relationship and ensure the required properties (BookId and CategoryId) must be set to create this object.
And also we've derived this class from `Entity` class and therefore we've had to override the **GetKeys** method of this class to define **Composite Key** * And also we've derived this class from `Entity` class and therefore we've had to override the **GetKeys** method of this class to define **Composite Key**.
> The composite key is composed of `BookId` and `CategoryId` in our case. > The composite key is composed of `BookId` and `CategoryId` in our case. And they are unique with together.
> For more information about **Entities with Composite Keys**, you can read the relavant section from [Entites documentation](https://docs.abp.io/en/abp/latest/Entities#entities-with-composite-keys) > For more information about **Entities with Composite Keys**, you can read the relavant section from [Entites documentation](https://docs.abp.io/en/abp/latest/Entities#entities-with-composite-keys)
//TODO: add BookManager class later?
* **Category.cs**
* After defining our entities, now we can open the `BookStore.Domain.Shared` project and add the relevant constants and enums from there. ```csharp
using System;
using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing;
* namespace BookStore.Categories
{
public class Category : AuditedAggregateRoot<Guid>
{
public string Name { get; private set; }
/* This constructor is for deserialization / ORM purpose */
private Category()
{
}
public Category(Guid id, string name) : base(id)
{
SetName(name);
}
public Category SetName(string name)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name), CategoryConsts.MaxNameLength);
return this;
}
}
}
```
### Step 2 - (Database Integration) ### Step 2 - (Database Integration)
### Step 3 - (Database Migration)
--- ### Step 4 - (Create Application Services)
* Create the Domain Entities ✓ ### Step 5 - (UI)
* Database Integration ✓
* Db migration ✓
* app services (category -> crud app service)
* ui

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/demo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-entity-structure.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-file-structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Loading…
Cancel
Save