如何根据 ASP.NET MVC (many-to-many) 中的图书 ID 显示所有作者

How to display all Authors based on ID of Book in ASP.NET MVC (many-to-many)

我正在尝试建立一个图书馆。在我的主页上,我设法显示所有书籍的所有详细信息。当我点击作者页面时,我想显示他所有的书(图片、标题和作者)。问题是当我的书有多个作者时,我无法显示所有作者。

我的主页: Screenshot

作者: Screenshot

型号:

 public class Book : IEntityBase
 {
    [Key]
    public int Id { get; set; }
    public string Title { get; set; }
    public DateTime RelaseDate { get; set; }
    public string Description { get; set; }
    public string ImageURL { get; set; }
    public bool IsBorrowed { get; set; }
    public string ISBN { get; set; }

    //Relationships
    public int PublisherId { get; set; }
    [ForeignKey("PublisherId")]
    public virtual Publisher Publisher { get; set; }

    public int CategoryId { get; set; }
    [ForeignKey("CategoryId")]
    public virtual Category Categories { get; set; }

    public virtual ICollection <Author_Book> Authors_Books { get; set; }
}
public class Author_Book
{
    public int AuthorId { get; set; }
    public virtual Author Author { get; set; }

    public int BookId { get; set; }
    public virtual Book Book { get; set; }
}
public class Author : IEntityBase
{
    [Key]
    public int Id { get; set; }
    
    [Display(Name = "Autor")]
    [Required(ErrorMessage = "Author jest wymagany")]
    public string FullName { get; set; }

    [Display(Name = "Biografia")]
    [Required(ErrorMessage = "Biografia jest wymagana")]
    public string Bio { get; set; }

    [Display(Name = "Zdjęcie")]
    public string ImageURL { get; set; }

    //Relationships
    public virtual ICollection<Author_Book> Authors_Books { get; set; }
    public virtual ICollection<Author_Publisher> Author_Publisher { get; set; }
    public virtual ICollection<Author_Category> Author_Category { get; set; }
}

作者控制器:

    //GET: author/details
    public async Task<IActionResult> Details(int id)
    {
        var authorDetails = await _service.GetAuthorByIdAsync(id);
        if (authorDetails == null) return View("NotFound");
        return View(authorDetails);
    }

方法 GetAuthorbyIdAsync:

    public async Task<Author> GetAuthorByIdAsync(int id)
    {
        var bookDetails = await _db.Authors
            .Include(ab => ab.Authors_Books)
            .ThenInclude(b => b.Book)
            .FirstOrDefaultAsync(n => n.Id == id);
        return bookDetails;
    }

作者查看详情

@model Author
<div class="row">
<div class="col-md-8 offset-2">
    <div class="card mb-12">
        <div class="row g-0">
            <div class="col-md-12">
                <div class="card-header">
                    <p class="card-text">
                        <h1 class="card-title">
                            @Model.FullName
                        </h1>
                    </p>
                </div>
            </div>
            <div class="col-md-2">
                <img src="@Model.ImageURL" width="100%" alt="@Model.FullName">
            </div>
            <div class="col-md-10">
                <div class="card-body">

                    <p class="card-text">@Model.Bio</p>
                </div>
            </div>
            <div class="col-md-12">
                <h2 class="card-text text-center"> Wypożycz inne książki @Model.FullName:</h2>
                <div class="row m-2">
                    @foreach (var book in Model.Authors_Books)
                    {
                        <div class="col-12 col-sm-6 col-xl-4">
                            <div class="card h-100" style="width: 250px;">
                                <div class="card-body text-center">
                                    <img src="@book.Book.ImageURL" width="100%" alt="@book.Book.Title">
                                    <br />
                                    <br />
                                    <h5 class="card-text"><b>@book.Book.Title</b></h5>

                                    <h6 class="card-text"><b>@book.Author.FullName</b></h6>
                                    
                                    <a class="btn btn-outline-primary" asp-controller="Book" asp-action="Details" asp-route-id="@book.Book.Id">
                                         Więcej
                                    </a>

                                </div>
                            </div>
                        </div>
                    }
                </div>
            </div>

            <div class="col-md-12">
                <div class="card-footer">
                    <p class="card-text">
                        <a class="btn btn-outline-success float-right" asp-action="Edit" asp-route-id="@Model.Id">Edytuj</a>
                        <a class="btn btn-outline-secondary" asp-controller="Book" asp-action="Index">Wróć</a>
                    </p>
                </div>
            </div>
        </div>
    </div>
</div>

这里有两个重要问题,第一个是您甚至没有尝试显示与 Book 相关的 Author 记录,您显示的是 Book.Title但不是与本书相关的 Author 条记录:

<h5 class="card-text"><b>@book.Book.Title</b></h5>
<h6 class="card-text"><b>@book.Author.FullName</b></h6>

至少你会期望

<h5 class="card-text"><b>@book.Book.Title</b></h5>
<h6 class="card-text"><b>@book.Book.Author.FullName</b></h6>

但是那是行不通的,因为有很多 Author 与本书相关的记录,您已经在主页上正确地展示了这些记录。您可以使用这个简单的 C# 表达式创建与本书相关的作者的 CSV 字符串:

String.Join(", ", book.Book.Authors_Books.Select(ba => ba.Author.FullName))

因此将您的模板更改为:

<h5 class="card-text"><b>@book.Book.Title</b></h5>
<h6 class="card-text"><b>@(String.Join(", ", book.Book.Authors_Books.Select(ba => ba.Author.FullName)))</b></h6>

但要使其正常工作,我们需要通过附加一个 ThenInclude 来告诉 EF 同时加载其他引用,这将从数据库加载正确的数据。

public async Task<Author> GetAuthorByIdAsync(int id)
{
    var authorAndBooks = await _db.Authors
        .Include(author => author.Authors_Books)
            .ThenInclude(authorBook => authorBook.Book)
                .ThenInclude(book => book.Authors_Books)
        .FirstOrDefaultAsync(author => author.Id == id);
    return authorAndBooks;
}

*注意:我重命名了您查询中的一些变量以更好地表示它们相关的数据类型,看来您误解了 lambda 变量类型

If your view is rendered server-side, or you have forced serialization to include references then you may not see any issues.


当涉及到 views 和序列化时,结果并不总是如你所愿,当数据被发送回视图时,在大多数情况下它会是使用故意排除递归实体引用的逻辑进行序列化。当发生这种情况时,结果图 parent 中的 Author 及其关联的 Author_Book 将不会包含在嵌套引用中。

看到这个fiddle:https://dotnetfiddle.net/JCF4dB

您可以看到 authorDetails 的序列化输出在 Authors_Books 的第二个数组中只有一条记录,这是意料之中的。要解释原因,请查看 Authors_Books 的第一个实例,请注意 "Author" 属性 一起被省略了,即使您在运行时通过代码查询它也会 return最外面,或者parentAuthorWaris Dirie

{
  "Id": 1,
  "FullName": "Waris Dirie",
  ...
  "Authors_Books": [
    {
      "Id": 1,
      "AuthorId": 1,
      "BookId": 1,
      "Book": {}
    }
  ]
}

如果不排除引用,您将得到这样的递归循环:

{
  "Id": 1,
  "FullName": "Waris Dirie",
  ...
  "Authors_Books": [
    {
      "Id": 1,
      "AuthorId": 1,
      "Author": 
        {
          "Id": 1,
          "FullName": "Waris Dirie",
          ...
          "Authors_Books": [
            {
              "Id": 1,
              "AuthorId": 1,
              "Author": 
                {
                  "Id": 1,
                  "FullName": "Waris Dirie",
                  ...
                  "Authors_Books": [
                    {
                      "Id": 1,
                      "AuthorId": 1,
                      "Author": { ... }
                      "BookId": 1,
                      "Book": {}
                    }
                  ]        
                },
              "BookId": 1,
              "Book": {}
            }
          ]        
        },
      "BookId": 1,
      "Book": {}
    }
  ]
}

相同的机制可防止一个数组中的 object 在一个完全不同的数组中引用,而不会通过序列化有效负载进行投影。

解决这个问题的一种方法是识别如果 Book 已经通过 parent Author_Book 记录链接到 Author,那么我们应该使用该引用在渲染列表中排在第一位,后面附有其他作者。要将其可视化,请更改您的模板:

<h6 class="card-text">
    <b>@book.Author.FullName, @(String.Join(", ",  book.Book.Authors_Books.Select(ba => ba.Author.FullName)))</b>
</h6>

或者您可以将 parent 记录添加到 child 数组中,这可以处理只有一个作者而没有任何附加条件逻辑的情况。使用 Union 函数创建一个包含所有记录的可枚举对象:

book.Book.Authors_Books.Union(new[] { book }).Select(ba => ba.Author.FullName)

将其放回原始模板中:

<h6 class="card-text">
    <b>@book.Book.Authors_Books.Union(new[] { book }).Select(ba => ba.Author.FullName)))</b>
</h6>

这些EF M:N(多对多)关系的解决方案一般是为了避免递归循环,只在一个方向上遍历关系,但是作为你可以在这里看到,只要对这些数据结构的行为方式和原因有一点了解和理解,我们就可以解决默认序列化对我们施加的限制。


要在您编写代码时进一步帮助识别这些数据概念,请更改您的集合 属性 名称以使其更有意义,请尝试以下任一概念:

public class Book : IEntityBase
{
    [Key]
    public int Id { get; set; }
    ...
    public virtual ICollection <Author_Book> Book_Authors { get; set; }
}
public class Author : IEntityBase
{
    [Key]
    public int Id { get; set; }
    ...
    public virtual ICollection <Author_Book> Author_Books { get; set; }
}

public class Book : IEntityBase
{
    [Key]
    public int Id { get; set; }
    ...
    public virtual ICollection <Author_Book> Authors { get; set; }
}
public class Author : IEntityBase
{
    [Key]
    public int Id { get; set; }
    ...
    public virtual ICollection <Author_Book> Books { get; set; }
}