与 Entity Framework Core 的多对多关系
与 Entity Framework Core 的高级关系(续)
本系列教程现已提供在线视频课程。您可以在 YouTube 上观看第一个小时的课程,或在 Udemy 上获取完整课程。您也可以继续阅读。尽情享受吧!:)
与 Entity Framework Core 的高级关系(续)
与技能的多对多关系
使用 Entity Framework Core 实现多对多关系与实现其他关系略有不同。
在我们的角色扮演游戏示例中,我们添加了一系列技能,所有角色都可以使用。这意味着,每个角色的某项特定技能无法升级。每个角色都可以从众多技能中选择。所以,一般来说,即使是骑士也可以投掷火球,法师也可以疯狂地击溃对手。
Skill
当然,首先要做的是添加模型。
我们创建一个新的 C# 类并添加属性Id
、Name
和Damage
。
public class Skill
{
public int Id { get; set; }
public string Name { get; set; }
public int Damage { get; set; }
}
注意,我们这里没有添加 type 列表Character
。如果我们想要实现一对多关系,我们会添加 type 列表。但是对于多对多关系,我们需要一个特殊的实现——那就是连接表。
Entity Framework Core 目前无法自行创建连接表。因此,我们必须手动添加一个连接表,并告诉 Entity Framework 如何连接两个实体Skill
和Character
。
首先,让我们为这个实体添加模型。我们创建一个新的 C# 类并将其命名为CharacterSkill
。现在,为了连接技能和角色,我们必须将它们添加为属性。因此,我们添加了Character
和Skill
。
此外,我们需要为该实体添加一个主键。它将是 和 的复合键Skill
。Character
为了实现这一点,按照惯例,我们CharacterId
为Character
和SkillId
分别添加一个属性Skill
。
public class CharacterSkill
{
public int CharacterId { get; set; }
public Character Character { get; set; }
public int SkillId { get; set; }
public Skill Skill { get; set; }
}
但这还不是全部的魔法。我们仍然需要告诉 Entity Framework Core,我们想要使用这两个 ID 作为复合主键。我们借助Fluent API来实现这一点。
我们将焦点转移到DataContext
类上。首先,我们添加新的DbSet
属性Skills
和CharacterSkills
。
public DbSet<Skill> Skills { get; set; }
public DbSet<CharacterSkill> CharacterSkills { get; set; }
之后,我们需要添加一些新内容。我们重写了方法OnModelCreating()
。该方法接受一个ModelBuilder
参数,该参数“定义实体的形状、实体之间的关系以及它们如何映射到数据库”。这正是我们需要的。
我们这里唯一需要配置的是由和CharacterSkill
组成的实体的复合键。我们使用 来实现。CharacterId
SkillId
modelBuilder.Entity<CharacterSkill>().HasKey(cs => new { cs.CharacterId, cs.SkillId });
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CharacterSkill>()
.HasKey(cs => new { cs.CharacterId, cs.SkillId });
}
就是这样。由于使用了CharacterId
和 的命名约定SkillId
,我们无需配置任何其他内容。否则,我们将不得不使用 Fluent API 来配置角色和技能之间的关系,例如HasOne()
和 之类的方法WithMany()
。但 Entity Framework Core 会识别这些,我们很快就能在迁移文件中看到正确的实现。
我们还要做最后一件事,那就是将CharacterSkill
列表添加到Character
模型中Skill
。
因此,在这两个 C# 类中,我们都添加了一个CharacterSkills
类型为 的新属性List<CharacterSkill>
。
public class Skill
{
public int Id { get; set; }
public string Name { get; set; }
public int Damage { get; set; }
public List<CharacterSkill> CharacterSkills { get; set; }
}
public class Character
{
public int Id { get; set; }
public string Name { get; set; } = "Frodo";
public int HitPoints { get; set; } = 100;
public int Strength { get; set; } = 10;
public int Defense { get; set; } = 10;
public int Intelligence { get; set; } = 10;
public RpgClass Class { get; set; } = RpgClass.Knight;
public User User { get; set; }
public Weapon Weapon { get; set; }
public List<CharacterSkill> CharacterSkills { get; set; }
}
好的。所有内容保存完成后,我们就可以运行迁移了。
首先,我们使用 添加新的迁移dotnet ef migrations add Skill
。
在创建的迁移文件中,您可以看到将为我们生成两个新表,Skills
和CharacterSkills
。
CharacterSkill
在迁移设计文件中,再往下一点,您现在可以看到加入实体与实体Character
和之间的关系的配置Skill
。
modelBuilder.Entity("dotnet_rpg.Models.CharacterSkill", b =>
{
b.HasOne("dotnet_rpg.Models.Character", "Character")
.WithMany("CharacterSkills")
.HasForeignKey("CharacterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("dotnet_rpg.Models.Skill", "Skill")
.WithMany("CharacterSkills")
.HasForeignKey("SkillId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
再次,由于使用了Id
属性的命名约定,我们不必手动执行此操作。
现在是时候将此迁移添加到数据库中了dotnet ef database update
。
更新完成后,您可以刷新 SQL Server Management Studio 中的数据库并查看新表Skills
和CharacterSkills
正确的键。
太棒了!现在该往这些表格里填充一些内容了。
为角色扮演游戏角色添加技能
向数据库的技能池中添加新技能非常简单。我们需要一个服务、一个接口、一个控制器等等。我想说,我们更专注于添加 RPG 角色和这些技能之间的关联。
因此,我们不需要添加技能服务,而是使用 SQL Server Management Studio 在数据库中手动添加一些技能。
只需右键单击Skills
表格并选择“编辑前 200 行”。
现在我们可以添加一些技能,如火球、狂暴或暴风雪。
太好了。现在我们可以专注于关系了。我已经剧透了,我们需要一些 DTO。让我们创建一个新文件夹,并添加具有属性和 的CharacterSkill
新 C# 类。AddCharacterSkillDto
CharacterId
SkillId
namespace dotnet_rpg.Dtos.CharacterSkill
{
public class AddCharacterSkillDto
{
public int CharacterId { get; set; }
public int SkillId { get; set; }
}
}
接下来,我们创建文件夹Skill
并创建 DTO GetSkillDto
,因为我们只需要它来显示角色的技能。我们需要的属性是Name
和Damage
。
namespace dotnet_rpg.Dtos.Skill
{
public class GetSkillDto
{
public string Name { get; set; }
public int Damage { get; set; }
}
}
之后,我们向 中添加另一个属性,GetCharacterDto
即Skills
类型List<GetSkillsDto>
。
public class GetCharacterDto
{
public int Id { get; set; }
public string Name { get; set; } = "Frodo";
public int HitPoints { get; set; } = 100;
public int Strength { get; set; } = 10;
public int Defense { get; set; } = 10;
public int Intelligence { get; set; } = 10;
public RpgClass Class { get; set; } = RpgClass.Knight;
public GetWeaponDto Weapon { get; set; }
public List<GetSkillDto> Skills { get; set; }
}
请注意,我们已经直接访问了技能,而无需CharacterSkill
先使用连接实体。您稍后将看到我们是如何实现这一点的。
好的,DTO 已经准备好了,现在我们可以转到服务和控制器文件。
我们创建一个名为 的新文件夹CharacterSkillService
,并添加一个名为 的新接口ICharacterSkillService
。
ServiceResponse
我们只添加一个返回a 的方法,GetCharacterDto
因为类似于 ,WeaponService
这样我们就可以看到添加的技能了。我们调用该方法AddCharacterSkill()
,并将 作为一个参数传入AddCharacterSkillDto
。当然,在执行此操作时,我们必须添加一些 using 指令。
using System.Threading.Tasks;
using dotnet_rpg.Dtos.Character;
using dotnet_rpg.Dtos.CharacterSkill;
using dotnet_rpg.Models;
namespace dotnet_rpg.Services.CharacterSkillService
{
public interface ICharacterSkillService
{
Task<ServiceResponse<GetCharacterDto>> AddCharacterSkill(AddCharacterSkillDto newCharacterSkill);
}
}
现在我们可以创建了CharacterSkillService
。这项服务看起来与非常相似WeaponService
。我们首先实现接口,然后自动ICharacterSkillService
添加方法并添加关键字。AddCharacterSkill()
async
public class CharacterSkillService : ICharacterSkillService
{
public async Task<ServiceResponse<GetCharacterDto>> AddCharacterSkill(AddCharacterSkillDto newCharacterSkill)
{
throw new NotImplementedException();
}
}
在编写此方法的实际代码之前,我们先添加构造函数。类似于WeaponService
我们注入的DataContext
、IHttpContextAccessor
和IMapper
。我们添加 using 指令,从参数初始化所有字段,并且如果需要,在每个字段前面添加下划线。
private readonly DataContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMapper _mapper;
public CharacterSkillService(DataContext context, IHttpContextAccessor httpContextAccessor, IMapper mapper)
{
_mapper = mapper;
_httpContextAccessor = httpContextAccessor;
_context = context;
}
现在来谈谈AddCharacterSkill()
方法。
首先,我们初始化返回ServiceResponse
并构建一个空的 try/catch 块。
如果发生异常,我们已经可以将Success
的状态设置response
为false
,并将设置Message
为异常消息。
public async Task<ServiceResponse<GetCharacterDto>> AddCharacterSkill(AddCharacterSkillDto newCharacterSkil
{
ServiceResponse<GetCharacterDto> response = new ServiceResponse<GetCharacterDto>();
try
{
}
catch (Exception ex)
{
response.Success = false;
response.Message = ex.Message;
}
return response;
}
接下来将从Character
数据库中接收CharacterId
通过给出的正确信息AddCharacterSkillDto
。
再次,它与非常相似WeaponService
。
首先,我们Characters
从 访问_context
,并使用 的方法进行过滤FirstOrDefaultAsync()
,并根据 进行过滤newCharacterSkill.CharacterId
,此外,还通过 进行身份验证User
。你还记得这一行很长的代码,它用于从 claims 中获取用户 ID,对吧?
Character character = await _context.Characters
.FirstOrDefaultAsync(c => c.Id == newCharacterSkill.CharacterId &&
c.User.Id == int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)));
但这还不是全部。为了获取所有Weapon
技能以及用户的相关内容,我们必须将它们包含在内。
我们可以从 开始。添加Weapon
之后,技能会变得更有趣一些。我们再次添加,但首先我们访问,然后使用访问 的child属性。_context.Characters
.Include(c => c.Weapon)
.Include()
CharacterSkills
Skill
CharacterSkills
.ThenInclude()
这样,我们就可以从character
存储在数据库中的每个属性中获取。
Character character = await _context.Characters
.Include(c => c.Weapon)
.Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
.FirstOrDefaultAsync(c => c.Id == newCharacterSkill.CharacterId &&
c.User.Id == int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)));
搞定这些之后,我们添加常用的空值检查。所以,如果character
是,null
我们就设置Success
状态,Message
然后 并返回response
。
Character character = await _context.Characters
.Include(c => c.Weapon)
.Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
.FirstOrDefaultAsync(c => c.Id == newCharacterSkill.CharacterId &&
c.User.Id == int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier))
if (character == null)
{
response.Success = false;
response.Message = "Character not found.";
return response;
}
接下来是Skill
。通过参数给出的SkillId
,newCharacterSkill
我们从数据库中获取技能。
Skill skill = await _context.Skills
.FirstOrDefaultAsync(s => s.Id == newCharacterSkill.SkillId);
与 类似character
,如果我们找不到具有给定 的技能SkillId
,我们将设置ServiceResponse
并返回它。
if (skill == null)
{
response.Success = false;
response.Message = "Skill not found.";
return response;
}
现在我们拥有了创建新 所需的一切CharacterSkill
。
我们初始化一个新characterSkill
对象,并将该对象的Character
和Skill
属性设置为我们之前从数据库中获得的character
和。skill
CharacterSkill characterSkill = new CharacterSkill
{
Character = character,
Skill = skill
};
之后,我们将这个新内容添加CharacterSkill
到数据库中AddAsync(characterSkill)
,将所有更改保存到数据库中,最后将设置response.Data
为映射的character
。
await _context.CharacterSkills.AddAsync(characterSkill);
await _context.SaveChangesAsync();
response.Data = _mapper.Map<GetCharacterDto>(character);
这就是整个AddCharacterSkill()
方法。
public async Task<ServiceResponse<GetCharacterDto>> AddCharacterSkill(AddCharacterSkillDto newCharacterSkill)
{
ServiceResponse<GetCharacterDto> response = new ServiceResponse<GetCharacterDto>();
try
{
Character character = await _context.Characters
.Include(c => c.Weapon)
.Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
.FirstOrDefaultAsync(c => c.Id == newCharacterSkill.CharacterId &&
c.User.Id == int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier))
if (character == null)
{
response.Success = false;
response.Message = "Character not found.";
return response;
}
Skill skill = await _context.Skills
.FirstOrDefaultAsync(s => s.Id == newCharacterSkill.SkillId);
if (skill == null)
{
response.Success = false;
response.Message = "Skill not found.";
return response;
}
CharacterSkill characterSkill = new CharacterSkill
{
Character = character,
Skill = skill
};
await _context.CharacterSkills.AddAsync(characterSkill);
await _context.SaveChangesAsync();
response.Data = _mapper.Map<GetCharacterDto>(character);
}
catch (Exception ex)
{
response.Success = false;
response.Message = ex.Message;
}
return response;
}
为了能够调用该服务,我们需要CharacterSkillController
,所以让我们创建这个新的 C# 文件。
与往常一样,我们从 、和派生并ControllerBase
添加属性。我们需要用户信息,因此此控制器只能由经过身份验证的用户访问。[Route(“[controller]”)]
[ApiController]
[Authorize]
[Authorize]
[ApiController]
[Route("[controller]")]
public class CharacterSkillController : ControllerBase
然后我们需要一个仅注入的构造函数ICharacterSkillService
。
private readonly ICharacterSkillService _characterSkillService;
public CharacterSkillController(ICharacterSkillService characterSkillService)
{
_characterSkillService = characterSkillService;
}
最后,我们添加一个带有as 参数的public async
POST
方法,该参数将传递给的方法。AddCharacterSkill()
AddCharacterSkillDto
AddCharacterSkill()
_characterSkillService
[HttpPost]
public async Task<IActionResult> AddCharacterSkill(AddCharacterSkillDto newCharacterSkill)
{
return Ok(await _characterSkillService.AddCharacterSkill(newCharacterSkill));
}
到目前为止控制器。
现在我们在文件中注册新的服务Startup.cs
。和往常一样,我们services.AddScoped()
在方法中使用 for ConfigureServices()
。
services.AddScoped<ICharacterSkillService, CharacterSkillService>();
最后一件事是对 进行改变AutoMapperProfile
。
简单的部分是一张新的地图GetSkillDto
。
CreateMap<Skill, GetSkillDto>();
现在事情变得更有趣了。我已经告诉过你,我们想要直接访问角色的技能,而不显示连接实体CharacterSkill
。我们可以借助 AutoMapper 和函数来实现Select()
。
首先,我们利用-MapForMember()
函数<Character, GetCharacterDto>
。通过此函数,我们可以为映射类型的特定成员定义特殊的映射。
在我们的例子中,我们希望正确设置Skills
DTO。
为了做到这一点,我们访问Character
对象,并从该对象(即函数MapFrom()
)中抓取CharacterSkills
并从每个中选择。Skill
CharacterSkill
CreateMap<Character, GetCharacterDto>()
.ForMember(dto => dto.Skills, c => c.MapFrom(c => c.CharacterSkills.Select(cs => cs.Skill)));
这就是我们直接掌握技能的方法。
太棒了!现在该测试一下了。
确保用户已登录并已获取正确的令牌。然后,我们可以使用http://localhost:5000/characterskill
带有 HTTP 方法 的URL POST
。调用主体由characterId
和组成skillId
。
{
"characterid" : 5,
"skillid" : 1
}
执行此调用后,我们将获得完整的 RPG 角色及其武器和新技能。
{
"data": {
"id": 5,
"name": "Frodo",
"hitPoints": 200,
"strength": 10,
"defense": 10,
"intelligence": 10,
"class": 1,
"weapon": {
"name": "The Master Sword",
"damage": 10
},
"skills": [
{
"name": "Fireball",
"damage": 30
}
]
},
"success": true,
"message": null
}
当我们添加另一项技能时,我们会看到完整的技能阵列。
{
"data": {
"id": 5,
"name": "Frodo",
"hitPoints": 200,
"strength": 10,
"defense": 10,
"intelligence": 10,
"class": 1,
"weapon": {
"name": "The Master Sword",
"damage": 10
},
"skills": [
{
"name": "Fireball",
"damage": 30
},
{
"name": "Frenzy",
"damage": 20
}
]
},
"success": true,
"message": null
}
在数据库中,您还可以看到连接表已填充新的 ID。
完美!RPG角色装备了武器和技能。
请随意尝试一下。
如果您想使用GetCharacterById()
中的方法CharacterService
查看任何角色的装备,请确保添加Include()
如前所示的方法,这意味着包括Weapon
以及Skills
的CharacterSkills
。
public async Task<ServiceResponse<GetCharacterDto>> GetCharacterById(int id)
{
ServiceResponse<GetCharacterDto> serviceResponse = new ServiceResponse<GetCharacterDto>();
Character dbCharacter =
await _context.Characters
.Include(c => c.Weapon)
.Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
.FirstOrDefaultAsync(c => c.Id == id && c.User.Id == GetUserId());
serviceResponse.Data = _mapper.Map<GetCharacterDto>(dbCharacter);
return serviceResponse;
}
当你所有的 RPG 角色都设置好后,我想是时候战斗了!
概括
恭喜!您已在应用程序中实现了所有类型的关系。
但这还不是全部。
在本章中,首先,你学习了如何通过 Web 服务调用获取已验证的用户,并从数据库中获取基于该用户的正确数据。这样,你就能向每位用户展示她自己的角色。
之后,我们讨论了一对一的关系。一个RPG角色现在可以装备一件武器,而且只能装备这一件武器。
关于多对多关系,我们将技能添加到角色中,并添加必要的连接实体或表CharacterSkills
。角色可以拥有多项技能,技能也可以拥有多个角色。
除此之外,您还创建了所有必要的服务和控制器来添加武器和技能,并且您学习了如何包含更深的嵌套实体以及如何使用 AutoMapper定义自定义映射。
在下一章中,我们将更进一步,实现让RPG角色相互战斗的功能。
这就是本系列教程的第 11 部分。希望对你有所帮助。想要收到下一部分的通知,只需在dev.to上关注我或订阅我的新闻通讯即可。你将第一时间知道。
下次再见!
小心。
接下来:.NET Core 3.1 不仅仅是 CRUD
图片由 cornecoba 在freepik.com上创建。
但请稍等,还有更多!
- 让我们在Twitter、YouTube、LinkedIn或dev.to上建立联系。
- 免费获取5 个软件开发人员职业秘诀。
- 请访问patrickgod.com获取更多对您的开发生活和职业生涯有价值的文章。