Initializer trong Entity Framework: Khởi tạo và cập nhật CSDL

    0

    Bài học này sẽ giúp bạn giải quyết các vấn đề liên quan đến việc khởi tạo cơ sở dữ liệu với Entity Framework code-first. Bạn sẽ học về quy tắc và bộ khởi tạo CSDL tự động (Database Initialization) của Entity Framework. Bạn cũng sẽ làm quen với việc cập nhật cấu trúc CSDL từ code-first khi thay đổi entity class.

    Quay trở lại Tự học lập trình ADO.NET và Entity Framework

    Vấn đề khởi tạo cơ sở dữ liệu trong code-first

    Như bạn đã biết trong bài học về lập trình Entity Framework code-first cơ bản, bạn yêu cầu Entity Framework tạo ra cơ sở dữ liệu bằng cách gọi phương thức CreateIfNotExists. Phương thức này sẽ kiểm tra xem đã có CSDL tương ứng chưa. Nếu chưa có thì tạo CSDL mới.

    Bạn thậm chí không cần tự gọi CreateIfNotExists. Trong lần truy xuất dữ liệu đầu tiên, EF có thể tự động kiểm tra CSDL và tạo mới nếu cần thiết. Cơ chế này có tên gọi là Database Initialization (tự động khởi tạo CSDL).

    Hãy cùng thực hiện ví dụ. Tạo một blank solution và thêm vào đó project P00_AutoInitialization. Cài đặt thư viện Entity Framework cho project từ Nuget.

    Lưu ý: bạn chỉ cần cài đặt thư viện EF, không cần thực hiện bất kỳ thao tác cấu hình nào khác, cũng như không cần tạo connection string trong App.Config.

    Viết code sau vào file Program.cs:

    using System.Data.Entity;
    using System.Linq;
    using static System.Console;
    
    namespace DataLayer
    {
        public class Person
        {
            public int Id { get; private set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
    
        public class Context : DbContext
        {
            public Context() : base()
            {
    
            }
    
            public DbSet<Person> People { get; set; }
        }
        class Program
        {
            static void Main(string[] args)
            {
                using (var context = new Context())
                {
                    WriteLine($"Database exists? {context.Database.Exists()}");
    
                    context.People.Add(new Person { FirstName = "Donald", LastName = "Trump" });
                    context.People.Add(new Person { FirstName = "Barack", LastName = "Obama" });
                    context.SaveChanges();
    
                    var people = context.People.ToArray();
    
                    WriteLine("People list:");
                    foreach (var p in people)
                    {
                        WriteLine($"{p.FirstName} {p.LastName}");
                    }
                }
                ReadKey();
            }
        }
    }

    Khi chạy chương trình lần đầu tiên, trên console sẽ thông báo chưa có CSDL. Sau một lát, sẽ hiện ra danh sách. Lần sau bạn chạy chương trình sẽ nhận thông báo CSDL đã có sẵn và một danh sách mới.

    Nếu bạn cài đặt LocalDb, khi mở thư mục <Ổ hệ thống>:\Users\<tên user hiện hành> sẽ thấy hai file DataLayer.Context.mdfDataLayer.Context_log.ldf. Đây là các file của cơ sở dữ liệu do Entity Framework tạo ra khi sử dụng LocalDb – được lựa chọn mặc định khi tạo CSDL. Nếu không có LocalDb, CSDL có tên tương tự sẽ được tạo ra trong instance mặc định của Sql Server trên máy local.

    Ví dụ trên minh họa cho khả năng tự động tạo CSDL của Entity framework code-first. Rõ ràng bạn không gọi bất kỳ một lệnh nào để tạo CSDL. Bạn cũng không cung cấp bất kỳ thông gì cho Entity Framework về server hay tên CSDL.

    Có lẽ bạn sẽ đặt ra một vài câu hỏi, đại loại như làm thế nào Entity Framework xác định được tên của CSDL cần tạo? Làm thế nào để kiểm soát quá trình tạo CSDL của EF?

    Bạn cũng để ý thấy, cách thêm dữ liệu ban đầu như trên không thực sự phù hợp. Làm thế nào để tự động thêm dữ liệu ban đầu chỉ khi khởi tạo CSDL?

    Ngoài ra, bạn sẽ nhanh chóng nhận ra một vấn đề nữa. Nếu bạn thay đổi entity class (và việc này chắc chắn luôn xảy ra) và chạy lại chương trình, Entity Framework sẽ đưa ra thông báo “The model backing the <DbContextName> context has changed since the database was created“. Lỗi này xảy ra là do cấu trúc entity class và bảng dữ liệu giờ không còn tương thích nhau nữa. Làm thế nào để kiểm soát vấn đề này?

    Tham số phục vụ khởi tạo cơ sở dữ liệu trong Entity Framework

    Trước hết chúng ta cùng xem cách Entity Framework lựa chọn tên và server CSDL khi khởi tạo với tiếp cận code-first.

    Như bạn đã thực hiện trong bài trước, mỗi CSDL tương ứng với một lớp context kế thừa từ DbContext. Trong lớp context bạn phải xây dựng một constructor không tham số và gọi constructor của lớp cha. Constructor của lớp cha DbContext có hai overload: overload thứ nhất không có tham số; overload thứ hai nhận một tham số string.

    Tùy thuộc vào việc sử dụng overload nào và cách viết tham số, Entity Framework sẽ đưa ra quyết định về tên của CSDL tương ứng. Ngoài ra, Entity Framework có thể quyết định tạo CSDL mới hoặc sử dụng CSDL đã có. Sơ đồ dưới đây thể hiện cách Entity Framework đưa ra quyết định về CSDL:

    Entity Framework code-first database initialization

    Như trong sơ đồ bạn có thể thấy, có 3 tình huống diễn ra khi gọi constructor của lớp cha DbContext:

    1. Không có tham số
    2. Tên CSDL
    3. Tên Connection String

    Chúng ta sẽ xem xét từng tình huống.

    Sử dụng constructor không tham số

    Hãy cùng thực hiện ví dụ. Thêm project P01_Initialization vào solution và cài đặt Entity Framework cho project này. Bạn cũng không cần thực hiện bất kỳ cấu hình nào khác.

    Mục đích của các ví dụ trong phần này là thử nghiệm các cách thức lựa chọn server và tên cơ sở dữ liệu của Entity Framework code-first. Do đó chúng ta tạo ra một project mới đơn giản hơn để chỉ tập trung vào vấn đề khởi tạo CSDL mà không cần quan tâm đến các bảng trong đó.

    Nếu không muốn tạo project mới, bạn cũng có thể sử dụng luôn P00_AutoInitialization ở trên.

    Viết code cho Program.cs như sau:

    using System;
    using System.Data.Entity;
    
    namespace DataLayer
    {
        public class Context : DbContext
        {
            public Context() : base()
            {
            }        
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                bool success = false;
                using (var context = new Context())
                {
                    success = context.Database.CreateIfNotExists();
                }
    
                Console.WriteLine($"Database creation: {success}");
                Console.ReadKey();
            }
        }
    }

    Trong ví dụ vừa rồi, bạn chỉ tạo lớp Context nằm trong namespace DataLayer và sử dụng nó. Bạn không cần xây dựng entity class.

    Nếu chạy chương trình lần đầu tiên bạn sẽ nhận được thông báo tạo thành công cơ sở dữ liệu!!!!! Tại sao lại vậy? Chuyện gì đã xảy ra? Cơ sở dữ liệu nằm ở đâu và tên là gì?

    Entity Framework có khả năng tự xác định server CSDL, tự tạo CSDL trong đó. Nếu không có bất kỳ thông tin cấu hình nào, EF sẽ chủ động tìm kiếm Sql Server cài đặt trên máy local. Nếu bạn cài đặt LocalDb, EF sẽ ưu tiên sử dụng LocalDb.

    Khi đã tìm kiếm được server, EF sẽ tự động tạo CSDL và đặt tên theo quy tắc {Tên_namespace}.{Tên_context_class}. Như trong ví dụ trên CSDL sẽ được EF đặt tên là DataLayer.Context.

    Nếu bạn gọi base constructor không tham số trong lớp context, EF sẽ tạo ra CSDL với tên gọi theo cấu trúc {Namespace}.{Context class name}. EF có thể tự động tìm kiếm và kết nối server trên máy cục bộ.

    Như bạn đã biết, mỗi entity class tương đương với 1 (vài) bảng, bản thân context class tương đương với cả CSDL. Do vậy, kể cả khi bạn không xây dựng bất kỳ entity class nào, EF cũng vấn tạo ra CSDL từ lớp context bạn xây dựng.

    Đây là cách thức đơn giản nhất để làm việc với Entity Framework trong môi trường phát triển.

    Cơ sở dữ liệu của Sql Server (và LocalDb) bao gồm hai file, mdf ldf. Đối với LocalDb, hai file này sẽ được tạo ra ở thư mục <Ổ hệ thống>:\Users\<tên user>.

    Cung cấp tên CSDL

    Overload thứ hai của base constructor nhận một tham số kiểu string. Tùy thuộc vào cách viết chuỗi, EF sẽ nhìn nhận nó theo hai cách khác nhau. Nếu viết chuỗi ở dạng "name=...", EF sẽ hiểu rằng bạn đang cung cấp tên của connection string sau dấu bằng. Connection string này lưu trong file cấu hình (App.config, Web.config). Nếu truyền một chuỗi ký tự thông thường, EF sẽ hiểu bạn đang cung cấp tên CSDL.

    Trong tình huống truyền tên CSDL, EF cũng tự xác định và kết nối tới local server như tình huống bên trên. Khác biệt ở chỗ EF sẽ sử dụng tên cho bạn cung cấp thay cho tên theo cấu trúc mặc định.

    Hãy thay đổi ví dụ trên một chút:

    public class Context : DbContext
    {
        public Context() : base("PersonnelDatabase")
        {
        }        
    }

    Giờ chạy chương trình bạn sẽ thu được một CSDL có tên gọi PersonnelDatabase và ở cùng server với CSDL ở trên:

    Hai phương pháp này rất đơn giản, tiện lợi và phù hợp để bạn thử nghiệm EF và CSDL. Tuy nhiên, nó có hạn chế vì bạn không kiểm soát được kết nối tới server.

    Khi phát triển ứng dụng thực sự, bạn nên sử dụng phương pháp cuối cùng dưới đây (cũng là phương pháp được giới thiệu từ bài học trước).

    Cung cấp tên ConnectionString

    Đây là phương pháp mà bạn kiểm soát hoàn toàn việc sử dụng server nào và đặt tên CSDL là gì thông qua connection string lưu trong file cấu hình của ứng dụng (app.config hoặc web.config).

    Ở phương pháp này, bạn vẫn cùng overload thứ hai của base constructor nhưng chuỗi tham số cần viết theo quy tắc “name=<tên_connection_string>”. Trong đó, connection string lưu trong file cấu hình.

    Hãy thay đổi ví dụ trên một chút. Trước hết tạo ra node connectionStrings trong app.config như sau:

    <connectionStrings>
      <add name="personnel_connection_string" 
            providerName="System.Data.SqlClient" 
            connectionString="Data source=.; Initial Catalog=Personnel; Integrated security=SSPI"/>
    </connectionStrings>

    Chú ý là node <connectionStrings> phải đặt sau node <configSections>.

    Bạn đã có bài học chi tiết về connectionstring trước đây khi học về ado.net. Hãy đọc lại bài học nếu đã quên.

    Điều chỉnh lại constructor của Context như sau:

    public class Context : DbContext
    {
        public Context() : base("name=personnel_connection_string")
        {
        }
    }

    Lưu ý cách viết tham số "name=personnel_connection_string" và tên connectionstring personnel_connection_string.

    Trong tình huống này, EF sẽ kiểm tra xem CSDL (tham số Initial Catalog) đã tồn tại trên server (tham số Data source) hay chưa. Nếu chưa nó sẽ tự tạo CSDL có tên như chỉ định của tham số Initial Catalog. Nếu CSDL đã có sẵn, EF sẽ sử dụng luôn mà không tạo CSDL mới.

    Như bạn đã thấy, CSDL mới giờ không được tạo ra trên LocalDb như trước nữa mà nằm trên local SqlExpress với tên gọi như chỉ định (Personnel). Bạn hoàn toàn kiểm soát được quá trình này thông qua connection string.

    Bộ khởi tạo CSDL tự động (Database initializer) trong code-first

    Trong bài học trước và ở project P01_Initialization, bạn sử dụng phương thức CreateIfNotExists() để yêu cầu chủ động tạo CSDL nếu CSDL chưa có sẵn.

    Như bạn thấy trong project P00_AutoInitialization, Entity Framework thậm chí có thể tự động tạo CSDL mà không cần gọi tới CreateIfNotExists. Khi chương trình code-first chạy lần đầu tiên và thực hiện bất kỳ truy vấn nào, cơ sở dữ liệu sẽ được tạo ra tự động.

    Cơ chế này trong Entity Framework được gọi là khởi tạo cơ sở dữ liệu tự động (database initialization). Class sử dụng cho quá trình này được gọi là bộ khởi tạo (initializer).

    Các bộ khởi tạo CSDL

    Để xử lý trong các tình huống liên quan đến tự động khởi tạo CSDL, EF đưa ra một số bộ khởi tạo (database initializer).

    Mỗi bộ khởi tạo thực chất là một class đã xây dựng sẵn với tên gọi tương ứng và bạn có thể sử dụng. Khi sử dụng bộ khởi tạo, trong client code bạn không cần gọi phương thức CreateIfNotExists() hay Create() nữa.

    Cơ chế tự khởi tạo CSDL này rất tiện lợi và hữu ích. Khi bạn thực sự truy xuất dữ liệu, bộ khởi tạo sẽ hoạt động và quyết định làm gì với CSDL.

    Có 3 bộ khởi tạo đã được xây dựng sẵn:

    • CreateDatabaseIfNotExists: đây là bộ khởi tạo mặc định. Bộ khởi tạo này sẽ tạo ra CSDL theo từng cấu hình nếu CSDL đó chưa tồn tại. Tuy nhiên, nếu bạn thay đổi model class và chạy chương trình sử dụng bộ khởi tạo này, EF sẽ bị lỗi (ở runtime):
    • DropCreateDatabaseIfModelChanges: bộ khởi tạo này sẽ xóa CSDL và tạo CSDL mới nếu bạn thay đổi model class. Khi sử dụng cách thức này bạn không cần phải tự mình duy trì cấu trúc CSDL nữa. Tuy nhiên, nếu dùng cách này, bạn sẽ mất dữ liệu.
    • DropCreateDatabaseAlways: mỗi lần chạy ứng dụng, CSDL cũ sẽ bị xóa và CSDL mới được tạo ra bất kể bạn có thay đổi model class hay không. Cách thức này nghe hơi vô lý nhưng nó có thể áp dụng nếu trong quá trình phát triển ứng dụng bạn cần một CSDL hoàn toàn “sạch” cho mỗi lần chạy chương trình.

    Ngoài 3 bộ khởi tạo trên, EF cho phép bạn tự xây dựng bộ khởi tạo riêng. Bạn sẽ học cách làm bộ khởi tạo riêng ở phần cuối bài học.

    Sử dụng database initializer

    Bạn có hai cách sử dụng initializer khác nhau: Viết code trong constructor của context class hoặc thiết lập trong file cấu hình.

    Viết code trong constructor của context class

    Tiếp tục với project P00_AutoInitialization đã tạo ở phần trên, bạn thay đổi constructor của Context như sau:

        public Context() : base()
        {
            var initializer = new DropCreateDatabaseIfModelChanges<Context>();
            Database.SetInitializer(initializer);
        }
    

    Bạn quyết định sử dụng chiến lược DropCreateDatabaseIfModelChanges bằng cách tạo ra một object của class tương ứng và truyền cho phương thức tĩnh SetInitializer của lớp System.Data.Entity.Database.

    var initializer = new DropCreateDatabaseIfModelChanges();
    Database.SetInitializer(initializer);

    Bạn làm tương tự nếu muốn sử dụng DropCreateDatabaseAlways. Nếu sử dụng CreateDatabaseIfNotExists thì không cần làm gì vì chiến lược này được sử dụng mặc định.

    Sử dụng file cấu hình (app.config, web.config)

    Nếu không muốn hard-code, bạn có thể chỉ định initializer trong file cấu hình như sau:

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
    <!-- lưu ý viết sau phần configSections -->
      <appSettings>
        <add key="DatabaseInitializerForType DataLayer.Context, P00_AutoInitialization"
            value="System.Data.Entity.DropCreateDatabaseAlways`1[[DataLayer.Context, P00_AutoInitialization]], EntityFramework" />
      </appSettings>
    </configuration>

    Khi sử dụng file cấu hình lưu ý cách viết key và value theo quy tắc sau:

    key="DatabaseInitializerForType {namespace}.{context_class}, {assembly}". 
    value="System.Data.Entity.DropCreateDatabaseAlways`1[[ {namespace}.{context_class}, {assembly} ]], EntityFramework".

    Như trong ví dụ trên, namespace là DataLayer, context class là Context, assembly là P00_AutoInitialization.

    Nếu có initializer cả trong code và file cấu hình, thì thông tin trong file cấu hình sẽ được sử dụng. Cách sử dụng file cấu hình linh hoạt hơn vì bạn có thể thay đổi initializer ngay cả khi ứng dụng đã build.

    Chủ động kích hoạt bộ khởi tạo

    Như trên bạn đã thấy, khi sử dụng CSDL bộ khởi tạo sẽ được gọi tự động. Mặc dù nó rất tiện lợi nhưng trong nhiều trường hợp bạn muốn chủ động kích hoạt quá trình này.

    Entity Framework cho phép chủ động kích hoạt quá trình khởi tạo CSDL bằng cách gọi phương thức Initialize() như sau:

    context.Database.Initialize(false);

    Nghĩa là bạn cần tạo ra object context và gọi phương thức Initialize của property Database. Phương thức này sẽ kích hoạt quá trình khởi tạo CSDL theo bộ khởi tạo mà bạn đã quyết định sử dụng ở lớp Context.

    Phương thức này nhận một tham số kiểu bool. Nếu true – phương thức sẽ luôn được chạy bất kể ngay trước đó nó đã được gọi. Nếu là false, phương thức chỉ chạy nếu chưa từng có lời gọi nào diễn ra trước đó.

    Như vậy cho đến giờ bạn đã biết Entity Framework có 3 cách khác nhau để giúp bạn tạo ra cơ sở dữ liệu từ domain class:
    (1) Tự động khi bạn bắt đầu sử dụng đến CSDL;
    (2) Chủ động gọi bằng phương thức Create() hoặc CreateIfNotExists();
    (3) Chủ động kích hoạt bộ khởi tạo bằng phương thức Initialize().
    Trong bài sau bạn sẽ biết đến cách thứ 4 sử dụng Migration.

    Tắt chế độ khởi tạo tự động

    Chế độ khởi tạo tự động của Entity Framework rất hữu ích và tiện lợi nhưng không phải lúc nào bạn cũng muốn sử dụng nó. Nếu muốn kiểm soát hoàn toàn quá trình khởi tạo CSDL, bạn có thể tắt chế độ tự động đi rồi viết code riêng.

    Cách tắt chế độ khởi tạo tự động rất đơn giản và bạn có thể thực hiện bằng code hoặc file cấu hình.

    Như đối với project trên, nếu dùng code:

    Database.SetInitializer<Context>(null);

    Nếu dùng file cấu hình:

    <add key="DatabaseInitializerForType DataLayer.Context, P00_AutoInitialization" value="Disabled" />

    Tự động thêm dữ liệu khi khởi tạo

    Như trên đã nói, EF cho phép xây dựng bộ khởi tạo riêng bằng cách kế thừa từ các bộ khởi tạo có sẵn. Một ưu điểm rất lớn của bộ khởi tạo riêng là bạn có thể thực hiện được những công việc mình cần ngay từ lúc tạo CSDL. Một trong những việc như vậy là tự động thêm một số dữ liệu ban đầu cho CSDL.

    Việc thêm dữ liệu ban đầu khi tạo CSDL rất hữu ích, cả trong lúc phát triển lẫn lúc triển khai. Ví dụ, bạn có thể đưa các dữ liệu dạng danh mục vào ngay từ đầu (ví dụ danh sách tỉnh – thành phố) thông qua code mà không cần nhập bằng tay về sau. Ở giai đoạn phát triển bạn có thể thêm ngay dữ liệu thử nghiệm từ code cho nhanh. Entity Framework gọi quá trình này là seeding.

    Chúng ta sẽ xem xét cách thực hiện qua ví dụ.

    Tạo thêm project mới P02_Seeding (+ cài đặt Entity Framework) và viết code cho Program.cs như sau:

    using System.Data.Entity;
    using System.Linq;
    using static System.Console;
    
    namespace P02_Seeding
    {
        public class Person
        {
            public int Id { get; private set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
    
        public class Context : DbContext
        {
            public Context() : base("Personnel")
            {
                var initializer = new MyInitializer();
                Database.SetInitializer(initializer);
            }
    
            public DbSet<Person> People { get; set; }
        }
    
        public class MyInitializer : CreateDatabaseIfNotExists<Context>
        {
            protected override void Seed(Context context)
            {
                context.People.AddRange(new[]
                {
                    new Person{ FirstName = "Donald", LastName = "Trump"},
                    new Person{ FirstName = "Barack", LastName = "Obama"},
                    new Person{ FirstName = "George", LastName = "Bush"},
                    new Person{ FirstName = "Bill", LastName = "Clinton"},
                });
                context.SaveChanges();
            }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                Title = "Data Seeding";
    
                using (var context = new Context())
                {
                    var people = context.People.ToArray();
                    foreach (var p in people)
                    {
                        WriteLine($"First person: {p.FirstName} {p.LastName}");
                    }
                }
    
                ReadKey();
            }
        }
    }

    Trong ví dụ trên, bạn tự xây dựng bộ initializer riêng MyInitializer kế thừa từ CreateDatabaseIfNotExists. Điều này có nghĩa là MyInitializer có đủ đặc điểm của CreateDatabaseIfNotExists như bạn đã biết ở trên.

    Tuy nhiên, trong MyInitializer bạn ghi đè phương thức Seed của lớp cha. Phương thức này luôn được gọi tự động một khi CSDL được tạo ra, và chỉ được gọi khi tạo CSDL. Những lần chạy sau, phương thức này không được gọi nữa. Do vậy, seed data chỉ được thêm vào một lần duy nhất lúc tạo CSDL.

    Bạn có thể sử dụng nó cho nhiệm vụ tạo dữ liệu ban đầu cũng như các thao tác khác với CSDL nếu cần. Trong ví dụ trên, chúng ta thêm một số dữ liệu sau khi tạo CSDL.

    Bạn sử dụng MyInitializer giống hệt như các bộ initializer có sẵn của Entity Framework như đã biết.

    Trong client code, bạn không cần gọi Create() hoặc CreateIfNotExists() như trước nữa mà có thể trực tiếp truy xuất CSDL luôn. MyInitializer sẽ tự động thực hiện khâu khởi tạo CSDL cho bạn theo đúng chiến lược của CreateDatabaseIfNotExists.

    Kết luận

    Trong bài học này bạn đã học về cách thức tự động khởi tạo cơ sở dữ liệu của EF code-first. Ba vấn đề được xem xét kỹ là: cách thức EF quyết định server và tên CSDL thông qua tham số của constructor của lớp context; sử dụng bộ khởi tạo CSDL tự động; xây dựng bộ khởi tạo riêng với data seeding.

    Toàn bộ nội dung của bài này đều hướng tới việc kiểm soát cách thức code-first tạo cơ sở dữ liệu.

    Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần Bình luận ở cuối trang. Nếu cần trao đổi riêng, hãy gửi email hoặc nhắn tin qua form liên hệ. Nếu bài viết hữu ích với bạn, hãy giúp chúng tôi chia sẻ tới mọi người. Cảm ơn bạn!

    Bình luận

    avatar
      Đăng ký theo dõi  
    Thông báo về