前言:上次创建完项目后鸽了这么久,终于想起了我的账号&密码...
回归正题,我这次来尝试做个的数据层。
对于一个博客系统而言至少需要可对文章进行增删查改操作,除了对文章的操作外,还需要作者的相关信息以及该系统的访问情况。当然一个个人博客系统也可能会有用户(暂时先不考虑实现)。
以上应该足够构成一个博客系统的基本组成。
那么它们的关系大概是这样的:
作者:文章 = 1:n (一个作者可有多篇文章)
文章:系统访问情况 = 1:1(一篇文章有一份记录,记录该文章访问情况)
ok,关系还挺简单(嘿嘿,主要是做的简单)
那么这次就用Entity Framework Core和postgresql帮我完成数据层持久化操作。
需要安装一些nuget包:
- Npgsql.EntityFrameworkCore.PostgreSQL
PostgreSQL数据提供的支持EF Core的基础类库,是通过EF Core使用PostgreSQL数据库的根本。 - Npgsql.EntityFrameworkCore.PostgreSQL.Design
使用Guid(对应Postgre数据的类型为uuid)类型的主键必须,int/long类型的主键不添加也没问题。
接下来,先创建实体。
文章实体部分:
/// <summary>
/// 文章实体
/// </summary>
public class Article
{
/// <summary>
/// 文章Id
/// </summary>
public int ArticleId { get; set; }
/// <summary>
/// 文章标题
/// </summary>
public string Title { get; set; } = default!;
/// <summary>
/// 文章内容(客户端传输base64编码内容,服务端将内容转为文本后存为静态文件,再将文件路径存入数据库)
/// </summary>
public string Content { get; set; } = default!;
/// <summary>
/// 文章创建时间
/// </summary>
public DateTime CreateTime { get; set; } = DateTime.Now;
/// <summary>
/// 文章修改时间(若未被修改则该时间为空)
/// </summary>
public DateTime? ModifyTime { get; set; }
//=================文章与作者的关系=======================
public virtual int AuthorId { get; set; }
public virtual Author Author { get; set; } = default!;
//=================文章与文章记录关系=====================
public virtual int LogId { get; set; }
public virtual ArticleLog Log { get; set; }
}
文章记录实体部分:
/// <summary>
/// 文章记录
/// </summary>
public class ArticleLog
{
/// <summary>
/// 文章记录Id
/// </summary>
public int ArticleLogId { get; set; }
/// <summary>
/// 文章浏览次数
/// </summary>
public int ViewNum { get; set; }
//===============文章与记录的关系==================
//因为文章与记录为一对一关系,所以只需一方有个外键就行了。在这里我将外键映射放在文章实体中
//public virtual int ArticleId { get; set; }
public virtual Article Article { get; set; } = default!;
}
作者实体部分:
/// <summary>
/// 作者实体
/// </summary>
public class Author
{
/// <summary>
/// 作者Id
/// </summary>
public int AuthorId { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 作者账号
/// </summary>
public string Account { get; set; } = default!;
/// <summary>
/// 作者账号的密码
/// </summary>
public string Password { get; set; } = default!;
/// <summary>
/// 上次登录时间
/// </summary>
public DateTime LoginTime { get; set; }
//=================作者与文章的关系================
public virtual IEnumerable<Article>? Articles { get; set; }
}
注意:
1.如果使用了开启可空类型而在使用引用类型不初始化,则会警告或报错类型为空(导致编译不通过),所以上述代码中对于引用类型使用了default!
来初始化(看起来是没问题了),but,这样做只是编译通过了,它的赋值结果还是null。
2.实体之间关系需要使用virtual关键字修饰
接下来添加映射关系(使用EFCore中为我们定义的IEntityTypeConfiguration
文章实体与数据库之间的映射关系:
public class ArticleConfig : IEntityTypeConfiguration<Article>
{
public void Configure(EntityTypeBuilder<Article> builder)
{
//在数据库中该实体被映射为表:article
builder.ToTable("article");
//以实体中ArticleId字段映射为数据库中名称为pk_id的主键
builder.HasKey(k => k.ArticleId);
//将主键映射为pk_id为名称、INTEGER为类型的自增长字段
builder.Property(k => k.ArticleId).ValueGeneratedOnAdd().HasColumnType("INTEGER").HasColumnName("pk_id");
//实体中Content字段段映射为数据库中名称为content、类型为VARCHAR(50)非空的字段
builder.Property(p => p.Title).HasColumnType("VARCHAR(50)").HasColumnName("title").IsRequired();
//实体中Content字段段映射为数据库中名称为content、类型为VARCHAR(512)非空的字段
builder.Property(p => p.Content).HasColumnType("VARCHAR(512)").HasColumnName("content").IsRequired();
//实体中CreateTime字段段映射为数据库中名称为create_time、类型为DATE的非空字段
builder.Property(p => p.CreateTime).HasColumnType("DATE").HasColumnName("create_time").IsRequired();
//实体中ModifyTime字段段映射为数据库中名称为modify_time、类型为DATE的可空字段
builder.Property(p => p.ModifyTime).HasColumnType("DATE").HasColumnName("modify_time").IsRequired(false);
//文章与作者之间的关系,使用文章实体中AuthorId字段作为外键,映射为数据库中名为fk_article_id、类型为INTEGER的非空外键
builder.Property(f => f.AuthorId).HasColumnType("INTEGER").HasColumnName("fk_article_id").IsRequired();
builder.HasOne(o => o.Author).WithMany(m => m.Articles).HasForeignKey(f => f.AuthorId);
//文章与文章记录之间的关系,使用文章实体中LogId字段作为外键,映射为数据库中名为fk_log_id、类型为INTEGER的非空外键
builder.Property(f=>f.LogId).HasColumnType("INTEGER").HasColumnName("fk_log_id").IsRequired();
builder.HasOne(o => o.Log).WithOne(o => o.Article).HasForeignKey<Article>(f => f.LogId);
//文章与作者关系已在作者实体配置,此处不需要重复配置
}
}
文章记录实体与数据库之间的映射关系:
/// <summary>
/// 文章记录实体与数据库之间映射关系
/// </summary>
public class ArticleLogConfig : IEntityTypeConfiguration<ArticleLog>
{
public void Configure(EntityTypeBuilder<ArticleLog> builder)
{
//在数据库中该实体被映射为表:article_log
builder.ToTable("article_log");
//以实体中ArticleLogId字段映射为数据库中名称为pk_id、INTEGER类型的自增长主键
builder.HasKey(k => k.ArticleLogId);
//将主键映射为pk_id为名称、INTEGER为类型的自增长字段
builder.Property(k => k.ArticleLogId).ValueGeneratedOnAdd().HasColumnType("INTEGER").HasColumnName("pk_id");
//实体中ViewNum字段段映射为数据库中名称为view_num、类型为INTEGER的非空字段
builder.Property(p => p.ViewNum).HasColumnType("INTEGER").HasColumnName("view_num").IsRequired();
//映射关系已经在文章配置中配置,所以此处不需要配置
}
}
作者实体与数据库之间映射关系
/// <summary>
/// 作者实体与数据库之间映射关系
/// </summary>
public class AuthorConfig : IEntityTypeConfiguration<Author>
{
public void Configure(EntityTypeBuilder<Author> builder)
{
//在数据库中该实体被映射为表:author
builder.ToTable("author");
//以实体中AuthorId字段映射为数据库中主键
builder.HasKey(k => k.AuthorId);
//将主键映射为pk_id为名称、INTEGER为类型的自增长字段
builder.Property(k => k.AuthorId).ValueGeneratedOnAdd().HasColumnType("INTEGER").HasColumnName("pk_id");
//实体中Name字段段映射为数据库中名称为name、类型为VARCHAR(20)的非空字段
builder.Property(p => p.Name).HasColumnType("VARCHAR(20)").HasColumnName("name").IsRequired();
//实体中Account字段段映射为数据库中名称为account、类型为VARCHAR(40)的非空字段
builder.Property(p => p.Account).HasColumnType("VARCHAR(40)").HasColumnName("account").IsRequired();
//实体中Password字段段映射为数据库中名称为account、类型为VARCHAR(32)的非空字段
//密码部分使用MD5摘要后存入数据库(毕竟不能限制密码长度=_=)
builder.Property(p => p.Password).HasColumnType("VARCHAR(32)").HasColumnName("password").IsRequired();
//实体中LoginTime字段段映射为数据库中名称为login_time、类型为DATE的非空字段
builder.Property(p => p.LoginTime).HasColumnType("DATE").HasColumnName("login_time").IsRequired();
}
}
好啦,数据实体与映射规则有了,还需要帮我们完成映射的上下文,emmmmmmmmm就叫DbManager吧。
/// <summary>
/// 管理数据库的连接与实体的映射
/// </summary>
public class DbManager:DbContext
{
//数据库连接字符串
private string? _connectString;
public DbManager(string connectString)
{
_connectString = connectString;
}
/// <summary>
/// 应用实体映射规则(没错就是我们创建的以Config结尾的那些类)
/// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//这里将自动加载所有的映射类=_=
//如果希望手动加载,则(以AuthorConfig为例):
//modelBuilder.ApplyConfiguration(new AuthorConfig());
//使用Assembly.GetExecutingAssembly();会有性能问题
//使用typeof(TSelf).Assembly表示当前程序集
modelBuilder.ApplyConfigurationsFromAssembly(typeof(DbManager).Assembly);
base.OnModelCreating(modelBuilder);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (_connectString!=null)
{
optionsBuilder.UseNpgsql(_connectString);
}
base.OnConfiguring(optionsBuilder);
}
}
为了确保延迟加载,避免导航属性的循环引用,需要引入个nuget包:
Microsoft.EntityFrameworkCore.Proxies
最终的DbManager代码:
/// <summary>
/// 管理数据库的连接与实体的映射
/// </summary>
public class DbManager:DbContext
{
//数据库连接字符串
private string? _connectString;
public DbManager(string connectString)
{
_connectString = connectString;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(DbManager).Assembly);
base.OnModelCreating(modelBuilder);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (_connectString!=null)
{
optionsBuilder.UseNpgsql(_connectString);
}
//确认启用延迟加载,以免自动加载导航属性从而循环引用问题
optionsBuilder.UseLazyLoadingProxies();
base.OnConfiguring(optionsBuilder);
}
}
行了,现在我们的实体有了,数据库配置也行了,我们还差个对外的调用类,就取名为IDbMapper和DbMapper吧。(什么?为什么有两?嘿嘿,因为在dotnet core中一般是以注入的方式提供服务)
(先来波同步哒)
/// <summary>
/// 对外的数据层访问接口
/// </summary>
public interface IDbMapper
{
/// <summary>
/// 向数据库中加入实体数据
/// </summary>
/// <typeparam name="TEntity">待加入的实体类型</typeparam>
/// <param name="entity">待加入的实体数据</param>
/// <returns>被追踪的实体对象</returns>
TEntity? Add<TEntity>(TEntity entity) where TEntity : class, new();
/// <summary>
/// 删除数据库中的实体数据
/// </summary>
/// <typeparam name="TEntity">待删除的实体类型</typeparam>
/// <param name="entity">待删除的实体数据</param>
/// <returns>被删除实体数据</returns>
TEntity? Delete<TEntity>(TEntity entity) where TEntity : class, new();
/// <summary>
/// 更新数据库中数据
/// </summary>
/// <typeparam name="TEntity">待更新的实体类型</typeparam>
/// <param name="entity">待更新的实体类型</param>
/// <returns>返回追踪的实体数据</returns>
TEntity? Update<TEntity>(TEntity entity) where TEntity : class, new();
/// <summary>
/// 获取所有实体数据
/// </summary>
/// <typeparam name="TEntity">待获取的实体类型</typeparam>
/// <returns>数据库中数据列表</returns>
IEnumerable<TEntity> GetAllEntities<TEntity>() where TEntity : class, new();
/// <summary>
/// 获取第一个实体数据
/// </summary>
/// <typeparam name="TEntity">待获取的实体类型</typeparam>
/// <returns>数据库中第一个该类型数据</returns>
TEntity? GetFirstEntity<TEntity>() where TEntity : class, new();
/// <summary>
/// 获取符合条件的数据列表
/// </summary>
/// <typeparam name="TEntity">待获取的数据类型</typeparam>
/// <param name="exp">Lamda条件语句</param>
/// <returns>符合条件的数据列表</returns>
IEnumerable<TEntity> GetEntities<TEntity>(Expression<Func<TEntity, bool>> exp) where TEntity : class, new();
/// <summary>
/// 获取一个符合条件的数据
/// </summary>
/// <typeparam name="TEntity">待获取的数据类型</typeparam>
/// <param name="exp">Lamda条件语句</param>
/// <returns>符合条件的数据</returns>
TEntity? GetEntity<TEntity>(Expression<Func<TEntity, bool>> exp) where TEntity : class, new();
}
DbMapper的实现部分代码
/// <summary>
/// 数据访问层实现
/// </summary>
public class DbMapper:IDbMapper
{
protected DbManager dbManager { get; }
public DbMapper(DbManager dbManager)
{
this.dbManager = dbManager;
}
protected virtual IEnumerable<TEntity> CompileQuery<TEntity>(Expression<Func<TEntity, bool>> exp) where TEntity : class, new()
{
var fn = EF.CompileQuery((DbManager context, Expression<Func<TEntity, bool>> exps) =>
context.Set<TEntity>().Where(exps));
return fn(dbManager, exp);
}
protected virtual TEntity? CompileQuerySingle<TEntity>(Expression<Func<TEntity, bool>> exp) where TEntity : class, new()
{
var fn = EF.CompileQuery((DbManager context, Expression<Func<TEntity, bool>> exps) => context.Set<TEntity>().FirstOrDefault(exps));
return fn(dbManager, exp);
}
public TEntity? Add<TEntity>(TEntity entity) where TEntity : class, new()
{
TEntity? result = null;
try
{
var trace = dbManager.Set<TEntity>().Add(entity);
dbManager.SaveChanges();
result = trace.Entity;
}
catch
{
//TODO:记录异常
}
return result;
}
public TEntity? Delete<TEntity>(TEntity entity) where TEntity : class, new()
{
TEntity? result = null;
try
{
var trace = dbManager.Set<TEntity>().Remove(entity);
dbManager.SaveChanges();
result = trace.Entity;
}
catch
{
//TODO:记录异常
}
return result;
}
public TEntity? Update<TEntity>(TEntity entity) where TEntity : class, new()
{
TEntity? result = null;
try
{
var trace = dbManager.Set<TEntity>().Update(entity);
dbManager.SaveChanges();
result = trace.Entity;
}
catch
{
//TODO:记录异常
}
return result;
}
public IEnumerable<TEntity> GetEntities<TEntity>(Expression<Func<TEntity, bool>> exp) where TEntity : class, new()
{
return CompileQuery(exp);
}
public TEntity? GetEntity<TEntity>(Expression<Func<TEntity, bool>> exp) where TEntity : class, new()
{
return CompileQuerySingle(exp);
}
public IEnumerable<TEntity> GetAllEntities<TEntity>() where TEntity : class, new()
{
return dbManager.Set<TEntity>().ToList();
}
public TEntity? GetFirstEntity<TEntity>() where TEntity : class, new()
{
return dbManager.Set<TEntity>().FirstOrDefault();
}
}
最终代码结构:
呼~,最后我们来测试一波~
测试方法:
[TestMethod]
public void DbDataTest()
{
var connectString = @"咱的数据库连接字符串~~~";
var dbManager = new DbManager(connectString);
var dbMapper = new DbMapper(dbManager);
//确保数据库被删除啦
dbManager.Database.EnsureDeleted();
//确保数据库被新建
dbManager.Database.EnsureCreated();
dbMapper.Add(new Author
{
Name = "233",
Account = "233",
Password = "233",
LoginTime=DateTime.Now
});
var author = dbMapper.GetFirstEntity<Author>();
Assert.IsTrue(author.Name == "233");
}
}
结果如下:
数据库中数据:
打完收工~~~~