четверг, 8 августа 2013 г.

Understanding Database Initializers in Entity Framework Code First


Entity Framework Code First allows you to create data model classes prior to creating a database. When you run the application for the first time, a database is created for you on the fly based on the classes you created. Database initializers allow you to decide a strategy of database creation and seed data generation. Code First provides its own set of database initializer classes and also allows you to create your own. In this article you will learn to use the inbuilt database initializers in your application. You will also learn to create your own database initializers. Additionally, the article will illustrate how to populate a database with seed data during the initialization process.
Note:
This article will not discuss the basics of Code First. If you are not familiar with Code First readIntroduction to Entity Framework Code First. You will be using the same code sample in this article and we won't discuss the sample application in detail again.

What is Database Initialization?

A database initializer is a class that takes care of database creation and initialization in a Code First application. It is the job of database initializer to create the database and required tables based on the data model classes you create. To understand the role of database initializer, let's create a simple console application (see code download accompanying this article).
Have a look at the following model classes from the application:
  1. [Table("BlogPosts")]
  2. public class BlogPost
  3. {
  4. [Key]
  5. public Guid Id { get; set; }
  6. public string Title { get; set; }
  7. public string Content { get; set; }
  8. public DateTime PublishDate { get; set; }
  9.  
  10. [ForeignKey("Category")]
  11. public Guid CategoryId { get; set; }
  12. public virtual Category Category { get; set; }
  13. }
  14.  
  15. [Table("Categories")]
  16. public class Category
  17. {
  18. [Key]
  19. public Guid Id { get; set; }
  20. public string Name { get; set; }
  21. public virtual ICollection<BlogPost> BlogPosts { get; set; }
  22. }
The BlogPost class represents a blog post and corresponds to a BlogPosts table as indicated by the [Table] attribute. Similarly, Category class represents a blog post category and corresponds to the Categories table. Also have a look at the BlogContext class that represents a database context:
  1. public class BlogContext : DbContext
  2. {
  3. public BlogContext():base()
  4. {
  5. }
  6.  
  7. public DbSet<BlogPost> BlogPosts { get; set; }
  8. public DbSet<Category> Categories { get; set; }
  9. }
The Main() method then makes use of these classes to add a record in both of the tables.
  1. static void Main(string[] args)
  2. {
  3. using (var db = new BlogContext())
  4. {
  5. Guid postId = Guid.NewGuid();
  6. Guid catId = Guid.NewGuid();
  7. var cat = new Category { Id = catId, Name = "ASP.NET" };
  8. var post = new BlogPost { Id=postId,Title="Title1", Content="Hello World!", PublishDate=new DateTime(2011,1,1), Category=cat};
  9. db.Categories.Add(cat);
  10. db.BlogPosts.Add(post);
  11. Console.WriteLine(db.Database.Connection.ConnectionString);
  12. int i = db.SaveChanges();
  13. Console.WriteLine("{0} records added...", i);
  14. }
  15. Console.ReadLine();
  16. }
If you run the application you will find a database created as shown below:
The database was created automatically
Figure 1: The database was created automatically
Notice that the database CodeFirstDbInitializerDemo.BlogContext was created automatically and contains two application tables viz. BlogPosts and Categories. There is also a metadata table - EdmMetadata - that stores model metadata. This all happened because of an inbuilt database initializer - CreateDatabaseIfNotExists. As the name suggests the default database initializer will create a database only if it doesn't exist. Now, manually delete the BlogPosts table and run the application again.
This time you will get an exception indicating that the already existing database was used and since it no longer has BlogPosts table the code throws an exception while saving the changes.
DbUpdateException
Figure 2: DbUpdateException

Available Database Initializers

Entity Framework Code First comes with three basic database initializers viz. CreateDatabaseIfNotExists, DropCreateDatabaseWhenModelChanges and DropCreateDatabaseAlways.

CreateDatabaseIfNotExists

This is the default database initializer class used by Code First unless you specify some other class. As the name suggests, the CreateDatabaseIfNotExists class creates a database only if it doesn't exist. This initializer also comes in handy when you wish to avoid any accidental deletion of the database.

DropCreateDatabaseWhenModelChanges

This database initializer creates a database if it doesn't exist already. Additionally, if a database is already present but there is a mismatch between the model classes and table schema then it deletes the database and re-creates it. You will find this more useful during development and testing when models are changing often.

DropCreateDatabaseAlways

The DropCreateDatabaseAlways class deletes and creates a database irrespective of whether it is already present or not. This way, with every run of the application you will be deleting and re-creating the database. You will find this initializer useful during testing when you want to run the application with a fresh set of data.
You can also create your own database initializer by implementing the IDatabaseInitializer interface. You will develop a custom database initializer in the later sections.

Using Inbuilt Database Initializers

Now that you are aware of the inbuilt database initializers provided by Code First, let's see how to use them in your application.
To use a particular database initializer add the following piece of code at the beginning of the Main() method:
  1. Database.SetInitializer(new DropCreateDatabaseAlways<BlogContext>());
The SetInitializer() method takes an instance of database initializer class and sets it as a database initializer for the current application domain. When you set a database initializer, it won't be called immediately. It will be called when the context (BlogContext) is used for the first time. In the preceding example, the actual database creation will occur only when you add a new Category and BlogPost and not when a new instance of BlogContext is created.
  1. static void Main(string[] args)
  2. {
  3. Database.SetInitializer(new DropCreateDatabaseAlways<BlogContext>());
  4. using (var db = new BlogContext()) //initializer won't be called here
  5. {
  6. ...
  7. db.Categories.Add(cat); //initializer will be called here
  8. db.BlogPosts.Add(post);
  9. ...
  10. }
  11. Console.ReadLine();
  12. }
If you wish to use CreateDatabaseIfNotExists database initializer, you need not do anything specific since it is the default database initializer. Of course, you can set it explicitly using SetInitializer() method as shown above.
By default, Code First runs the database initialization logic once per AppDomain when the context is used for the first time. You can, however, override this default behavior using the Initialize() method. You may wish to call a database initializer explicitly when your model is complex and initialization is going to take more time to run. This way rather than spending time at some later stage you are running the initialization process at some known step (and can show a wait message to the end user). To use the Initialize() method you will need to modify your code as shown below:
  1. Database.SetInitializer(new DropCreateDatabaseAlways<BlogContext>());
  2. using (var db = new BlogContext())
  3. {
  4. db.Database.Initialize(false);
  5. ...
  6. }
In the above code snippet you are calling the Initialize() method immediately after creating a context instance. In this case, the database will be created immediately after calling the Initialize() method instead of waiting until the context is used for the first time. The Initialize() method takes a boolean parameter that controls whether the initialization process should re-run if it has already run for the application. Specifying false will skip the initialization process if it has already executed. A value of true will initialize the database again even if it was already initialized.
At times you may want to use an existing database with Code First. In such cases you may not want to execute any initialization logic at all. You can suppress the database initialization process altogether by passing null to SetInitializer() method.
  1. Database.SetInitializer<BlogContext>(null);

Seeding Data

During the testing phase you often need to populate database tables with sample data. At times you also need to populate some application data at the time of database creation. For example, while creating our sample database you may want to populate the Categories table with some predefined categories. Such seed data can be added to the database being created by overriding the Seed() method of the database initializer class. Consider the following piece of code :
  1. public class BlogContextSeedInitializer : DropCreateDatabaseAlways<BlogContext>
  2. {
  3. protected override void Seed(BlogContext context)
  4. {
  5. Category cat1 = new Category { Id = Guid.NewGuid(), Name = ".NET Framework" };
  6. Category cat2 = new Category { Id = Guid.NewGuid(), Name = "SQL Server" };
  7. Category cat3 = new Category { Id = Guid.NewGuid(), Name = "jQuery" };
  8. context.Categories.Add(cat1);
  9. context.Categories.Add(cat2);
  10. context.Categories.Add(cat3);
  11. context.SaveChanges();
  12. }
  13. }
Here, you created a custom database initializer by inheriting DropCreateDatabaseAlways class. Further, you need to override the Seed() method. The Seed() method receives the context object as a parameter. You then create three categories and add them to the context. Finally SaveChanges() method saves the data to the database that was created during the initialization process.
To see the Seed() method in action, you need to use BlogContextSeedInitializer in the Main() method. Adding the following line of code will do that job:
  1. Database.SetInitializer(new BlogContextSeedInitializer());
If you run the application again and check the Categories table, you should see sample data added to it.
Sample data added
Figure 3: Sample data added

Creating a Custom Database Initializer

In the preceding examples you used inbuilt database initializers. You can also create a custom database initializer by implementing the IDatabaseInitializer interface. You need to implement the InitializeDatabase() method of IDatabaseInitializer interface and write your own logic of database creation. The following code shows a sample implementation of the InitializeDatabase() method:
  1. public class BlogContextCustomInitializer : IDatabaseInitializer<BlogContext>
  2. {
  3. public void InitializeDatabase(BlogContext context)
  4. {
  5. if (context.Database.Exists())
  6. {
  7. if (!context.Database.CompatibleWithModel(true))
  8. {
  9. context.Database.Delete();
  10. }
  11. }
  12. context.Database.Create();
  13. context.Database.ExecuteSqlCommand("CREATE TABLE GLOBAL_DATA([KEY] VARCHAR(50), [VALUE] VARCHAR(250))");
  14. }
  15. }
The InitializeDatabase() method receives an instance of a content class. You can then use the Exists() method to determine whether a database is already present. The CompatibleWithModel() method tells you (true / false) whether the database schema is compatible with the model. If the database is not compatible you delete and recreate it using Delete() and Create() methods respectively. If no database exists then you create a new one using the Create() method. Notice how the code is using the ExecuteSqlCommand() method to create the GLOBAL_DATA table that is not part of the model. Though we don't use that table in our example it illustrates how custom initializers can be used to perform custom tasks.
Now, set the BlogContextCustomInitializer class as the initializer using the SetInitializer() method and run the application. You will find that in addition to model tables, the GLOBAL_DATA table is also created.
The GLOBAL_DATA table is created
Figure 4: The GLOBAL_DATA table is created

Specifying Database Initializer in Configuration File

In all of the preceding examples you specified a database initializer in the code itself. You can also specify it in the application configuration file by adding a key in the section. This technique can be useful if you are switching between database initializers often. The key and value must be in a specific format as shown in the following markup:
  1. key="DatabaseInitializerForType CodeFirstDbInitializerDemo.BlogContext,CodeFirstDbInitializerDemo"
  2.  
  3. value="CodeFirstDbInitializerDemo.BlogContextCustomInitializer, CodeFirstDbInitializerDemo" />
Notice the key carefully. It must begin with a predefined value DatabaseInitializerForType followed by a white space and then followed by the assembly qualified name of the context class. The first part of the assembly qualified name is of the form MyNamespace.MyContextClass and the second part is the name of the assembly that contains the context class (CodeFirstDbInitializerDemo). The value is an assembly qualified name of custom database initializer class.
To see the above setting in action, comment out the SetInitializer() call from the Main() method and run the application again. You will find that the application picks up the database initializer details from the configuration file and creates the database as per the logic specified therein.

Summary

Database initialization in Code First refers to creating database and tables based on a model. Code First provides three inbuilt database initializers, viz. CreateDatabaseIfNotExists, DropCreateDatabaseWhenModelChanges and DropCreateDatabaseAlways. You can also create your own database initializer either by inheriting from existing initializers or by implementing the IDatabaseInitializer interface. The SetInitializer() method allows you to specify a database initializer to use for your application. In case your application needs to seed data you can override the Seed() method of the initializer class.

Комментариев нет: