zoukankan      html  css  js  c++  java
  • 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.1

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

    文章目录

    此分支项目代码

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

    需求分析

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

    用户表:

    密码表:

    角色表:

    好像博客园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. 在代码中直接创建数据库

    一、三两种方法的差别我在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的日志:

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

  • 相关阅读:
    2014找工作----扎实的基础和开阔的视野是企业最看重的因素
    2014找工作总结-机会往往留给有准备的人
    【STL源码剖析读书笔记】【第1章】STL概论与版本简介
    【c++ primer读书笔记】【第13章】拷贝控制
    【c++ primer读书笔记】【第12章】动态内存
    【c++ primer读书笔记】【第11章】关联容器
    【c++ primer读书笔记】【第10章】泛型算法
    【c++ primer读书笔记】【第9章】顺序容器
    WebSocket 是什么原理?为什么可以实现持久连接
    IDEA将项目导出war包方法(详细)
  • 原文地址:https://www.cnblogs.com/kasnti/p/12238012.html
Copyright © 2011-2022 走看看