编程语言
首页 > 编程语言> > ASP.NET Core 中的 ORM 之 Entity Framework

ASP.NET Core 中的 ORM 之 Entity Framework

作者:互联网

EF Core 简介

Entity Framework Core 是微软自家的 ORM 框架。作为 .Net Core 生态中的一个重要组成部分,它是一个支持跨平台的全新版本,用三个词来概况 EF Core 的特点:轻量级、可扩展、跨平台。

目前 EF Core 支持的数据库:

使用 EF Core(Code First)

  1. 新建一个 WebAPI 项目

  2. 通过 Nuget 安装 EF Core 引用

    // SQL Server
    Install-Package Microsoft.EntityFrameworkCore.SqlServer

    其他数据库请查看:https://docs.microsoft.com/zh-cn/ef/core/providers/

  3. 添加实体

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
        public int Rating { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
  4. 添加数据库上下文

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }

    有两种方式配置数据库连接,一种是注册 Context 的时候提供 options。比较推荐这种方式。

    public class BloggingContext : DbContext
    {
        public BloggingContext(DbContextOptions<BloggingContext> options)
            : base(options)
        { }
    
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }

    在 Startup 中配置

    public void ConfigureServices(IServiceCollection services)
    {
        var connectionString = @"Server=.;Database=Blogging;Trusted_Connection=True;";
        services.AddDbContext<BloggingContext>(o => o.UseSqlServer(connectionString));
    
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

    一种是重载 OnConfiguring 方法提供连接字符串:

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=.;Database=Blogging;Trusted_Connection=True;");
            base.OnConfiguring(optionsBuilder);
        }
    }
  5. 在Controller 中使用 Context

    public class BlogsController : ControllerBase
    {
        private readonly BloggingContext _context;
    
        public BlogsController(BloggingContext context)
        {
            _context = context;
        }
    
        // GET: api/Blogs
        [HttpGet]
        public IEnumerable<Blog> GetBlogs()
        {
            return _context.Blogs;
        }
    }

迁移 Migration

  1. 通过 Nuget 引入EF Core Tool 的引用

    Install-Package Microsoft.EntityFrameworkCore.Tools

    如果需要使用 dotnet ef 命令, 请添加 Microsoft.EntityFrameworkCore.Tools.DotNet

  2. 生成迁移

    打开Package Manager Console,执行命令 Add-Migration InitialCreate。
    执行成功后会在项目下生成一个 Migrations目录,包含两个文件:

更新迁移到数据库

执行命令 Update-Database。
如果执行成功,数据库应该已经创建成功了。现在可以测试刚才创建的WebAPI应用了。

使用代码 Database.Migrate(); 可以达到同样的目的

    public BloggingContext(DbContextOptions<BloggingContext> options)
        : base(options)
    {
        Database.Migrate();
    }

EF Core 中的一些常用知识点

实体建模

EF 根据对 Model 的配置生成表和字段,主要有三种配置方式:

实体关系

种子数据

填充种子数据可以让我们在首次使用应用之前向数据库中插入一些初始化数据。有两种方法:

并发管理

数据库并发指的是多个进程或用户同时访问或更改数据库中的相同数据的情况。 并发控制指的是用于在发生并发更改时确保数据一致性的特定机制。

EF Core 默认支持乐观并发控制,这意味着它将允许多个进程或用户独立进行更改而不产生同步或锁定的开销。 在理想情况下,这些更改将不会相互影响,因此能够成功。 在最坏的情况下,两个或更多进程将尝试进行冲突更改,其中只有一个进程应该成功。

处理冲突的策略:

执行 SQL 语句和存储过程

EF Core 使用以下方法执行 SQL 语句和存储过程:

  1. SQL 查询必须返回实体或查询类型的所有属性的数据
  2. 结果集中的列名必须与属性映射到的列名称匹配。
  3. SQL 查询不能包含相关数据。 但是可以使用 Include 运算符返回相关数据。
  4. 不要使用 TOP 100 PERCENT 或 ORDER BY 等子句。可以通过 Linq 在代码里面编写。

DbContext.Database.ExecuteSqlCommand()

ExecuteSqlCommand方法返回一个整数,表示执行的SQL语句影响的行数。有效的操作是 INSERT、UPDATE 和 DELETE,不能用于返回实体。

测试一下 INSERT:

int affectRows = _context.Database.ExecuteSqlCommand($"Insert into Blogs([Url],[Rating])Values({blog.Url}, {blog.Rating})");

通过 SQL Server Profiler 查看 SQL 语句:

exec sp_executesql N'Insert into Blogs([Url],[Rating])Values(@p0, @p1)',N'@p0 nvarchar(4000),@p1 int',@p0=N'testurl',@p1=3

延迟加载和预先加载

EF Core 通过在模型中使用导航属性来加载相关实体。 有三种常见模式可用于加载相关数据。

  1. Nuget 安装 Microsoft.EntityFrameworkCore.Proxies

  2. 调用 UseLazyLoadingProxies 来启用延迟加载。

    services.AddDbContext<BloggingContext>(option => option.UseLazyLoadingProxies().UseSqlServer(connectionString));
  3. 导航属性添加 virtual 修饰符。

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
        public int Rating { get; set; }
    
        public virtual IList<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public int BlogId { get; set; }
        public virtual Blog Blog { get; set; }
    }
  4. 测试,当代码执行到var posts = blog.Posts时候,会去数据库里面查询Posts记录。

    var blog = await _context.Blogs.FindAsync(id);
    var posts = blog.Posts;

    尽量避免在循环时候使用延迟加载,会导致每次循环都去访问数据库。

IQueryable 和 IEnumerable

直接通过一个实例测试一下:

var testIQueryable = _context.Blogs.Where(r => r.Rating > 10);
var testIEnumerable = _context.Blogs.AsEnumerable().Where(r => r.Rating > 10);

var testIQueryableList = testIQueryable.ToList();
var testIEnumerableList = testIEnumerable.ToList();

查看生产的 SQL 语句

IQueryable 是将 Linq 表达式翻译成 T-SQL 语句之后再向 SQL 服务器发送命令.
IEnumerable 是在调用自己的 Linq 方法之前先从 SQL 服务器取到数据并加载到本地内存中。

生成迁移 SQL 脚本

EF Core 将迁移更新到生产环境可以使用 Script-Migration 命令生成sql脚本,然后到生产数据库执行.

此命令有几个选项。

待补充...

SQL 监视工具

有几种方法可以监视 EF Core 自动生成的 SQL 语句:

  1. Nuget 安装 MiniProfiler 引用

    Install-Package MiniProfiler.AspNetCore.Mvc
    Install-Package MiniProfiler.EntityFrameworkCore
  2. 修改 SwaggerUI/index.html 页面: 在项目下面新建一个文件 SwaggerIndex.html 并复制以下代码,设置编译为 Embedded resource

    <script async="async" id="mini-profiler" src="/profiler/includes.min.js?v=4.0.138+gcc91adf599" data-version="4.0.138+gcc91adf599" data-path="/profiler/" data-current-id="4ec7c742-49d4-4eaf-8281-3c1e0efa748a" data-ids="" data-position="Left" data-authorized="true" data-max-traces="15" data-toggle-shortcut="Alt+P" data-trivial-milliseconds="2.0" data-ignored-duplicate-execute-types="Open,OpenAsync,Close,CloseAsync"></script>
    
    <!-- HTML for static distribution bundle build -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>%(DocumentTitle)</title>
        <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
        <link rel="stylesheet" type="text/css" href="./swagger-ui.css">
        <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
        <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
        <style>
            html {
                box-sizing: border-box;
                overflow: -moz-scrollbars-vertical;
                overflow-y: scroll;
            }
    
            *,
            *:before,
            *:after {
                box-sizing: inherit;
            }
    
            body {
                margin: 0;
                background: #fafafa;
            }
        </style>
        %(HeadContent)
    </head>
    
    <body>
    
        <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
            <defs>
                <symbol viewBox="0 0 20 20" id="unlocked">
                    <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
                </symbol>
    
                <symbol viewBox="0 0 20 20" id="locked">
                    <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z" />
                </symbol>
    
                <symbol viewBox="0 0 20 20" id="close">
                    <path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z" />
                </symbol>
    
                <symbol viewBox="0 0 20 20" id="large-arrow">
                    <path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z" />
                </symbol>
    
                <symbol viewBox="0 0 20 20" id="large-arrow-down">
                    <path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z" />
                </symbol>
    
    
                <symbol viewBox="0 0 24 24" id="jump-to">
                    <path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z" />
                </symbol>
    
                <symbol viewBox="0 0 24 24" id="expand">
                    <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" />
                </symbol>
    
            </defs>
        </svg>
    
        <div id="swagger-ui"></div>
    
        <!-- Workaround for https://github.com/swagger-api/swagger-editor/issues/1371 -->
        <script>
            if (window.navigator.userAgent.indexOf("Edge") > -1) {
                console.log("Removing native Edge fetch in favor of swagger-ui's polyfill")
                window.fetch = undefined;
            }
        </script>
    
        <script src="./swagger-ui-bundle.js"></script>
        <script src="./swagger-ui-standalone-preset.js"></script>
        <script>
            window.onload = function () {
                var configObject = JSON.parse('%(ConfigObject)');
                var oauthConfigObject = JSON.parse('%(OAuthConfigObject)');
                // Apply mandatory parameters
                configObject.dom_id = "#swagger-ui";
                configObject.presets = [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset];
                configObject.layout = "StandaloneLayout";
                // If oauth2RedirectUrl isn't specified, use the built-in default
                if (!configObject.hasOwnProperty("oauth2RedirectUrl"))
                    configObject.oauth2RedirectUrl = window.location.href.replace("index.html", "oauth2-redirect.html");
                // Build a system
                const ui = SwaggerUIBundle(configObject);
                // Apply OAuth config
                ui.initOAuth(oauthConfigObject);
            }
        </script>
    </body>
    
    </html>
      <ItemGroup>
        <EmbeddedResource Include="SwaggerIndex.html" />
      </ItemGroup>
  3. 在 Startup 中配置 MiniProfiler: 在 ConfigureServices 里面添加services.AddMiniProfiler().AddEntityFramework(), 在 Configure 里面添加app.UseMiniProfiler(); 并配置 Swagger 的 IndexStream.

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    
        //Swagger
        services.AddSwaggerGen(options =>
        {
            options.DescribeAllEnumsAsStrings();
            options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info
            {
                Title = "API Docs",
                Version = "v1",
            });
        });
    
        //Profiling
        services.AddMiniProfiler(options =>
            options.RouteBasePath = "/profiler"
        ).AddEntityFramework();
    }
    
    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
    
            // profiling, url to see last profile check: http://localhost:56775/profiler/results
            app.UseMiniProfiler();
        }
    
        app.UseSwagger();
    
        app.UseSwagger().UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "API V1");
            // index.html customizable downloadable here: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.SwaggerUI/index.html
            // this custom html has miniprofiler integration
            c.IndexStream = () => GetType().GetTypeInfo().Assembly.GetManifestResourceStream("ORMDemo.EFWithRepository.SwaggerIndex.html");
        });
    
        app.UseMvc();
    }
  4. 运行项目,MiniProfiler 监控页面应该已经出现在 Swagger UI 页面的左上角了。

仓储模式和工作单元模式

仓储模式(Repository)是用来解耦的(通过在数据访问层和业务逻辑层之间创建抽象层)。
但仓储只关注于单一聚合的持久化,而业务用例却常常会涉及多个聚合的更改,为了确保业务用例的一致型,我们需要引入工作单元来管理多个聚合。

工作单元模式(unit of work)的作用就是在业务用例的操作中跟踪对象的所有更改(增加、删除和更新),并将所有更改的对象保存在其维护的列表中。在业务用例的终点,通过事务,一次性提交所有更改,以确保数据的完整性和有效性。总而言之,UOW协调这些对象的持久化及并发问题。

在 EF Core 中 DBContext 已经实现了工作单元模式,同时也比较容易更换统一的数据存储介质(通过支持的数据库驱动)。那么还有没有必要在 EF Core 上面再封装一层实现自己的仓储和工作单元呢?

下面实现一个简单的仓储和工作单元模式:

使用 EF Core(DB First)

EF Core 的 DB First 是通过 Scaffold-DbContext 命令根据已经存在的数据库创建实体类和context类。

可以通过PM> get-help scaffold-dbcontext –detailed查看命令的详细参数

Scaffold-DbContext [-Connection] <String> [-Provider] <String> [-OutputDir <String>] [-ContextDir <String>] [-Context <String>] [-Schemas <String[]>] [-Tables <String[]>] [-DataAnnotations] [-UseDatabaseNames] [-Force] 
[-Project <String>] [-StartupProject <String>] [<CommonParameters>]

使用之前创建的 blogging 数据库简单的测试一下:

  1. 新建一个项目,然后通过 Nuget 安装 EF Core 引用

    Install-Package Microsoft.EntityFrameworkCore.SqlServer
    Install-Package Microsoft.EntityFrameworkCore.Tools
  2. 执行命令创建实体

    Scaffold-DbContext "Server=CD02SZV3600503\SQLEXPRESS;Database=BloggingWithRepository;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

执行成功后可以看到在 Models 文件夹下面创建的实体类和 Context 类。

源代码

Github

参考

标签:Core,ASP,get,int,Blogs,Entity,set,BlogId,public
来源: https://blog.51cto.com/u_7605937/2704217