Jakeuj's Notes master Help

ABP.IO 新手教學 No.11 開發教學 第 10 部分:書籍與作者的關係 {id="ABP-IO-Tutorial-No-11-Part-10-Books-Authors-Relationship"}

建立關聯

Web 應用程序開發教程 - 第 10 部分:書籍與作者的關係

關於本教程

在本系列教程中,您將構建一個名為Acme.BookStore. 此應用程序用於管理書籍及其作者的列表。它是使用以下技術開發的:

  • Entity Framework Core作為 ORM 提供者。

  • Angular作為 UI 框架。

本教程分為以下幾個部分;

下載源代碼

本教程根據您的UI數據庫首選項有多個版本。我們準備了幾個要下載的源代碼組合:

介紹

我們創造BookAuthor為書商店應用程序的功能。但是,目前這些實體之間沒有任何關係。

在本教程中,我們將在和實體之間建立1 到 N 的關係。 AuthorBook

添加與圖書實體的關係

// 雖然 DDD 規則是不建立導航屬性,但是在 EntityFramework Core 中,其實加導航屬性比較方便 (但在其他 ORM 就僅通過 id 引用其他聚合)

Books/Book.csAcme.BookStore.Domain項目中打開並向Book實體添加以下屬性:

public Guid AuthorId { get; set; }

數據庫和數據遷移

// 開發環境可以直接在 PowerShell 執行 dotnet ef database drop來直接用最新版本來重建資料庫

Book實體添加了一個新的必需屬性 AuthorId 。但是, 關於數據庫的現有書籍呢?他們目前沒有AuthorId ,當我們嘗試運行應用程序時,這將是一個問題。

這是一個典型的遷移問題 ,決定取決於您的情況;

  • 如果您尚未將應用程序發佈到生產環境中,您可以刪除數據庫中現有的書籍,甚至可以刪除開發環境中的整個數據庫。

  • 您可以在數據遷移或種子階段以編程方式更新現有數據。

  • 您可以在數據庫上手動處理它。

我們更喜歡刪除數據庫 (您可以Drop-Database包管理器控制台中運行 ),因為這只是一個示例項目,數據丟失並不重要。

由於本主題與 ABP 框架無關,因此我們不會深入了解所有場景。

1627034727

更新 EF 核心映射

打開 Acme.BookStore.EntityFrameworkCore 項目 EntityFrameworkCore 文件夾下的 BookStoreDbContextModelCreatingExtensions 類,修改 builder.Entity<Book> 部分如下圖:

builder.Entity<Book>(b => { b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); b.ConfigureByConvention(); //auto configure for the base class props b.Property(x => x.Name).IsRequired().HasMaxLength(128); // ADD THE MAPPING FOR THE RELATION b.HasOne<Author>().WithMany().HasForeignKey(x => x.AuthorId).IsRequired(); });
1627034388

添加新的 EF Core 遷移

啟動解決方案配置為使用Entity Framework Core Code First Migrations 。由於我們已經更改了數據庫映射配置,我們應該創建一個新的遷移並將更改應用於數據庫。

Acme.BookStore.EntityFrameworkCore.DbMigrations項目目錄中打開命令行終端並鍵入以下命令:

dotnet ef migrations add Added_AuthorId_To_Book

// 會自動生成以下類別

這應該在其Up方法中使用以下代碼創建一個新的遷移類:

migrationBuilder.AddColumn<Guid>( name: "AuthorId", table: "AppBooks", nullable: false, defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); migrationBuilder.CreateIndex( name: "IX_AppBooks_AuthorId", table: "AppBooks", column: "AuthorId"); migrationBuilder.AddForeignKey( name: "FK_AppBooks_AppAuthors_AuthorId", table: "AppBooks", column: "AuthorId", principalTable: "AppAuthors", principalColumn: "Id", onDelete: ReferentialAction.Cascade);
  • AuthorIdAppBooks表中添加一個字段。

  • AuthorId字段上創建索引。

  • 聲明AppAuthors表的外鍵。

1627034898

更改數據播種機

由於AuthorIdBook實體的必需屬性,因此當前的數據播種器代碼無法工作。在Acme.BookStore.Domain項目中打開BookStoreDataSeederContributor ,修改如下:

using System; using System.Threading.Tasks; using Acme.BookStore.Authors; using Acme.BookStore.Books; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; namespace Acme.BookStore { public class BookStoreDataSeederContributor : IDataSeedContributor, ITransientDependency { private readonly IRepository<Book, Guid> _bookRepository; private readonly IAuthorRepository _authorRepository; private readonly AuthorManager _authorManager; public BookStoreDataSeederContributor( IRepository<Book, Guid> bookRepository, IAuthorRepository authorRepository, AuthorManager authorManager) { _bookRepository = bookRepository; _authorRepository = authorRepository; _authorManager = authorManager; } public async Task SeedAsync(DataSeedContext context) { if (await _bookRepository.GetCountAsync() > 0) { return; } var orwell = await _authorRepository.InsertAsync( await _authorManager.CreateAsync( "George Orwell", new DateTime(1903, 06, 25), "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." ) ); var douglas = await _authorRepository.InsertAsync( await _authorManager.CreateAsync( "Douglas Adams", new DateTime(1952, 03, 11), "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." ) ); await _bookRepository.InsertAsync( new Book { AuthorId = orwell.Id, // SET THE AUTHOR Name = "1984", Type = BookType.Dystopia, PublishDate = new DateTime(1949, 6, 8), Price = 19.84f }, autoSave: true ); await _bookRepository.InsertAsync( new Book { AuthorId = douglas.Id, // SET THE AUTHOR Name = "The Hitchhiker's Guide to the Galaxy", Type = BookType.ScienceFiction, PublishDate = new DateTime(1995, 9, 27), Price = 42.0f }, autoSave: true ); } } }

唯一的變化是我們設置AuthorIdBook實體的屬性。

1627035218

現在,您可以運行.DbMigrator控制台應用程序遷移數據庫架構種子的初始數據。

1627035315

應用層

我們將更改BookAppService以支持作者關係。

數據傳輸對象

讓我們從 DTO 開始。

BookDto - 書籍資料傳輸對象

打開 Acme.BookStore.Application.Contracts 項目 Books 文件夾中的BookDto類並添加以下屬性:

public Guid AuthorId { get; set; } public string AuthorName { get; set; }

最後的BookDto課程應該如下:

using System; using Volo.Abp.Application.Dtos; namespace Acme.BookStore.Books { public class BookDto : AuditedEntityDto<Guid> { public Guid AuthorId { get; set; } public string AuthorName { get; set; } public string Name { get; set; } public BookType Type { get; set; } public DateTime PublishDate { get; set; } public float Price { get; set; } } }
1627035471

CreateUpdateBookDto - 創建更新書籍

打開 Acme.BookStore.Application.Contracts 項目 Books 文件夾中的CreateUpdateBookDto類,添加一個屬性 AuthorId ,如圖:

public Guid AuthorId { get; set; }

AuthorLookupDto - 作者查找

Acme.BookStore.Application.Contracts項目裡面的Books文件夾創建一個新的類AuthorLookupDto

using System; using Volo.Abp.Application.Dtos; namespace Acme.BookStore.Books { public class AuthorLookupDto : EntityDto<Guid> { public string Name { get; set; } } }

這將用於將添加到IBookAppService.

圖書應用服務

打開工程文件夾中的IBookAppService界面,添加一個新的方法,命名為,如下圖: BooksAcme.BookStore.Application.ContractsGetAuthorLookupAsync

using System; using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; namespace Acme.BookStore.Books { public interface IBookAppService : ICrudAppService< //Defines CRUD methods BookDto, //Used to show books Guid, //Primary key of the book entity PagedAndSortedResultRequestDto, //Used for paging/sorting CreateUpdateBookDto> //Used to create/update a book { // ADD the NEW METHOD Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync(); } }
1627035721

這個新方法將用於從 UI 獲取作者列表並填充下拉列表以選擇一本書的作者。

圖書應用服務

打開工程BookAppService所在Books文件夾中的界面,將Acme.BookStore.Application文件內容替換為如下代碼:

using System; using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; using System.Threading.Tasks; using Acme.BookStore.Authors; using Acme.BookStore.Permissions; using Microsoft.AspNetCore.Authorization; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; namespace Acme.BookStore.Books { [Authorize(BookStorePermissions.Books.Default)] public class BookAppService : CrudAppService< Book, //The Book entity BookDto, //Used to show books Guid, //Primary key of the book entity PagedAndSortedResultRequestDto, //Used for paging/sorting CreateUpdateBookDto>, //Used to create/update a book IBookAppService //implement the IBookAppService { private readonly IAuthorRepository _authorRepository; public BookAppService( IRepository<Book, Guid> repository, IAuthorRepository authorRepository) : base(repository) { _authorRepository = authorRepository; GetPolicyName = BookStorePermissions.Books.Default; GetListPolicyName = BookStorePermissions.Books.Default; CreatePolicyName = BookStorePermissions.Books.Create; UpdatePolicyName = BookStorePermissions.Books.Edit; DeletePolicyName = BookStorePermissions.Books.Create; } public override async Task<BookDto> GetAsync(Guid id) { //Get the IQueryable<Book> from the repository var queryable = await Repository.GetQueryableAsync(); //Prepare a query to join books and authors var query = from book in queryable join author in _authorRepository on book.AuthorId equals author.Id where book.Id == id select new { book, author }; //Execute the query and get the book with author var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query); if (queryResult == null) { throw new EntityNotFoundException(typeof(Book), id); } var bookDto = ObjectMapper.Map<Book, BookDto>(queryResult.book); bookDto.AuthorName = queryResult.author.Name; return bookDto; } public override async Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input) { //Get the IQueryable<Book> from the repository var queryable = await Repository.GetQueryableAsync(); //Prepare a query to join books and authors var query = from book in queryable join author in _authorRepository on book.AuthorId equals author.Id select new {book, author}; //Paging query = query .OrderBy(NormalizeSorting(input.Sorting)) .Skip(input.SkipCount) .Take(input.MaxResultCount); //Execute the query and get a list var queryResult = await AsyncExecuter.ToListAsync(query); //Convert the query result to a list of BookDto objects var bookDtos = queryResult.Select(x => { var bookDto = ObjectMapper.Map<Book, BookDto>(x.book); bookDto.AuthorName = x.author.Name; return bookDto; }).ToList(); //Get the total count with another query var totalCount = await Repository.GetCountAsync(); return new PagedResultDto<BookDto>( totalCount, bookDtos ); } public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync() { var authors = await _authorRepository.GetListAsync(); return new ListResultDto<AuthorLookupDto>( ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors) ); } private static string NormalizeSorting(string sorting) { if (sorting.IsNullOrEmpty()) { return $"book.{nameof(Book.Name)}"; } if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase)) { return sorting.Replace( "authorName", "author.Name", StringComparison.OrdinalIgnoreCase ); } return $"book.{sorting}"; } } }

讓我們看看我們所做的更改:

  • 添加[Authorize(BookStorePermissions.Books.Default)]以授權我們新添加/覆蓋的方法(請記住,當為類聲明時,authorize 屬性對類的所有方法都有效)。

  • 注入IAuthorRepository作者查詢。

  • 重寫 base 的GetAsync方法,該方法CrudAppService返回BookDto具有給定的單個對象id

    • 使用簡單的 LINQ 表達式連接書籍和作者,並一起查詢給定書籍 ID。

    • 用於AsyncExecuter.FirstOrDefaultAsync(...)執行查詢並得到結果。這是一種使用異步 LINQ 擴展而不依賴於數據庫提供程序 API 的方法。查看存儲庫文檔以了解我們使用它的原因。

    • 如果數據庫中不存在請求的書,則拋出EntityNotFoundException結果HTTP 404 (未找到)結果。

    • 最後, BookDto使用 來創建一個對象ObjectMapper ,然後AuthorName手動分配。

  • 覆蓋 base 的GetListAsync方法,該方法CrudAppService返回書籍列表。邏輯與前面的方法類似,因此您可以輕鬆理解代碼。

  • 創建了一個新方法: GetAuthorLookupAsync. 這個簡單得到所有作者。UI 使用此方法填充下拉列表並在創建/編輯書籍時進行選擇和創作。

1627036184

// 在 Book 實體定義導覽屬性 public Author Author { get; set; } 可以直接取得關聯實體資料Author.Name

1627037183

// 這邊體驗了一把單元測試的好處,照下方修改單元測試後,確實有正確拿到 AuthorName!

對像到對象映射配置

AuthorLookupDtoGetAuthorLookupAsync方法中引入了類和使用的對象映射。因此,我們需要BookStoreApplicationAutoMapperProfile.csAcme.BookStore.Application項目文件中添加一個新的映射定義:

CreateMap<Author, AuthorLookupDto>();

單元測試

由於我們對BookAppService的修改導致單元測試會失敗。 打開 Acme.BookStore.Application.Tests項目Books文件夾中的 BookAppService_Tests ,修改內容如下:

using System; using System.Linq; using System.Threading.Tasks; using Acme.BookStore.Authors; using Shouldly; using Volo.Abp.Application.Dtos; using Volo.Abp.Validation; using Xunit; namespace Acme.BookStore.Books { public class BookAppService_Tests : BookStoreApplicationTestBase { private readonly IBookAppService _bookAppService; private readonly IAuthorAppService _authorAppService; public BookAppService_Tests() { _bookAppService = GetRequiredService<IBookAppService>(); _authorAppService = GetRequiredService<IAuthorAppService>(); } [Fact] public async Task Should_Get_List_Of_Books() { //Act var result = await _bookAppService.GetListAsync( new PagedAndSortedResultRequestDto() ); //Assert result.TotalCount.ShouldBeGreaterThan(0); result.Items.ShouldContain(b => b.Name == "1984" && b.AuthorName == "George Orwell"); } [Fact] public async Task Should_Create_A_Valid_Book() { var authors = await _authorAppService.GetListAsync(new GetAuthorListDto()); var firstAuthor = authors.Items.First(); //Act var result = await _bookAppService.CreateAsync( new CreateUpdateBookDto { AuthorId = firstAuthor.Id, Name = "New test book 42", Price = 10, PublishDate = System.DateTime.Now, Type = BookType.ScienceFiction } ); //Assert result.Id.ShouldNotBe(Guid.Empty); result.Name.ShouldBe("New test book 42"); } [Fact] public async Task Should_Not_Create_A_Book_Without_Name() { var exception = await Assert.ThrowsAsync<AbpValidationException>(async () => { await _bookAppService.CreateAsync( new CreateUpdateBookDto { Name = "", Price = 10, PublishDate = DateTime.Now, Type = BookType.ScienceFiction } ); }); exception.ValidationErrors .ShouldContain(err => err.MemberNames.Any(m => m == "Name")); } } }
  • 改變斷言條件在Should_Get_List_Of_Booksb => b.Name == "1984"b => b.Name == "1984" && b.AuthorName == "George Orwell"檢查,如果作者的名字充滿。

  • 更改了在創建新書時Should_Create_A_Valid_Book設置的方法AuthorId ,因為它不再需要了。

1627037702

// 這邊改完尋覽屬性跑了一下上面的單元測試,成功通過測試,這樣 publish 的時候可以減少一些低級錯誤,離菜鳥又遠了一些吧!

public override async Task<BookDto> GetAsync(Guid id) { var book = await Repository.GetAsync(id); var bookDto = ObjectMapper.Map<Book, BookDto>(book); bookDto.AuthorName = book.Author.Name; return bookDto; }

// 以下前端部分快速帶過

用戶界面

服務代理生成

由於 HTTP API 已更改,您需要更新 Angular 客戶端服務代理 。在運行generate-proxy命令之前,您的主機必須已啟動並正在運行。

angular文件夾中運行以下命令(您可能需要停止 angular 應用程序):

abp generate-proxy

此命令將更新文件/src/app/proxy/夾下的服務代理文件。

書單

圖書列表頁面更改是微不足道的。打開/src/app/book/book.component.html並在NameType列之間添加以下列定義:

<ngx-datatable-column [name]="'::Author' | abpLocalization" prop="authorName" [sortable]="false" ></ngx-datatable-column>

當您運行應用程序時,您可以在表格上看到Author列:

書店書籍與作者姓名角度

創建/編輯表單

下一步是向創建/編輯表單添加作者選擇(下拉列表)。最終的 UI 將如下所示:

書店角度作者選擇

添加了作者下拉列表作為表單中的第一個元素。

打開/src/app/book/book.component.ts和修改內容如下圖:

import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; import { BookService, BookDto, bookTypeOptions, AuthorLookupDto } from '@proxy/books'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Component({ selector: 'app-book', templateUrl: './book.component.html', styleUrls: ['./book.component.scss'], providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>; form: FormGroup; selectedBook = {} as BookDto; authors$: Observable<AuthorLookupDto[]>; bookTypes = bookTypeOptions; isModalOpen = false; constructor( public readonly list: ListService, private bookService: BookService, private fb: FormBuilder, private confirmation: ConfirmationService ) { this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items)); } ngOnInit() { const bookStreamCreator = (query) => this.bookService.getList(query); this.list.hookToQuery(bookStreamCreator).subscribe((response) => { this.book = response; }); } createBook() { this.selectedBook = {} as BookDto; this.buildForm(); this.isModalOpen = true; } editBook(id: string) { this.bookService.get(id).subscribe((book) => { this.selectedBook = book; this.buildForm(); this.isModalOpen = true; }); } buildForm() { this.form = this.fb.group({ authorId: [this.selectedBook.authorId || null, Validators.required], name: [this.selectedBook.name || null, Validators.required], type: [this.selectedBook.type || null, Validators.required], publishDate: [ this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, Validators.required, ], price: [this.selectedBook.price || null, Validators.required], }); } save() { if (this.form.invalid) { return; } const request = this.selectedBook.id ? this.bookService.update(this.selectedBook.id, this.form.value) : this.bookService.create(this.form.value); request.subscribe(() => { this.isModalOpen = false; this.form.reset(); this.list.get(); }); } delete(id: string) { this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => { if (status === Confirmation.Status.confirm) { this.bookService.delete(id).subscribe(() => this.list.get()); } }); } }
  • 添加了AuthorLookupDto, Observable和 的導入map

  • 後添加authors$: Observable<AuthorLookupDto[]>;字段selectedBook

  • 添加this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items));到構造函數中。

  • 添加 authorId: [this.selectedBook.authorId || null, Validators.required],buildForm()函數中。

打開/src/app/book/book.component.html並在書名表單組之前添加以下表單組:

<div class="form-group"> <label for="author-id">Author</label><span> * </span> <select class="form-control" id="author-id" formControlName="authorId"> <option [ngValue]="null">Select author</option> <option [ngValue]="author.id" *ngFor="let author of authors$ | async"> {{ author.name }} </option> </select> </div>

就這樣。只需運行應用程序並嘗試創建或編輯作者。

// 完結,灑花!

Jakeuj

PS5

  • ABP

  • 回首頁

本文章從點部落遷移至 Writerside

14 October 2025