博客 / 詳情

返回

【EF Core】“DB First”方案下用編程方式生成數據庫模型代碼

大夥伴們只要學過三天 EF Core 一定知道,.NET SDK 有一個 dotnet-ef 工具(需要安裝),可以用來創建/遷移數據庫、生成模型代碼、優化模型和查詢代碼等。必要時還能生一個單獨的 exe,可以運行它來更新數據庫結構。

不過,按照官方的設計思路,肯定不會把所有功能都堆在 exe 項目中的,這不,dotnet-ef 只是做個封裝,可以通過命令行執行罷了,其實核心功能是寫在 Design 包裏面(Nuget 包名:Microsoft.EntityFrameworkCore.Design)。於是,咱們可以開發自己的 EF 輔助工具。比如,你可以把命令行操作的功能搞成窗口圖形化操作。當然,這些功能僅限開發者使用,用户一般不需要(不一般的用户除外)。

如此,在 DB First 方案(先有數據庫)中,咱們可以把生成實體類以及 dbContext 類的功能直接寫到項目代碼中,然後加上一個條件編譯,在需要生成代碼時開啓一下編譯符號,運行一個項目就能生成實體模型了。其他情況下把條件編譯符號註釋掉就可以。

這個功能就有點像 Sugar 的玩法。老周在某些項目中就是這麼幹的。不過老周更喜歡 EF,理由有:

1、EF Core 更靈活。

2、EF Core 的表達樹翻譯功能比 Sugar 完善,功能更多。

3、有官方支持的優先用原則,沒有才考慮第三方。

好了,不扯廢話了,咱們開始!

一、基礎知識

首先,咱們要明確功能:數據庫已經有了,可能是你創建的,可能是別人創建的。很多團隊都會把搞數據庫,寫存儲過程的單獨一堆人去幹,然後,項目的非數據庫部分另一堆人去做。所以,小型數據庫才考慮用 EF Core 去創建,複雜的數據庫還是先創建數據庫好一些。咱們要做的就是根據現有的數據庫和表,直接生成實體類和 DbContext 的派生類

在分析思路之前,既然大夥兒都是玩 .NET 的,那就堅守這個原則:處處都是服務容器和依賴注入

好,有這個思想準備,咱們才能講知識點。咱們來認識幾個新朋友,熟悉一下,以後才能好好利用他們,嗯,朋友是拿來利用的。

第一位,本名 IDatabaseModelFactory。他的絕活本領是爆庫。你要從數據庫生成實體,那你得知道數據庫裏有哪些表,表中有哪些列,哪個是主鍵,列的類型是什麼……沒事,這些信息交給這位朋友就行。

調用以下方法,你能得到一個 DatabaseModel 對象。

DatabaseModel Create(string connectionString, DatabaseModelFactoryOptions options);

第一個參數就是連接字符串,這個不用介紹了吧。要爆庫你得知道庫在哪裏吧。第二個參數是選項類,用來配置相關參數的。

public DatabaseModelFactoryOptions(IEnumerable<string>? tables = null, IEnumerable<string>? schemas = null)
{
    Tables = tables ?? [];
    Schemas = schemas ?? [];
}

上述是它的構造函數,Tables 是你要告訴朋友,你想要哪些表;Schemas 表示你要的架構,比如 dbo。這兩個參數都可以是 null,如果是 null 表示要全庫爆。

Create 方法返回的 DatabaseModel 對象包含數據庫名、數據庫中表、列、主鍵、外鍵、索引等相關信息。

好了,拿到數據庫信息了,輪到第二位朋友出場—— IScaffoldingModelFactory。他的絕活是加工,把你從數據庫中爆出來的信息處理後,直接返回一個 IModel。對,就是 EF Core 中用的數據庫模型,所以,如果你不打算生成代碼,這時候你完全可以把這個 IModel 作為 DbContext 的外部模型使用。還記得老周寫過使用外部模型的水文嗎?在 DbContext 選項配置時,用 UseModel 方法。

但,建議你不要這麼幹,你想想每次運行程序都要爆一次數據庫再轉換為 EF Core 模型,既浪費性能也沒啥實際意義。所以,這個生成的 IModel 還要進一步處理。

第三位朋友叫 IModelCodeGeneratorSelector。他是一名零件選配師,他會根據你的需要幫你找到合適的專屬文員(代碼生成器)。這位朋友有一個 Select 方法,調用後進行篩選。

[Obsolete("Use the overload that takes ModelCodeGenerationOptions instead.")]
IModelCodeGenerator Select(string? language);

// 注意,上面的方法過時,而下面的方法又反過去調用它
IModelCodeGenerator Select(ModelCodeGenerationOptions options)
#pragma warning disable CS0618 // Type or member is obsolete
        => Select(options.Language);
#pragma warning restore CS0618

Select 的舊版本已被標記為過時,但下面的方法又調用它。這是什麼騷操作?這是官方團隊的兼容操作。過時的方法只有一個字符串參數,表示生成的代碼語言(“VB”,“C#”)。而新方法的參數是一個 ModelCodeGenerationOptions 選項類,可以配置更多東西。

Select 方法幫你選好了心儀的文員妹妹,她叫 IModelCodeGenerator。她雖然學歷不高,但很勤奮很務實,你可以相信她。調用她的 GenerateModel 方法,她會幫你生成代碼。

ScaffoldedModel GenerateModel(
    IModel model,
    ModelCodeGenerationOptions options);

model 參數就是第二位朋友 IScaffoldingModelFactory 幫你生成的模型;options 參數是選項,和 IModelCodeGeneratorSelector.Select 方法用的是同一個。

到這裏基本工作就完成了,返回的 ScaffoldedModel 對象中已經包含代碼,以及代碼要存放的路徑了。不過,這些目前還在內存中,未真正寫入磁盤文件。程序退出後就沒了。

public class ScaffoldedModel
{
    // dbContext 類的代碼,以及文件路徑
    public virtual ScaffoldedFile ContextFile { get; set; } = null!;

    // 附加文件,通常是實體類的代碼以及文件路徑,每個實體類佔一個文件
    public virtual IList<ScaffoldedFile> AdditionalFiles { get; } = new List<ScaffoldedFile>();
}

public class ScaffoldedFile(string path, string code)
{
    // 代碼要存入的文件路徑
    public virtual string Path { get; set; } = path;

    // 已生成的代碼
    public virtual string Code { get; set; } = code;
}

總結一下,流程如下:

A、獲取數據庫信息;

B、生成設計時模型;

C、生成代碼;

D、保存代碼。

你一定會抱怨了,這過程有點複雜。別急,還沒完呢,繼續往下看,簡單的來了。

上面提到的幾位朋友,你一個個地告訴他們幹什麼是有些麻煩的,所以,把他們組成一個團隊,設立一名管理者,有事只要跟他們的老大説行了。這位由不民主制度任命的老大叫 IReverseEngineerScaffolder(位於 Microsoft.EntityFrameworkCore.Scaffolding 命名空間),默認實現類是 ReverseEngineerScaffolder(位於 Microsoft.EntityFrameworkCore.Scaffolding.Internal 命名空間)。雖然這個類是 public 的,但官方團隊讓它藏在 Internal 命名空間下,這表明:在功能上是不希望外部代碼訪問的。在使用時,咱們的確不用訪問該類,而是通過 IReverseEngineerScaffolder 接口來調用。

前面介紹的幾位朋友,吃過幾回飯後你可以忘記,現在你只要記住 IReverseEngineerScaffolder 即可。有事找他。

IReverseEngineerScaffolder 接口定義了兩個方法:

ScaffoldedModel ScaffoldModel(
    string connectionString,
    DatabaseModelFactoryOptions databaseOptions,
    ModelReverseEngineerOptions modelOptions,
    ModelCodeGenerationOptions codeOptions);

SavedModelFiles Save(
    ScaffoldedModel scaffoldedModel,
    string outputDir,
    bool overwriteFiles);

ScaffoldModel 方法根據數據庫生成代碼,Save 方法把代碼寫入文件。這樣一來,是不是變得簡單了?只要一個服務接口,調用兩個方法成員就完事了。

 

二、如何使用

既然處處是注入,那就得先初始化服務集合。Design 庫提供了兩個擴展方法:

public static IServiceCollection AddDbContextDesignTimeServices(
    this IServiceCollection services,
    DbContext context);

public static IServiceCollection AddEntityFrameworkDesignTimeServices(
    this IServiceCollection services,
    IOperationReporter? reporter = null,
    Func<IServiceProvider>? applicationServiceProviderAccessor = null);

第一個擴展方法在生成實體代碼時不需要,它的作用是把現有 DbContext 實例中的服務添加到服務容器中。咱們今天要實現的功能要用到第二個方法,它會添加設計時的一些基礎服務,包括我們上面提到的幾位新朋友。

當然,這是設計時的基礎服務,不包括 EF 核心服務。核心服務一般可以通過實現 IDesignTimeServices 接口來添加。

public interface IDesignTimeServices
{
    void ConfigureDesignTimeServices(IServiceCollection serviceCollection);
}

實現接口時,在 ConfigureDesignTimeServices 方法中,向服務容器添加需要的服務。

然後,在程序集級別使用 [DesignTimeProviderServices] 特性指定你實現 IDesignTimeServices 接口的類的完整名稱(連同命名空間)。其他工具(如 dotnet-ef,或你自己實現的工具)可以通過反射獲取它,動態實例化並調用 ConfigureDesignTimeServices 方法。當然,你不用反射,直接在代碼中 new 也可以的。

其實,你完全可以偷懶,不用去實現 IDesignTimeServices 接口,因為每種數據庫的提供者都會實現專用的類。比如 SQL Server 的提供者,會有一個 SqlServerDesignTimeServices 類,並且應用 [DesignTimeProviderServices] 特性。

[assembly: DesignTimeProviderServices("Microsoft.EntityFrameworkCore.SqlServer.Design.Internal.SqlServerDesignTimeServices")]

對於 SQLite 數據庫,會有一個 SqliteDesignTimeServices 類,同樣也會在程序集上應用 [DesignTimeProviderServices] 特性。

[assembly: DesignTimeProviderServices("Microsoft.EntityFrameworkCore.Sqlite.Design.Internal.SqliteDesignTimeServices")]

這些默認實現的設計時服務提供類默認會把 EF 的核心服務、關係數據庫相關、數據庫專用的服務全部添加到服務容器,你不需要額外去處理。

當所有需要的服務都添加到容器後,生成 ServiceProvider。然後你直接從服務容器中獲取 IReverseEngineerScaffolder 接口,配置好相關參數(如輸出目錄、DbContext 類的名稱等),先調用 ScaffoldModel 方法生成代碼,再調用 Save 方法寫入文件就行了。

 

三、實例演示

光説不練,慘過失戀。前文已介紹完所有基礎知識了,該練練手了。

老周以 SQL Server 來演示,先創建數據庫,以及兩張表。一張表是客户表,一張是照片表。因為這是一家照相館的信息管理系統。一位客户可以有多張照片,所以是“一對多”的關係(別問我怎麼沒有多對多,你一張照片給多個客户?這麼有分享精神的嗎?除非是大合照)。

USE [master]
GO

CREATE DATABASE [SomeDB]
GO

CREATE TABLE [dbo].[tb_customers] (
    [cust_id] INT            IDENTITY (1, 1) NOT NULL,
    [name]    NVARCHAR (12)  NOT NULL,
    [age]     INT            NULL,
    [address] NVARCHAR (100) NULL,
    [phone]   CHAR (11)      NOT NULL,
    [email]   NVARCHAR (64)  NULL,
    [remark]  NTEXT          NULL,
    CONSTRAINT [PK_customers] PRIMARY KEY CLUSTERED ([cust_id] ASC)
);

CREATE TABLE [dbo].[tb_photos] (
    [Id]      INT           IDENTITY (1, 1) NOT NULL,
    [tags]    NVARCHAR (30) NOT NULL,
    [dpi]     REAL          DEFAULT ((300.0)) NULL,
    [width]   FLOAT (53)    NOT NULL,
    [height]  FLOAT (53)    NOT NULL,
    [cust_id] INT           DEFAULT ((0)) NOT NULL,
    CONSTRAINT [PK_photos] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_photos_custs] FOREIGN KEY ([cust_id]) REFERENCES [dbo].[tb_customers] ([cust_id]) ON DELETE CASCADE ON UPDATE CASCADE
);

注意外鍵是在 tb_photos 表中定義的,這裏外鍵不唯一,要是搞唯一了就變成“一對一”關係了。

創建一個最簡單的.NET項目(控制枱),你需要向項目添加以下 nuget 包:

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
      <PrivateAssets>all</PrivateAssets>
      <!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
  </ItemGroup>

嚴重注意:Design 是開發工具包,默認是不讓你的代碼訪問的。當然,如果你考慮用反射的方法調用,那無所謂。這裏咱們沒必要去反射,應該直接訪問,所以,在 PackageReference 元素下,把 IncludeAssets 整個節點註釋掉,這樣就能直接訪問了。

其他的包都常規操作了,老周這裏用的是 SQL Server就用 Sqlserver 包,你用的如果是 SQLite 那就 Sqlite 包。這個就不多説了,都懂的。

咱們並不是每次運行程序都需要生成代碼的,除非是有改動(通常不會改,小改動的話也不用重新生成,直接手動改代碼就行),所以定義一個符號,當需要生成實體代碼時啓用,畢竟這是為開發者服務的功能,不是面向最終用户。

#define GEN_CODES
……

    static void Main(string[] args)
    {
#if GEN_CODES
        GenModelCodes();
#endif
    }

下面我們把注意力集中到 GenModelCodes 方法上。

#if GEN_CODES
    private static void GenModelCodes()
    {
          ……
    }
#endif

用常量定義一些基本參數。

// 連接字符串
const string connectStr = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SomeDB";
// 輸出目錄
const string outputDir = "..\\..\\..\\DBModels";
// 命名空間
const string myNamespace = "DB";
// 數據庫上下文名稱
const string contextName = "SomeDbContext";

outputDir 常量指定的是輸出目錄,可以用相對路徑(相對於當前程序),老周這裏用了三個 ..,即往上跳三層目錄。你猜猜是啥目錄?(項目根目錄)

對於命名空間,可以有兩個,一個是 DbContext 派生類所在命名空間,一個是實體類所在命名空間。當然,老周這裏只用了一個 DB,意思就是它們都位於 DB 命名空間內。

contextName 常量指示生成的 DbContext 子類的名字,這裏老周給了它一個風雅的名字 SomeDbContext。

接着,咱們需要配置三個選項類(上文介紹過了,雖然名字有點臭長,但不用死記,大概記得就行)。

DatabaseModelFactoryOptions dmfacOpts = new(
        // 選擇你要的表
        tables: ["tb_customers", "tb_photos"],
        // 選擇你要的架構
        schemas: ["dbo"]
    );
ModelCodeGenerationOptions modgenOpt = new()
{
    Language = "C#",    // 語言可以不設置,默認 C#
    ContextDir = "",
    ModelNamespace = myNamespace,
    ContextNamespace = myNamespace,
    ContextName = contextName,
    UseDataAnnotations = false,
    UseNullableReferenceTypes = true
};
ModelReverseEngineerOptions modreverOpt = new()
{
    UseDatabaseNames = true,
    NoPluralize = false
};

1、DatabaseModelFactoryOptions 選項:配置一下我們需要用的表和架構,其實這裏可以全 null,畢竟咱們全部生成。

2、ModelCodeGenerationOptions 選項:生成代碼相關。

  1. Language 屬性可以忽略的,默認就是 C#;
  2. ContextDir 屬性可以為 dbContext 類指定一個子目錄(相對於 outputDir),空字符串表示只放在 outputDir 下,不用單獨子目錄;
  3. ModelNamespace 屬性指定實體類代碼的命名空間;
  4. ContextNamespace 屬性指定 dbContext 類的命名空間。可以與實體類在同一命名空間;
  5. ContextName 屬性指定 dbcontext 類的類名;
  6. UseDataAnnotations 屬性表示用不用數據批註來配置模型,false 表示用 ModelBuilder 來配置,重寫 DbContext.OnModelCreating 方法;
  7. UseNullableReferenceTypes 屬性配置用不用可以為 null 類型,比如 string?、int?;
  8. ConnectionString 屬性是連接字符串,不用配置,因為 IReverseEngineerScaffolder.ScaffoldModel 方法的第一個參數就是連接字符串;
  9. ProjectDir 屬性是 .NET 項目所在目錄,這裏不用配置,因為連 DbContext 都沒有,這裏用不上。

3、ModelReverseEngineerOptions 選項:UseDatabaseNames 表示是否用數據庫原有的名字,即實體屬性等命名與數據庫中一樣;如果不用,那麼會生成 C# 命名風格的名稱,如 Name、TbCustomer 等。NoPluralize 表示禁用複數,這個主要是 dbcontext 類中 DbSet 類型屬性的命名,如 Students、Customers 等,false 表示不禁用。

下一步就是服務容器配置了。

ServiceCollection services = new();

添加設計時基礎服務。

services.AddEntityFrameworkDesignTimeServices();

然後就是獲取 IDesignTimeServices 服務了。有兩種方法:

第一種方法,我們是知道的,面向 SQL Server 的設計時服務類叫 SqlServerDesignTimeServices。對,直接 new 一下就好,最簡單。

IDesignTimeServices designtimeSvc = new SqlServerDesignTimeServices();
designtimeSvc.ConfigureDesignTimeServices(services);

記得調用 ConfigureDesignTimeServices 方法,否則白乾活。SqlServerDesignTimeServices 類雖然聲明上是 public,但功能意義上是內部類型,編譯器會發出 EF1001 警告。在使用類之前的任意位置禁用這個警告。

#pragma warning disable EF1001

第二種方法是運用反射,代碼雖然多一點,但不用禁用 EF1001 警告。

Assembly sqlserverProvdAssm = typeof(SqlServerServiceCollectionExtensions).Assembly;
DesignTimeProviderServicesAttribute? attr = sqlserverProvdAssm.GetCustomAttribute<DesignTimeProviderServicesAttribute>();
if (attr == null)
{
    Console.WriteLine("這個程序集不對勁!");
    return;
}
Type? designSvcType = sqlserverProvdAssm.GetType(attr.TypeName);
if (designSvcType == null)
{
    Console.WriteLine("鬧鬼了,居然找不到類型");
    return;
}
// 創建實例
IDesignTimeServices designtimeSvc = (IDesignTimeServices)Activator.CreateInstance(designSvcType)!;
// 調用方法配置服務
designtimeSvc.ConfigureDesignTimeServices(services);

由於我們項目已經引用了 Microsoft.EntityFrameworkCore.SqlServer 包,所以不需要再 Load 程序集了,它已經 Load 了。所以你在這個程序集中隨便選個公共類,獲取其 Type,就能得到 Assembly 了。我選的是 SqlServerServiceCollectionExtensions 類,是個定義擴展方法的類。

獲取到程序集後,拿到 DesignTimeProviderServices 特性。

DesignTimeProviderServicesAttribute? attr = sqlserverProvdAssm.GetCustomAttribute<DesignTimeProviderServicesAttribute>();

這個特性實例的 TypeName 屬性就是 SqlServerDesignTimeServices 類的全名。然後用 Activator.CreateInstance 方法動態創建其實例,賦值給 IDesignTimeServices 接口類型的變量,最後調用 ConfigureDesignTimeServices 方法就行了。

 

配置完服務容器後,生成一下服務 Provider。

IServiceProvider serviceProvider = services.BuildServiceProvider();

接下來,見證奇蹟的時候到了。從服務容器中獲取 IReverseEngineerScaffolder。

IReverseEngineerScaffolder scaffolder = serviceProvider.GetRequiredService<IReverseEngineerScaffolder>();

輕鬆地調用 ScaffoldModel 方法。

var scaffModel = scaffolder.ScaffoldModel(
        connectionString: connectStr,
        databaseOptions: dmfacOpts,
        modelOptions: modreverOpt,
        codeOptions: modgenOpt
    );

代碼已經生成,要保存到文件。

if (scaffModel != null)
{
    var res = scaffolder.Save(
             scaffoldedModel: scaffModel,
             outputDir: outputDir,
             overwriteFiles: true    // 覆蓋文件
         );
    if (res is not null)
    {
        Console.WriteLine("dbContext路徑:{0}", res.ContextFile);
        Console.WriteLine("實體路徑:");
        foreach (string f in res.AdditionalFiles)
        {
            Console.WriteLine("  {0}", f);
        }
    }
}

整個 GenModelCodes 方法的代碼如下:

#if GEN_CODES
    private static void GenModelCodes()
    {
        // 連接字符串
        const string connectStr = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SomeDB";
        // 輸出目錄
        const string outputDir = "..\\..\\..\\DBModels";
        // 命名空間
        const string myNamespace = "DB";
        // 數據庫上下文名稱
        const string contextName = "SomeDbContext";

        // 準備選項
        DatabaseModelFactoryOptions dmfacOpts = new(
                // 選擇你要的表
                tables: ["tb_customers", "tb_photos"],
                // 選擇你要的架構
                schemas: ["dbo"]
            );
        ModelCodeGenerationOptions modgenOpt = new()
        {
            Language = "C#",    // 語言可以不設置,默認 C#
            ContextDir = "",
            ModelNamespace = myNamespace,
            ContextNamespace = myNamespace,
            ContextName = contextName,
            UseDataAnnotations = false,
            UseNullableReferenceTypes = true
        };
        ModelReverseEngineerOptions modreverOpt = new()
        {
            UseDatabaseNames = true,
            NoPluralize = false
        };

        // 服務集合
        ServiceCollection services = new();
        // 1、設計時基礎服務
        services.AddEntityFrameworkDesignTimeServices();
        // 2、數據庫提供的設計時服務,它已包含框架基礎服務
        // 直接實例化
        IDesignTimeServices designtimeSvc = new SqlServerDesignTimeServices();
        designtimeSvc.ConfigureDesignTimeServices(services);
        // 或使用反射
        /*
        Assembly sqlserverProvdAssm = typeof(SqlServerServiceCollectionExtensions).Assembly;
        DesignTimeProviderServicesAttribute? attr = sqlserverProvdAssm.GetCustomAttribute<DesignTimeProviderServicesAttribute>();
        if (attr == null)
        {
            Console.WriteLine("這個程序集不對勁!");
            return;
        }
        Type? designSvcType = sqlserverProvdAssm.GetType(attr.TypeName);
        if (designSvcType == null)
        {
            Console.WriteLine("鬧鬼了,居然找不到類型");
            return;
        }
        // 創建實例
        IDesignTimeServices designtimeSvc = (IDesignTimeServices)Activator.CreateInstance(designSvcType)!;
        // 調用方法配置服務
        designtimeSvc.ConfigureDesignTimeServices(services);
        */

        // 構建服務
        IServiceProvider serviceProvider = services.BuildServiceProvider();

        // 生成代碼
        IReverseEngineerScaffolder scaffolder = serviceProvider.GetRequiredService<IReverseEngineerScaffolder>();
        var scaffModel = scaffolder.ScaffoldModel(
                connectionString: connectStr,
                databaseOptions: dmfacOpts,
                modelOptions: modreverOpt,
                codeOptions: modgenOpt
            );
        // 完事後還得保存
        if (scaffModel != null)
        {
            var res = scaffolder.Save(
                     scaffoldedModel: scaffModel,
                     outputDir: outputDir,
                     overwriteFiles: true    // 覆蓋文件
                 );
            if (res is not null)
            {
                Console.WriteLine("dbContext路徑:{0}", res.ContextFile);
                Console.WriteLine("實體路徑:");
                foreach (string f in res.AdditionalFiles)
                {
                    Console.WriteLine("  {0}", f);
                }
            }
        }
    }
#endif

現在,你可以運行一下試試(連接字符串記得改一下,別照抄)。然後你會得到以下寶藏:

image

看看生成的代碼。

using System;
using System.Collections.Generic;

namespace DB;

public partial class tb_customer
{
    public int cust_id { get; set; }

    public string name { get; set; } = null!;

    public int? age { get; set; }

    public string? address { get; set; }

    public string phone { get; set; } = null!;

    public string? email { get; set; }

    public string? remark { get; set; }

    public virtual ICollection<tb_photo> tb_photos { get; set; } = new List<tb_photo>();
}

/************************************************************/

using System;
using System.Collections.Generic;

namespace DB;

public partial class tb_photo
{
    public int Id { get; set; }

    public string tags { get; set; } = null!;

    public float? dpi { get; set; }

    public double width { get; set; }

    public double height { get; set; }

    public int cust_id { get; set; }

    public virtual tb_customer cust { get; set; } = null!;
}
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace DB;

public partial class SomeDbContext : DbContext
{
    public SomeDbContext()
    {
    }

    public SomeDbContext(DbContextOptions<SomeDbContext> options)
        : base(options)
    {
    }

    public virtual DbSet<tb_customer> tb_customers { get; set; }

    public virtual DbSet<tb_photo> tb_photos { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
        => optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SomeDB");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<tb_customer>(entity =>
        {
            entity.HasKey(e => e.cust_id).HasName("PK_customers");

            entity.Property(e => e.address).HasMaxLength(100);
            entity.Property(e => e.email).HasMaxLength(64);
            entity.Property(e => e.name).HasMaxLength(12);
            entity.Property(e => e.phone)
                .HasMaxLength(11)
                .IsUnicode(false)
                .IsFixedLength();
            entity.Property(e => e.remark).HasColumnType("ntext");
        });

        modelBuilder.Entity<tb_photo>(entity =>
        {
            entity.HasKey(e => e.Id).HasName("PK_photos");

            entity.Property(e => e.dpi).HasDefaultValue(300f);
            entity.Property(e => e.tags).HasMaxLength(30);

            entity.HasOne(d => d.cust).WithMany(p => p.tb_photos)
                .HasForeignKey(d => d.cust_id)
                .HasConstraintName("FK_photos_custs");
        });

        OnModelCreatingPartial(modelBuilder);
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

看看 SomeDbContext 類的 tb_customers 和 tb_photos 屬性,ModelReverseEngineerOptions 選項類中的 NoPluralize 屬性配置的就是這裏(屬性命名使用複數,當然,如果你生成的是中文名,那無所謂)。

重寫 OnConfiguring 方法,配置數據庫連接的地方有個警告。

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
        => optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SomeDB");

意思是提醒你不要把連接字符串硬編碼,這個後面咱們可以自己改代碼,使用配置文件中的連接字符串。

 

好了,今天的話題聊到這兒。下一篇咱們聊 Code First 方案下,用編程方式去生成遷移代碼,並遷移數據庫。

 

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.