ASP.NET Core搭建多层网站架构【5-网站数据库实体设计及映射配置】

2020/01/29, ASP.NET Core 3.1, VS2019, EntityFrameworkCore 3.1.1, Microsoft.Extensions.Logging.Console 3.1.1, Microsoft.Extensions.Logging.Debug 3.1.1html

摘要:基于ASP.NET Core 3.1 WebApi搭建后端多层网站架构【5-网站数据库实体设计及映射配置】
网站数据库实体设计,使用EntityFrameworkCore 3.1 FluentAPI映射配置实体,网站启动时建立数据库并添加种子数据,开发调试时能够看到执行的具体sql语句mysql

文章目录git

此分支项目代码github

本章节介绍后台管理的网站数据库实体设计,使用FluentAPI方式配置数据库字段映射,网站启动时建立数据库并添加种子数据算法

需求分析

首先要实现的功能有用户登陆、角色管理、日志记录
大概有四张表:用户表、密码表、角色表、日志表
日志表:
sql

用户表:
数据库

密码表:
json

角色表:
后端

好像博客园md不支持表格功能?因此只能截图展现,excel表格上传至项目docs文件夹中架构

字段设计说明

  • 日志表主键Id是数据库自增的,也就是在向数据库插入日志时,不用管Id,往里写入就行
  • 用户表、角色表的Id都是long类型的,也就是使用雪花算法生成的Id
  • 密码表的主键是Account,UserId是用户表外键
  • 用户表和角色表拥有StatusCode、Creator、CreateTime、Modifier、ModifyTime,标明该记录的状态、建立时间等信息

建立实体类

MS.Entities类库中添加Core文件夹,在Core文件夹中添加IEntity.cs类:

using System;

namespace MS.Entities.Core
{
    //没有Id主键的实体继承这个
    public interface IEntity
    {
    }
    //有Id主键的实体继承这个
    public abstract class BaseEntity : IEntity
    {
        public long Id { get; set; }
        public StatusCode StatusCode { get; set; }
        public long? Creator { get; set; }
        public DateTime? CreateTime { get; set; }
        public long? Modifier { get; set; }
        public DateTime? ModifyTime { get; set; }
    }
}

在Core中新建StatusCode.cs枚举:

using System.ComponentModel;

namespace MS.Entities.Core
{
    public enum StatusCode
    {
        [Description("已删除")]
        Deleted = -1,//软删除,已删除的没法恢复,没法看见,暂未使用
        [Description("生效")]
        Enable = 0,
        [Description("失效")]
        Disable = 1//失效的还能够改成生效
    }
}

日志表

MS.Entities类库中添加Logrecord.cs类:

using MS.Entities.Core;
using System;

namespace MS.Entities
{
    public class Logrecord : IEntity
    {
        public int Id { get; set; }
        public DateTime LogDate { get; set; }
        public string LogLevel { get; set; }
        public string Logger { get; set; }
        public string Message { get; set; }
        public string Exception { get; set; }
        public string MachineName { get; set; }
        public string MachineIp { get; set; }
        public string NetRequestMethod { get; set; }
        public string NetRequestUrl { get; set; }
        public string NetUserIsauthenticated { get; set; }
        public string NetUserAuthtype { get; set; }
        public string NetUserIdentity { get; set; }
    }
}

角色表

MS.Entities类库中添加Role.cs类:

using MS.Entities.Core;

namespace MS.Entities
{
    public class Role : BaseEntity
    {
        public string Name { get; set; }
        public string DisplayName { get; set; }
        public string Remark { get; set; } 
    }
}

用户表

MS.Entities类库中添加User.cs类:

using MS.Entities.Core;

namespace MS.Entities
{
    public class User : BaseEntity
    {
        public string Account { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public string Phone { get; set; }
        public long RoleId { get; set; } 

        public Role Role { get; set; }
    }
}

密码表

MS.Entities类库中添加UserLogin.cs类:

using MS.Entities.Core;
using System;

namespace MS.Entities
{
    public class UserLogin : IEntity
    {
        public long UserId { get; set; }
        public string Account { get; set; }
        public string HashedPassword { get; set; }
        public DateTime? LastLoginTime { get; set; }
        public int AccessFailedCount { get; set; }
        public bool IsLocked { get; set; }
        public DateTime? LockedTime { get; set; }

        public User User { get; set; }
    }
}

至此,实体类都已完成设计
项目完成后,以下图

建立映射配置

MS.DbContexts类库添加包引用:

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.1" />
  <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.1" />
</ItemGroup>

这两个包给DbContext扩展日志记录,能够实现查看EFCore生成的sql语句,具体使用方法后文会提到

MS.DbContexts类库中引用MS.EntitiesMS.UnitOfWork类库
MS.DbContexts类库中添加Mappings文件夹,在该文件夹中添加 LogrecordMap.csRoleMap.csUserLoginMap.csUserMap.cs

LogrecordMap.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MS.Entities;

namespace MS.DbContexts
{
    public class LogrecordMap : IEntityTypeConfiguration<Logrecord>
    {
        public void Configure(EntityTypeBuilder<Logrecord> builder)
        {
            builder.ToTable("TblLogrecords");
            builder.HasKey(c => c.Id);//自增主键
            builder.Property(c => c.LogDate).IsRequired();
            builder.Property(u => u.LogLevel).IsRequired().HasMaxLength(50);
            builder.Property(u => u.Logger).IsRequired().HasMaxLength(256);
            builder.Property(u => u.Message);
            builder.Property(u => u.Exception);
            builder.Property(u => u.MachineName).HasMaxLength(50);
            builder.Property(u => u.MachineIp).HasMaxLength(50);
            builder.Property(u => u.NetRequestMethod).HasMaxLength(10);
            builder.Property(u => u.NetRequestUrl).HasMaxLength(500);
            builder.Property(u => u.NetUserIsauthenticated).HasMaxLength(10);
            builder.Property(u => u.NetUserAuthtype).HasMaxLength(50);
            builder.Property(u => u.NetUserIdentity).HasMaxLength(50);
        }
    }
}

RoleMap.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MS.Entities;

namespace MS.DbContexts
{
    public class RoleMap : IEntityTypeConfiguration<Role>
    {
        public void Configure(EntityTypeBuilder<Role> builder)
        {
            builder.ToTable("TblRoles");
            builder.HasKey(c => c.Id);
            builder.Property(c => c.Id).ValueGeneratedNever();
            builder.HasIndex(c => c.Name).IsUnique();//指定索引,不能重复
            builder.Property(c => c.Name).IsRequired().HasMaxLength(16);
            builder.Property(c => c.DisplayName).IsRequired().HasMaxLength(50);
            builder.Property(c => c.Remark).HasMaxLength(4000);
            builder.Property(c => c.Creator).IsRequired();
            builder.Property(c => c.CreateTime).IsRequired();
            builder.Property(c => c.Modifier);
            builder.Property(c => c.ModifyTime);
            //builder.HasQueryFilter(b => b.StatusCode != StatusCode.Deleted);//默认不查询软删除数据
        }
    }
}

UserLoginMap.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MS.Entities;

namespace MS.DbContexts
{
    public class UserLoginMap : IEntityTypeConfiguration<UserLogin>
    {
        public void Configure(EntityTypeBuilder<UserLogin> builder)
        {
            builder.ToTable("TblUserLogins");
            builder.HasKey(c => c.Account);
            //builder.Property(c => c.UserId).ValueGeneratedNever();
            builder.Property(c => c.Account).IsRequired().HasMaxLength(20);
            builder.Property(c => c.HashedPassword).IsRequired().HasMaxLength(256);
            builder.Property(c => c.LastLoginTime);
            builder.Property(c => c.AccessFailedCount).IsRequired().HasDefaultValue(0);
            builder.Property(c => c.IsLocked).IsRequired();
            builder.Property(c => c.LockedTime);
            builder.HasOne(c => c.User);
        }
    }
}

UserMap.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MS.Entities;
using MS.Entities.Core;

namespace MS.DbContexts
{
    public class UserMap : IEntityTypeConfiguration<User>
    {
        public void Configure(EntityTypeBuilder<User> builder)
        {
            builder.ToTable("TblUsers");
            builder.HasKey(c => c.Id);
            builder.Property(c => c.Id).ValueGeneratedNever();
            builder.HasIndex(c => c.Account).IsUnique();//指定索引
            builder.Property(c => c.Account).IsRequired().HasMaxLength(16);
            builder.Property(c => c.Name).IsRequired().HasMaxLength(50);
            builder.Property(c => c.Email).HasMaxLength(100);
            builder.Property(c => c.Phone).HasMaxLength(25);
            builder.Property(c => c.RoleId).IsRequired();
            builder.Property(c => c.StatusCode).IsRequired().HasDefaultValue(StatusCode.Enable);
            builder.Property(c => c.Creator).IsRequired();
            builder.Property(c => c.CreateTime).IsRequired();
            builder.Property(c => c.Modifier);
            builder.Property(c => c.ModifyTime);

            builder.HasOne(c => c.Role);
            //builder.HasQueryFilter(b => b.StatusCode != StatusCode.Deleted);//默认不查询软删除数据
        }
    }
}

至此映射配置完成

说明

  • User和Role映射中注释掉了HasQueryFilter全局过滤查询,如须要可自行开启
  • LogrecordMap中Id仅配置主键,因此默认是数据库自增主键
  • RoleMap、UserMap中Id设为ValueGeneratedNever,不自动生成值,咱们使用雪花算法生成Id赋值
  • UserMap中配置了HasOne(Role),代表关联性,因此RoleId能自动映射为Role表的Id外键,UserLoginMap中的UserId也是如此
  • UserMap中手动显式指定了表名为TblUsers,加"Tbl"前缀是为了不和数据库默认关键字重复

创建DbContext上下文

MS.DbContexts类库中添加MSDbContext.cs类:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace MS.DbContexts
{
    public class MSDbContext : DbContext
    {
        //Add-Migration InitialCreate
        //Update-Database InitialCreate
        public MSDbContext(DbContextOptions<MSDbContext> options)
           : base(options)
        {
        }
        //此处用微软原生的控制台日志记录,若是使用NLog极可能数据库还没建立,形成记录日志到数据库性能降低(一直在尝试链接数据库,可是数据库还没建立)
        //此处使用静态实例,这样不会为每一个上下文实例建立新的 ILoggerFactory 实例,这一点很是重要。 不然会致使内存泄漏和性能降低。
        //此处使用了Debug和console两种日志输出,会输出到控制台和调试窗口
        public static readonly ILoggerFactory MyLoggerFactory = LoggerFactory.Create(builder => builder.AddDebug().AddConsole());
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            optionsBuilder.UseLoggerFactory(MyLoggerFactory);
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfiguration(new LogrecordMap());
            modelBuilder.ApplyConfiguration(new RoleMap());
            modelBuilder.ApplyConfiguration(new UserLoginMap());
            modelBuilder.ApplyConfiguration(new UserMap());

            base.OnModelCreating(modelBuilder);
        }
        
    }
}

说明:

  • 使用了微软原生的控制台日志记录,若是使用NLog极可能数据库还没建立,形成记录日志到数据库性能降低(一直在尝试链接数据库,可是数据库还没建立)
  • 使用静态实例,这样不会为每一个上下文实例建立新的 ILoggerFactory 实例,这一点很是重要。 不然会致使内存泄漏和性能降低。
  • 使用了Debug和console两种日志输出,会输出到控制台和调试窗口

至此,数据访问层建立完毕,项目完成后以下图所示

建立数据种子

目前我所知道的数据库的建立有三种(生成sql语句单独执行建立暂不讨论):

  1. 先建立迁移文件,而后在代码中自动迁移
  2. 使用.NET Core CLI命令建立数据库
  3. 在代码中直接建立数据库

1、三两种方法的差异我在EFCore自动迁移中写过,第一种方法有个缺点是若是建立迁移时使用MySQL数据库,编译好代码后,部署的环境必须是一样的数据库,而第三种方法没有这个问题。
第二种方法须要使用到CLI命令工具单独执行,因此我没有考虑

我选择直接建立,项目启动时,检查数据库是否存在,若是不存在则建立,建立成功后开始写入种子数据。

添加包引用

MS.WebApi应用程序中添加MySQL包引用,若是你使用SQL server,安装Microsoft.EntityFrameworkCore.SqlServer包便可:

<ItemGroup>
  <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
</ItemGroup>

我写本章节时,仍是3.1.0版本,可是写到第8.1章的时候升级了3.1.1,本文改为了3.1.1,代码中8.1以后的全部分支都改为了最新版本,可是在此以前的分支依然是3.1.0没有去作更新改动了(其实用起来区别也不大)

添加数据种子方法

MS.WebApi应用程序中添加Initialize文件夹,把自带的Startup.cs类移至Initialize文件夹中
在Initialize文件夹新建DBSeed.cs类:

using MS.Common.Security;
using MS.DbContexts;
using MS.Entities;
using MS.Entities.Core;
using MS.UnitOfWork;
using System;

namespace MS.WebApi
{
    public static class DBSeed
    {
        /// <summary>
        /// 数据初始化
        /// </summary>
        /// <param name="unitOfWork"></param>
        /// <returns>返回是否建立了数据库(非迁移)</returns>
        public static bool Initialize(IUnitOfWork<MSDbContext> unitOfWork)
        {
            bool isCreateDb = false;
            //直接自动执行迁移,若是它建立了数据库,则返回true
            if (unitOfWork.DbContext.Database.EnsureCreated())
            {
                isCreateDb = true;
                //打印log-建立数据库及初始化期初数据

                long rootUserId = 1219490056771866624;

                #region 角色、用户、登陆
                Role rootRole = new Role
                {
                    Id = 1219490056771866625,
                    Name = "SuperAdmin",
                    DisplayName = "超级管理员",
                    Remark = "系统内置超级管理员",
                    Creator = rootUserId,
                    CreateTime = DateTime.Now
                };
                User rootUser = new User
                {
                    Id = rootUserId,
                    Account = "admin",
                    Name = "admin",
                    RoleId = rootRole.Id,
                    StatusCode = StatusCode.Enable,
                    Creator = rootUserId,
                    CreateTime = DateTime.Now,
                };

                unitOfWork.GetRepository<Role>().Insert(rootRole);
                unitOfWork.GetRepository<User>().Insert(rootUser);
                unitOfWork.GetRepository<UserLogin>().Insert(new UserLogin
                {
                    UserId = rootUserId,
                    Account = rootUser.Account,
                    HashedPassword = Crypto.HashPassword(rootUser.Account),//默认密码同帐号名
                    IsLocked = false
                });
                unitOfWork.SaveChanges();

                #endregion
            }
            return isCreateDb;
        }


    }
}

上面的DBSeed中:

  • EnsureCreated方法确保建立了数据库(若是数据库不存在则建立并返回true,存在则返回false)
  • 建立了一个超级管理员角色,建立了一个超级管理员用户admin(密码同帐号)

添加数据库链接字符串

appsettings.json中添加数据库链接字符串(具体的链接自行配置):

"ConectionStrings": {
    "MSDbContext": "server=192.168.137.10;database=MSDB;user=root;password=mysql@local;"
  }

修改后以下图所示:

开启EntityFrameworkCore日志

appsettings.Development.json的"Logging:LogLevel"节点添加:

"Microsoft.EntityFrameworkCore": "Information"

修改完成后,以下图所示

为何要把开启EntityFrameworkCore日志写在appsettings.Development.json文件里呢?
由于appsettings.Development.json文件是默认开发时使用的配置,也就是只在开发时才开启EFCore的日志记录,实际生产环境不开启

注册工做单元

Startup.cs类,ConfigureServices方法中添加如下代码:

//using MS.DbContexts;
//using MS.UnitOfWork;
//using Microsoft.EntityFrameworkCore;
//以上添加到using引用
services.AddUnitOfWorkService<MSDbContext>(options => { options.UseMySql(Configuration.GetSection("ConectionStrings:MSDbContext").Value); });

说明:

  • 《1-项目结构分层创建》中,MS.WebApi应用程序引用了MS.Services,层层套娃,最终引用了MS.UnitOfWork,因此可使用AddUnitOfWorkService方法
  • 这里注册数据库用的是MySQL,因此是UseMySql方法

修改网站启动逻辑

Program.cs类中,修改Main方法为如下内容(覆盖原先的Main方法内容):

//using MS.DbContexts;
//using MS.UnitOfWork;
//以上代码添加到using
public static void Main(string[] args)
{
    try
    {
        var host = CreateHostBuilder(args).Build();
        using (IServiceScope scope = host.Services.CreateScope())
        {
            //初始化数据库
            DBSeed.Initialize(scope.ServiceProvider.GetRequiredService<IUnitOfWork<MSDbContext>>());
        }
        host.Run();
    }
    catch (Exception ex)
    {
        throw;
    }
}

至此,全部的修改已完成,网站启动将执行DBSeed.Initialize方法来初始化数据
项目完成后,以下图

启动项目,此时能够看见控制台EntityFramworkCore的日志:

而数据库中也生成了对应的数据库:

相关文章
相关标签/搜索