إضافة حقل جديد لقاعدة البيانات انطلاقاً من كود سي شارب Code First Method

في هذه التدوينة سنستخدم Entity Framework Code First Migrations لعمل بعض التغييرات في model classes بحيث تطبق هذه التغييرات على قاعدة البيانات. أي تعديل قاعدة البيانات عن طريق كود سي شارب.

بشكل افتراضي عند استخدام Entity Framework Code First لإنشاء قاعدة البيانات بشكل أوتوماتيكي كما فعلنا سابقا في هذه السلسلة فإن Code First  تضيف جدول إلى قاعدة البيانات للتحقق فيما إذا كانت بنية قاعدة البيانات متزامنة مع model classes فإذا لم يكن هناك تزامن فإن Entity Framework تعطي خطأ و هذا يجعل من السهل تعقب المشاكل في وقت التطوير.

هذه المقالة جزء من سلسلة لتعلم أساسيات ASP.NET MVC للمبتدئين:

إعداد Code First Migrations للنموذج

في Solution Explorer قم بالنقر بزر الفأرة اليميني على الملف Books.mdf وقم باختيار Delete لحذف قاعدة البيانات

في حال عدم ظهور الملف  Books.mdf قم بالنقر على أيقونة Show All Files كما هو موضح

قم بعمل Build للتطبيق للتأكد من عدم وجود أخطاء

من القائمة Tools قم بالنقر على  NuGet Package Manager ثم قم باخيتار  Package Manager Console

في نافذة Package Manager Console عند المؤشر <PM  قم بإدخال التعليمة التالية

Enable-Migrations -ContextTypeName ArabicArchive.Models.BookDBContext

إن تعليمة Enable-Migrations تضيف ملف Configuration.cs  في المجلد الجديد Migrations

يقوم Visual Studio بفتح الملف Configuration.cs 

قم باستبدال الكود في التابع Seed في ملف Configuration.cs بالكود التالي

    protected override void Seed(ArabicArchive.Models.BookDBContext context)
        {
           
            context.Types.AddOrUpdate(t => t.Title,
                new Models.Type
                {
                    Title = "Programming",
                    Description = "Books in computer programming field"
                },
                new Models.Type
                {
                     Title = "Mathematics",
                     Description = "Books in math field"
                }
                );
             context.SaveChanges();
            context.Books.AddOrUpdate(b => b.Title,
                new Book
                {
                    Title = "Clean Code",
                    Description = "A Handbook of Agile Software",
                    PublishDate = DateTime.Parse("2009-01-01"),
                    Author = "Robert C. Martin",
                    GoodreadsRate=4.5,
                    NumberOfPages=413,
                    TypeId=context.Types.Single(t => t.Title=="Programming").Id
             
                 },
                 new Book
                 {
                     Title = "HTML and CSS",
                     Description = "Design and Build Websites",
                     PublishDate = DateTime.Parse("2011-01-01"),
                     Author = "Jon Duckett",
                     GoodreadsRate = 4.5,
                     NumberOfPages = 493,
                     TypeId = context.Types.Single(t => t.Title == "Programming").Id

                 },
                 new Book
                  {
                    Title = "Calculus",
                    Description = "Early Transcendentals",
                    PublishDate = DateTime.Parse("2016-01-01"),
                    Author = "James Stewart",
                    GoodreadsRate = 4,
                    NumberOfPages = 1180,
                    TypeId = context.Types.Single(t => t.Title == "Mathematics").Id

                   },
                 new Book
                 {
                     Title = "Clean Code 2",
                     Description = "A Handbook of Agile Software",
                     PublishDate = DateTime.Parse("2010-01-01"),
                     Author = "Robert C. Martin",
                     GoodreadsRate = 4,
                     NumberOfPages = 500,
                     TypeId = context.Types.Single(t => t.Title == "Programming").Id

                 }

                );
        }

ملاحظة:بعد إضافة أنواع الكتب  نقوم باستدعاء التابع  ()context.SaveChanges ليتم إضافة أنواع الكتب أولاً إلى قاعدة البيانات بعد ذلك نضيف الكتب إلى القاعدة

 قم بالنقر بزر الفأرة اليميني على الخط الأحمر تحت كلمة Book  ثم اختر  using ArabicArchive.Models

بعذ ذلك يتم إضافة عبارة using التالية:

 using Models;

 ملاحظة:

إن Code First Migrations تستدعي التابع Seed بعد كل Migration وهذا التابع يحدث  الأسطر إذا كانت  موجودة مسبقاً أو يضيف أسطر جديدة في حال لم تكن موجودة.

إن التابع  AddOrUpdate في الكود التالي ينفذ عملية upsert

context.Books.AddOrUpdate(b => b.Title,
                new Book
                {
                    Title = "Clean Code",
                    Description = "A Handbook of Agile Software",
                    PublishDate = DateTime.Parse("2009-01-01"),
                    Author = "Robert C. Martin",
                    GoodreadsRate=4.5,
                    NumberOfPages=413,
                    TypeId=context.Types.Single(t => t.Title=="Programming").Id
             
                 }

ولأن تابع Seed ينفذ مع كل عملية migration فإننا لانستطيع إضافة البيانات فقط لأن الأسطر التي نحاول إضافتها موجودة مسبقاً حيث يتم إضافتها عند أول عملية  migration والتي تؤدي إلى إنشاء قاعدة البيانات.

إن عملية upsert تمنع الأخطاء التي يمكن أن تحدث عند محاولة إضافة سطر موجود مسبقاً حيث أنها تقوم بإجراء التعديلات على  البيانات التي قمنا بها أثناء اختبار التطبيق.

في بعض الحالات عند تعديل البيانات أثناء الاختبار نريد أن تبقى هذه  التعديلات بعد تحديث قاعدة البيانات وبالتالي نحتاج لتنفيذ تعليمة إضافة شرطية حيث أنها تقوم بإضافة سطر إذا لم يكن موجود مسبقاً. إن أول بارامتر ممرر للتابع AddOrUpdate  يحدد الخاصية التي نريد استخدامها لفحص فيما إذا كان السطر موجود.

من أجل بيانات الاختبار للكتاب  قمنا بتحديد الخاصية Title حيث أن كل عنوان في اللائحة غير مكرر:

context.Books.AddOrUpdate(b => b.Title,

 في هذا الكود نفترض أن العناوين للكتب هي غير مكررة فإذا قمنا بشكل يدوي بإضافة عنوان مكرر فإننا سنحصل على exception التالي في المرة التالية التي نقوم بها بعمل migration

Sequence contains more than one element

قم بعمل Build للمشروع بالضغط على CTRL-SHIFT-B 

الخطوة التالية هي إضافة  DbMigration class من أجل initial migration

إن هذه migration تضيف قاعدة بيانات جديدة ولهذا السبب قمنا بحذف الملف  Books.mdf في الخطوة السابقة

في نافذة Package Manager Console قم بإدخال الأمر add-migration Initial من أجل إضافة initial migration

إن الاسم Initial هو اسم  ملف ال migration الذي سيتم إضافته عند تنفيذ الأمر السابق ويمكن استخدام أي اسم نريده في التسمية:

بعد تنفيذ الأمر السابق بنجاح فإن Code First Migrations تضيف ملف  class في مجلد Migrations بحيث يكون اسم الملف {DateStamp_Initial.cs} وهذا الclass  يحتوي على الكود اللازم لتوليد بنية قاعدة البيانات.

إن اسم ملف migration   مسبوق بختم زمني للمساعدة في عملية الترتيب لعمليات migrations التي قمنا بها

قم بفتح الملف DateStamp_Initial.cs حيث يحتوي على التعليمات اللازمة لإضافة الجدول Books والجدول Types إلى قاعدة البيانات. عند تحديث قاعدة البيانات من خلال التعليمات التالية فإن الملف   {DateStamp_Initial.cs} سينفذ ويضيف بنية قاعدة البيانات بعد ذلك سينفذ تابع Seed لإضافة بيانات الاختبار للقاعدة.

في Package Manager Console قم بإدخال الأمر update-database لإضافة قاعدة البيانات وتنفيذ تابع Seed:

في حال الحصول على خطأ بأن الجدول موجود مسبقاً ولايمكن إعادة إضافته فيكون هذا الخطأ بسبب تشغيل التطبيق بعد حذف قاعدة البيانات وقبل تنفيذ الأمر update-database.

في هذه الحالة قم بحذف الملف Books.mdf  وقم بتنفيذ الأمر update-database  وفي حال استمرار الحصول على نفس الخطأ قم بحذف المجلد migrations  مع محتوياته وقم بإعادة تنفيذ الخطوات التي تم  ذكرها في بداية هذه المقالة.

قم بتشغيل التطبيق وانتقل إلى الرابط Books/ فنلاحظ أن  seed data تم إضافتها إلى قاعدة البيانات:

إضافة الخاصية Rating إلى Book Model 

سنبدأ أولاً بإضافة الخاصية الجديدة  Rating إلى Book class ، قم بفتح الملف Models\Book.cs وقم بإضافة الخاصية Rating كما هو موضح

public string Rating { set; get; }

الكود التالي هو الكود الكامل ل Book class

 public class Book
    {
      public int Id { set; get; }
      public  string Title { set; get; }
      public  string Description { set; get; }
      [Display(Name = "Publish Date")]
      [DataType(DataType.Date)]
      [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
      public DateTime PublishDate { set; get; }
      public   string Author { set; get; }
      public   int NumberOfPages { set; get; }
      public   double GoodreadsRate { set; get; } 
      public string Rating { set; get; }
      public   int TypeId { set; get; }
      public virtual Type Type { set; get; }
    }

 قم بعمل Build للتطبيق بالضغط على Ctrl+Shift+B

بما أننا قمنا بإضافة خاصية جديدة إلى Book class فإننا نحتاج أيضاً إلى تحديث الخاصية bind لتابع Create و تابع Edit في ملف BooksController.cs لإضافة الخاصية Rating

[Bind(Include = "Id,Title,Description,PublishDate,Author,NumberOfPages,GoodreadsRate,TypeId,Rating")]

أيضاً يجب أن نقوم بتحديث view templates لعرض و إضافة وتحديث الخاصية Rating

قم بفتح الملف Views\Books\Index.cshtml\ وقم بإضافة العمود <th>Rating</th> بعد العمود GoodreadsRate

ثم قم بإضافة العمود <td> في نهاية template  لعرض قيمة الخاصية  Rating للعنصر (item.Rating@)

الكود التالي هو للملف Index.cshtml

@model IEnumerable<ArabicArchive.Models.Book>

@{
ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
@Html.ActionLink("Create New", "Create")
@using (Html.BeginForm("Index","Books",FormMethod.Get))
{
<p>
Type:@Html.DropDownList("BookTypeId", "All")
Title: @Html.TextBox("SearchString") <br />
<input type="submit" value="Filter" /> </p>
}
</p>
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.Type.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Description)
</th>
<th>
@Html.DisplayNameFor(model => model.PublishDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Author)
</th>
<th>
@Html.DisplayNameFor(model => model.NumberOfPages)
</th>
<th>
@Html.DisplayNameFor(model => model.GoodreadsRate)
</th>
<th>
@Html.DisplayNameFor(model => model.Rating)
</th>
<th></th>
</tr>

@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Type.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Description)
</td>
<td>
@Html.DisplayFor(modelItem => item.PublishDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Author)
</td>
<td>
@Html.DisplayFor(modelItem => item.NumberOfPages)
</td>
<td>
@Html.DisplayFor(modelItem => item.GoodreadsRate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Rating)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
@Html.ActionLink("Details", "Details", new { id=item.Id }) |
@Html.ActionLink("Delete", "Delete", new { id=item.Id })
</td>
</tr>
}

</table>

 قم بفتح الملف Views\Movies\Create.cshtml\ وقم بإضافة الحقل Rating كما هو موضح بالكود الملون التالي

حيث يقوم هذا الكود بإضافة text box لإدخال ال Rating عند إضافة عند إضافة كتاب جديد

@Html.LabelFor(model => model.GoodreadsRate, htmlAttributes: new { @class = “control-label col-md-2” })

@Html.EditorFor(model => model.GoodreadsRate, new { htmlAttributes = new { @class = “form-control” } }) @Html.ValidationMessageFor(model => model.GoodreadsRate, “”, new { @class = “text-danger” })

</div>

@Html.LabelFor(model => model.Rating, htmlAttributes: new { @class = “control-label col-md-2” })

@Html.EditorFor(model => model.Rating, new { htmlAttributes = new { @class = “form-control” } }) @Html.ValidationMessageFor(model => model.Rating, “”, new { @class = “text-danger” })

</div>

@Html.LabelFor(model => model.TypeId, “Type”, htmlAttributes: new { @class = “control-label col-md-2” })

@Html.DropDownList(“TypeId”, null, htmlAttributes: new { @class = “form-control” }) @Html.ValidationMessageFor(model => model.TypeId, “”, new { @class = “text-danger” })

</div>

 

</div> </div> }

@Html.ActionLink(“Back to List”, “Index”)

@section Scripts { @Scripts.Render(“~/bundles/jqueryval”) }

بهذه الإضافات التي قمنا بها تم تحيدث الكود بحيث يدعم الخاصية Rating الجديدة

قم بتشغيل التطبيق وانتقل إلى الرابط Books/ وعندها سنرى الخطأ التالي

ويكون نص الخطأ

The model backing the ‘BookDBContext’ context has changed since the database was created. Consider using Code First Migrations to update the database(https://go.microsoft.com/fwlink/?LinkId=238269).

سبب ظهور هذا الخطأ لأننا قمنا بتحديث Book model class في التطبيق وهو الآن مختلف عن بنية الجدول Books  في قاعدة البيانات حيث أنه لايوجد عمود Rating في جدول قاعدة البيانات.

هناك عدة طرق لحل هذا الخطأ:

الطريقة الأولى: جعل Entity Framework وبشكل أتوماتيكي تحذف وتعيد إنشاء قاعدة البيانات بالاعتماد على البنية الجديدة ل  model class ، هذه الطريقة مريحة جداً في المراحل الأولى من دورة التطوير عندما نقوم بالتطوير النشط على قاعدة بيانات الاختبار، فهي تسمح لنا بسرعة تطوير model  وبنية قاعدة البيانات معاً

السيئة لهذه الطريقة هي أننا نفقد المعطيات الموجودة في قاعدة البيانات حيث أننا لانستخدم هذه الطريقة على قاعدة البيانات التي تحتوي بيانات التطبيق الفعلية.

يتم في هذه الطريقة استخدام initializer  ليتم إدخال بيانات الاختبار إلى قاعدة البيانات بشكل أوتوماتيكي للمزيد من المعلومات قم بالدخول للرابط التالي ASP.NET MVC/Entity Framework tutorial

الطريقة الثانية: تعديل بنية قاعدة البيانات وبشكل صريح بحيث تطابق model classes

فائدة هذه الطريقة هي المحافظة على البيانات الموجودة مسبقاً في قاعدة البيانات

نستطيع القيام بالتعديل إما بشكل يدوي أو عن طريق إنشاء  database change script

الطريقة الثالثة: هي باستخدام Code First Migrations لتحديث بنية قاعدة البيانات

في هذا الدرس سنستخدم Code First Migrations

قم بتحديث Seed method بحيث يتم إضافة قيمة في العمود الجديد

قم بفتح الملف Migrations\Configuration.cs وقم بإضافة الحقل Rating لكل book object

new Book
                {
                    Title = "Clean Code",
                    Description = "A Handbook of Agile Software",
                    PublishDate = DateTime.Parse("2009-01-01"),
                    Author = "Robert C. Martin",
                    GoodreadsRate = 4.5,
                    NumberOfPages = 413,
                    Rating="G",
                    TypeId = context.Types.Single(t => t.Title == "Programming").Id

                },

قم بعمل Build للتطبيق وقم بفتح نافذة Package Manager Console 

و قم بإدخال الأمر التالي add-migration Rating

إن الأمر add-migration يخبر migration framework لاكتشاف book model الحالي ومقارنته مع بنية الجدول book في قاعدة البيانات وإضافة الكود اللازم لتحديث قاعدة البيانات بحيث تطابق ال model  الجديد

إن الأسم  Rating هو اسم  ملف ال migration الذي سيتم إضافته عند تنفيذ الأمر السابق ويمكن استخدام أي اسم نريده في التسمية ويفضل استخدام اسم ذو معنى يدل على migration التي قمنا بها

عند انتهاء تنفيذ الأمر السابق يقوم Visual Studio بفتح ملف  class وهو عبارة عن class  يرث من DbMIgration class ونلاحظ في تابع Up الكود لإضافة عامود جديد

 public partial class Rating : DbMigration
    {
        public override void Up()
        {
            AddColumn("dbo.Books", "Rating", c => c.String());
        }
        
        public override void Down()
        {
            DropColumn("dbo.Books", "Rating");
        }
    }

قم بعمل Build  للتطبيق وقم بإدخال الأمر update-database في نافذة Package Manager Console

الصورة التالية تعرض خرج نافذة Package Manager Console 

قم بتشغيل التطبيق وانتقل إلى الرابط Books/ فنلاحظ ظهور الحقل Rating

قم بالنقر على الرابط Create New لإضافة كتاب جديد مع ملاحظة أننا نستطيع إضافة  Rating للكتاب الجديد

قم بالنقر على الزر Create لإضافة الكتاب فنلاحظ ظهور الكتاب الجديد مع الخاصية Rating في لائحة الكتب

إن المشروع الآن يستخدم migrations وبالتالي لن نقوم بحذف قاعدة البيانات وإعادة إنشاؤها عندما نريد إضافة حقل جديد

في الدرس القادم سوف نقوم بعمل تعديلات على بنية قاعدة البيانات واستخدام migrations  لتعديل قاعدة البيانات

يجب أيضاً أن نقوم بإضافة الخاصية Rating إلى Edit view  و Details view و Delete view

يمكن إدخال الأمر update-database في نافذة Package Manager Console وبالتالي لن يتم تنفيذ أي كود migration بسبب أن بنية قاعدة البيانات تطابق model classes

إن تنفيذ الأمر update-database سينفذ التابع Seed مرة ثانية

في هذا الدرس قمنا بتعديل model objects و مزامنة قاعدة البيانات مع model

في الدرس القادم  سوف نضيف validation logic إلى  model classes وتفعيل بعض قواعد العمل الإجبارية.

تغذية راجعة

اتمنى عدم التردد في الاستفسار عن أي مفهوم تم ذكره في هذا التدوينة، وأرجو تجربة ماورد في التدوينة بشكل فعلي وعدم الاكتفاء بالقراءة لتحقيق أكبر فائدة ممكنة، وكالعادة سأكون سعيد إن شاركتموني تجربتكم وتصويباتكم في حال وجود أخطاء كتابية.

 

هذه المقالة مستندة إلى سلسلة دروس مايكروسوفت الرسمية للـ ASP.NET MVC، وذلك لترتيب الدروس المناسب واعتقادي بسلاستها وأهمية نقلها إلى العربية بأسلوب مناسب وتجربة تتوافق مع الأدوات المتاحة لنا والمتوفرة في منطقتنا.

مدير تقني وشريك مؤسس لـ فسيلة تِك، مبرمج متعدد المهارات، مهتم في إنجاز أمور استثنائية في مجال التكنولوجيا وأعمل جاهداً لترك أثر إيجابي في الحياة