
ذات مرة كان هناك مشروع على EF 6 مع MSSQL DBMS. وكانت هناك حاجة لإضافة القدرة على العمل مع PostgreSQL. لم نتوقع حدوث مشاكل هنا ، لأن هناك عددًا كبيرًا من المقالات حول هذا الموضوع ، وفي المنتديات يمكنك العثور على مناقشة لمشاكل مماثلة. ومع ذلك ، في الواقع ، لم يكن كل شيء بهذه البساطة ، وسنتحدث في هذه المقالة عن هذه التجربة ، وعن المشكلات التي واجهناها أثناء تكامل المزود الجديد ، وعن الحل الذي اخترناه.
خلفية
لدينا منتج محاصر ، ولديه هيكل ثابت بالفعل. في البداية ، تم تكوينه للعمل مع واحد DBMS - MSSQL. يحتوي المشروع على طبقة الوصول إلى البيانات مع تنفيذ EF 6 (نهج Code First). نحن نعمل مع الهجرات من خلال هجرات EF 6. يتم إنشاء عمليات الترحيل يدويًا. يحدث التثبيت الأولي لقاعدة البيانات من تطبيق وحدة التحكم مع تهيئة السياق في سلسلة الاتصال ، وتم تمريره كوسيطة:
static void Main(string[] args) { if (args.Length == 0) { throw new Exception("No arguments in command line"); } var connectionString = args[0]; Console.WriteLine($"Initializing dbcontext via {connectionString}"); try { using (var context = MyDbContext(connectionString)) { Console.WriteLine("Database created"); } } catch (Exception e) { Console.WriteLine(e.Message); throw; } }
في الوقت نفسه ، تم وصف البنية التحتية للنطاق EF ومجال النطاق في مشروع آخر ، مرتبط بتطبيق وحدة التحكم كمكتبة. يبدو مُنشئ السياق في مشروع البنية التحتية كما يلي:
public class MyDbContext : IdentityDbContext<User, Role, Key, UserLogin, UserRole, UserClaim>, IUnitOfWork { public MyDbContext(string connectionString) : base(connectionString) { Database.SetInitializer(new DbInitializer()); Database.Initialize(true); } }
الإطلاق الأول
أول شيء فعلناه هو توصيل مجموعتين بالمشروع عبر nuget: Npgsql و EntityFramework6.Npgsql.
سجلنا أيضًا إعدادات Postgres في App.config لتطبيق وحدة التحكم لدينا.
حدد قسم الكيانFramework مصنع postgres الافتراضي كمصنع اتصال:
<entityFramework> <defaultConnectionFactory type="Npgsql.NpgsqlConnectionFactory, EntityFramework6.Npgsql" /> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> <provider invariantName="Npgsql" type="Npgsql.NpgsqlServices, EntityFramework6.Npgsql" /> </providers> </entityFramework>
في قسم DbProviderFactories ، تم تسجيل مصنع المزود الجديد:
<system.data> <DbProviderFactories> <add name="Npgsql Data Provider" invariant="Npgsql" support="FF" description=".Net Framework Data Provider for Postgresql" type="Npgsql.NpgsqlFactory, Npgsql" /> </DbProviderFactories> </system.data>
وعلى الفور ، حاولوا تهيئة قاعدة البيانات عن طريق تحديد عنوان خادم Postgres وبيانات اعتماد مسؤول الخادم في سلسلة الاتصال. والنتيجة هي السطر التالي:
"الخادم = مضيف محلي ؛ DataBase = TestPostgresDB؛ الأمن المتكامل = خطأ ؛ معرف المستخدم = postgres ؛ كلمة المرور = pa $$ w0rd "
كما هو متوقع ، فبفضل الوضع اليدوي EF Migrations ، لم تمر التهيئة ، وحدث خطأ في عدم تطابق صورة قاعدة بيانات النموذج الحالي. للتغلب على إنشاء الترحيل الأساسي مع الموفر الجديد واختبار تهيئة قاعدة البيانات على Postgres ، قمنا بتعديل تهيئة البنية الأساسية لدينا قليلاً.
أولاً ، قمنا بتشغيل "عمليات الترحيل التلقائي" - وهو خيار مفيد إذا قام أحد المطورين بإجراء تغييرات على نماذج المجال والبنية التحتية EF في الفريق:
public sealed class Configuration : DbMigrationsConfiguration<MyDbContext> { public Configuration() { AutomaticMigrationsEnabled = true; ContextKey = "Project.Infrastructure.MyDbContext"; } }
ثانياً ، لقد حددنا موفرًا جديدًا في الطريقة التي تم إعادة تعريفها في تهيئة قاعدة البيانات للفئة الموروثة CreateDatabaseIfNotExists ، حيث نبدأ عمليات الترحيل:
public class DbInitializer : CreateDatabaseIfNotExists<MyDbContext> { public override void InitializeDatabase(MyDbContext context) { DbMigrator dbMigrator = new DbMigrator(new Configuration {
بعد ذلك ، أطلقنا تطبيق وحدة التحكم مرة أخرى بنفس سلسلة الاتصال كوسيطة. هذه المرة ، تمت تهيئة السياق دون أخطاء ، ونماذج المجال الخاصة بنا تلائم بأمان قاعدة بيانات Postgres الجديدة. ظهرت التسمية "__MigrationHistory" في قاعدة البيانات الجديدة ، حيث كان هناك سجل واحد لأول ترحيل تم إنشاؤه تلقائيًا.
للتلخيص: تمكنا من توصيل مزود جديد بمشروع موجود دون أي مشاكل ، ولكن في نفس الوقت غيرت إعدادات آلية الترحيل.
قم بتشغيل وضع الترحيل اليدوي
كما ذكر أعلاه ، عندما يكون وضع الترحيل التلقائي قيد التشغيل ، فإنك تحرم فريقك من التطوير المتوازي في مجالات الوصول إلى البيانات والبيانات. بالنسبة لنا ، كان هذا الخيار غير مقبول. لذلك ، كنا بحاجة إلى إعداد الوضع اليدوي لعمليات الترحيل في المشروع.
أولاً ، لقد عدنا الحقل AutomaticMigrationsEnabled إلى false. ثم كان من الضروري التعامل مع إنشاء هجرات جديدة. لقد فهمنا أن عمليات ترحيل قواعد بيانات مختلفة ، على الأقل ، يجب تخزينها في مجلدات مشاريع مختلفة. لذلك ، قررنا إنشاء مجلد جديد لترحيل Postgres في مشروع بنية أساسية يسمى PostgresMigrations (المجلد مع migrations MsSql ، من أجل التوضيح ، قمنا بإعادة تسميته باسم MsSqlMigrations) ، وقمنا بنسخ ملف تكوين ترحيل MsSql إليه. في نفس الوقت ، لم ننسخ جميع عمليات ترحيل MsSql الموجودة إلى PostgresSql. أولاً ، نظرًا لأنها تحتوي جميعها على لقطة من التهيئة لموفر MsSql ، وبالتالي ، لن نتمكن من استخدامها على نظام إدارة قواعد البيانات الجديد. ثانياً ، إن تاريخ التغييرات ليس مهمًا لقواعد بيانات إدارة قواعد البيانات الجديدة ، ويمكننا متابعة أحدث لقطة لحالة نماذج المجال.
كنا نظن أن كل شيء كان جاهزًا لتشكيل الترحيل الأول إلى بوستجرس. تم حذف قاعدة البيانات التي تم إنشاؤها أثناء تهيئة السياق باستخدام وضع الترحيل التلقائي. واستناداً إلى حقيقة أنه بالنسبة إلى الترحيل الأول الذي تحتاجه لإنشاء قاعدة بيانات فعلية استنادًا إلى الحالة الحالية لنماذج المجال ، سجلنا بسعادة الأمر Update-Database في وحدة التحكم Manager Manager ، مع تحديد معلمة سلسلة الاتصال فقط. نتيجة لذلك ، حصلنا على خطأ يتعلق بالاتصال بـ DBMS.
بعد دراسة مبدأ العمل لأمر Update-Database بالإضافة إلى ذلك ، قمنا بما يلي:
- أضف الكود التالي إلى إعدادات تكوين الترحيل:
ل MsSql:
public Configuration() { AutomaticMigrationsEnabled = false; ContextKey = "Project.Infrastructure.MyDbContext"; MigrationsDirectory = @"MsSqlMigrations"; }
ل Postgres:
public Configuration() { AutomaticMigrationsEnabled = false; ContextKey = "Project.Infrastructure.MyDbContext"; MigrationsDirectory = @"PostgresMigrations"; }
- الإشارة إلى المعلمة الضرورية للأمر Update-Database لتمرير اسم الموفر
- إضافة معلمات تشير إلى المشروع الذي يحتوي على وصف للبنية التحتية ef ، والمجلد مع تكوين الترحيل للموفر الجديد
نتيجة لذلك ، حصلنا على هذا الأمر:
تحديث قاعدة البيانات - اسم المشروع "Project.Infrastructure" - التكوين Project Name: Project.Infrastructure.PostgresMigrations.Configuration -ConnectionString "Server = localhost؛ DataBase = TestPostgresDB؛ الأمن المتكامل = خطأ ؛ معرف المستخدم = postgres ؛ كلمة المرور = pa $$ w0rd "-ConnectionProviderName" Npgsql "
بعد تنفيذ هذا الأمر ، تمكنا من تنفيذ أمر Add-Migration باستخدام معلمات مماثلة ، وتسمية الترحيل الأول InitialCreate:
الوظيفة الإضافية - الاسم "InitialCreate" - اسم المشروع "CrossTech.DSS.Infrastructure" - التكوين - اسم النوع CrossTech.DSS.Infrastructure.PostgresMigrations.Configuration -ConnectionString "Server = localhost؛ DataBase = TestPostgresDB؛ الأمن المتكامل = خطأ ؛ معرف المستخدم = postgres ؛ كلمة المرور = pa $$ w0rd "-ConnectionProviderName" Npgsql "
ظهر ملف جديد في مجلد PostgresMigrations: 2017010120705068_InitialCreate.cs
ثم قمنا بحذف قاعدة البيانات التي تم إنشاؤها بعد تنفيذ أمر Update-Database وقمنا بتشغيل تطبيق وحدة التحكم لدينا مع سلسلة الاتصال المشار إليها أعلاه كوسيطة. وهكذا حصلنا على قاعدة البيانات بالفعل على أساس الترحيل الذي تم إنشاؤه يدويًا.
للتلخيص: تمكنا ، بأقل جهد ممكن ، من إضافة الترحيل الأول لموفر Postgres وتهيئة السياق من خلال تطبيق وحدة التحكم ، والحصول على قاعدة بيانات جديدة ، والتي جاءت فيها التغييرات من الترحيل اليدوي الأول.
التبديل بين مقدمي الخدمات
لا يزال لدينا سؤال واحد مفتوح: كيفية تكوين تهيئة السياق بحيث كان من الممكن الوصول إلى قواعد بيانات معينة في وقت التشغيل؟
كانت المهمة أنه في مرحلة التهيئة للسياق ، كان من الممكن اختيار قاعدة بيانات مستهدفة واحدة أو أخرى للموفر المطلوب. نتيجة للمحاولات المتكررة لتهيئة رمز التبديل هذا ، توصلنا إلى حل يشبه هذا.
في تطبيق وحدة التحكم للمشروع في app.config (وإذا كنت لا تستخدم app.config ، ثم machine.config) ، فإننا نضيف سلسلة اتصال جديدة مع الموفر واسم الاتصال ، وفي مُنشئ السياق ، نقوم "بإسقاط" اسم الاتصال بدلاً من سلسلة الاتصال. في نفس الوقت ، نقوم بتوصيل سلسلة الاتصال نفسها بالسياق من خلال المفرد لمثيل DbConfiguration. نقوم بتمرير مثيل الفئة الموروثة من DbConfiguration كمعلمة.
فئة DbConfiguration الناتجة الموروثة:
public class DbConfig : DbConfiguration { public DbConfig(string connectionName, string connectionString, string provideName) { ConfigurationManager.ConnectionStrings.Add(new ConnectionStringSettings(connectionName, connectionString, provideName)); switch (connectionName) { case "PostgresDbConnection": this.SetDefaultConnectionFactory(new NpgsqlConnectionFactory()); this.SetProviderServices(provideName, NpgsqlServices.Instance); this.SetProviderFactory(provideName, NpgsqlFactory.Instance); break; case "MsSqlDbConnection": this.SetDefaultConnectionFactory(new SqlConnectionFactory()); this.SetProviderServices(provideName, SqlProviderServices.Instance); this.SetProviderFactory(provideName, SqlClientFactory.Instance); this.SetDefaultConnectionFactory(new SqlConnectionFactory()); break; } } }
وتبدو تهيئة السياق نفسها الآن كالتالي:
var connectionName = args[0]; var connectionString = args[1]; var provideName = args[2]; DbConfiguration.SetConfiguration(new DbConfig(connectionName, connectionString, provideName)); using (var context = MyDbContext(connectionName)) { Console.WriteLine("Database created"); }
والذين تابعوا بعناية ، ربما لاحظ أنه كان علينا إجراء تغيير آخر في الكود. هذا هو تعريف قاعدة البيانات الهدف أثناء تهيئة قاعدة البيانات ، والذي يحدث في أسلوب InitializeDatabase الموضح سابقًا.
أضفنا مفتاحًا بسيطًا لتحديد تكوين الترحيل لموفر معين:
public class DbInitializer : CreateDatabaseIfNotExists<MyDbContext> { private string _connectionName; public DbInitializer(string connectionName) { _connectionName = connectionName; } public override void InitializeDatabase(MyDbContext context) { DbMigrationsConfiguration<MyDbContext> config; switch (_connectionName) { case "PostgresDbConnection": config = new PostgresMigrations.Configuration(); break; case "MsSqlDbConnection": config = new MsSqlMigrations.Configuration(); break; default: config = null; break; } if (config == null) return; config.TargetDatabase = new DbConnectionInfo(_connectionName); DbMigrator dbMigrator = new DbMigrator(config);
وبدأ مُنشئ السياق نفسه في الظهور كما يلي:
public MyDbContext(string connectionNameParam) : base(connectionString) { Database.SetInitializer(new DbInitializer(connectionName = connectionNameParam)); Database.Initialize(true); }
بعد ذلك ، أطلقنا تطبيق وحدة التحكم وحددنا معلمة تطبيق MsSql كموفر DBMS. قمنا بتعيين الوسائط للتطبيق كما يلي:
"MsSqlDbConnection" "Server = المضيف المحلي \ SQLEXPRESS؛ قاعدة البيانات = TestMsSqlDB؛ معرف المستخدم = sa ؛ كلمة المرور = pa $$ w0rd "" System.Data.SqlClient "
تم إنشاء قاعدة بيانات MsSql دون أخطاء.
ثم حددنا وسيطات التطبيق:
"PostgresDbConnection" "الخادم = مضيف محلي ؛ DataBase = TestPostgresDB؛ الأمن المتكامل = خطأ ؛ معرف المستخدم = postgres ؛ كلمة المرور = pa $$ w0rd "" Npgsql "
تم إنشاء قاعدة بيانات Postgres أيضًا دون أخطاء.
لذلك ، هناك مجاميع فرعية أخرى - لكي تتمكن EF من تهيئة سياق قاعدة البيانات لموفر معين ، في وقت التشغيل ، تحتاج إلى:
- "حدد" آلية الترحيل إلى هذا الموفر
- تكوين سلاسل اتصال DBMS قبل تهيئة السياق
نحن نعمل مع هجرات اثنين من نظم إدارة قواعد البيانات في فريق
كما رأينا ، يبدأ الجزء الأكثر إثارة للاهتمام بعد ظهور تغييرات جديدة في المجال. تحتاج إلى إنشاء عمليات ترحيل لاثنين من قواعد بيانات قواعد البيانات (DBMS) مع مراعاة موفر معين.
لذلك ، بالنسبة لخادم MSSQL ، تحتاج إلى تنفيذ أوامر متسلسلة (بالنسبة إلى Postgres ، الأوامر الموضحة أعلاه ، عند إنشاء الترحيل الأول):
- تحديث قاعدة البيانات وفقا لآخر لقطة
تحديث قاعدة البيانات - اسم المشروع "Project.Infrastructure" - التكوين Project Name.Infrastructure.MsSqlMigrations.Configuration -ConnectionString "Server = localhost؛ DataBase = TestMsSqlDB؛ الأمن المتكامل = خطأ ؛ معرف المستخدم = sa ؛ كلمة المرور = pa $$ w0rd "-ConnectionProviderName" System.Data.SqlClient "
- مضيفا الهجرة الجديدة
إضافة-الترحيل-الاسم "SomeMigrationName" -المشروع "Project.Infrastructure" -تكوين "اسم المشروع". البنية التحتية. MSSqlMigrations.Configuration -ConnectionString "Server = localhost؛ DataBase = TestMsSqlDB؛ الأمن المتكامل = خطأ ؛ معرف المستخدم = sa ؛ كلمة المرور = pa $$ w0rd "-ConnectionProviderName" System.Data.SqlClient "
عندما يقوم المطورون بإجراء تغييرات على المجال بشكل متوازٍ ، فإننا نواجه تعارضات متعددة عند دمج هذه التغييرات في نظام التحكم في الإصدار (للبساطة التي نسميها git). هذا يرجع إلى حقيقة أن عمليات الترحيل إلى EF تسير بالتتابع واحد تلو الآخر. وإذا قام أحد المطورين بإنشاء عملية ترحيل ، فلن ينجح مطور آخر ببساطة في إضافة الترحيل بالتتابع. كل ترحيل لاحق يخزّن معلومات حول السابقة. وبالتالي ، من الضروري تحديث لقطات النموذج المسمى في الترحيل إلى آخر مرة تم إنشاؤها.
في الوقت نفسه ، فإن حل النزاعات على هجرات EF في فريق ينال الأولوية لتحديد أهمية التغييرات لمطور معين. ولأن التغييرات التي تكون أعلى في الأولوية ، يجب أن تكون تلك أول من يملأها ، وبقية المطورين وفقًا للتسلسل الهرمي المتفق عليه يحتاجون إلى القيام بما يلي:
- حذف إنشاء الهجرات المحلية
- قم بسحب التغييرات من المستودع إلى نفسك ، حيث قام بالفعل الزملاء الآخرون ذوو الأولوية العالية بصب هجراتهم بالفعل
- إنشاء الهجرة المحلية وتحميل التغييرات الناتجة مرة أخرى إلى بوابة
بقدر ما نحن على دراية بآلية ترحيل EF ، يمكننا الحكم على أن نهج تطوير الفريق الموصوف هو النهج الوحيد في الوقت الحالي. نحن لا نعتبر هذا الحل مثاليًا ، لكن له الحق في الحياة. وأصبحت مسألة إيجاد بديل لآلية EF Migrations ملحّة بالنسبة إلينا.
في الختام
يعد العمل مع العديد من نظم إدارة قواعد البيانات (DBMS) باستخدام EF6 بالاقتران مع EF Migrations أمرًا حقيقيًا ، ولكن في هذا الإصدار ، لم يأخذ الرجال من Microsoft في الاعتبار إمكانية العمل المتوازي للفريق الذي يستخدم أنظمة التحكم في الإصدار.
هناك العديد من حلول EF Migrations البديلة في السوق (المدفوعة والمجانية): DbUp ، RoundhousE ، ThinkingHome.Migrator ، FluentMigrator ، إلخ. واستنادا إلى الاستعراضات ، فهي أكثر مثل المطورين من EF Migrations.
لحسن الحظ ، لدينا الآن فرصة لإجراء نوع من الترقية في مشروعنا. وفي المستقبل القريب ، سنتحول إلى EF Core. لقد قمنا بوزن إيجابيات وسلبيات آلية EF Core Migrations وتوصلنا إلى استنتاج مفاده أنه سيكون من الأنسب لنا العمل مع حل الطرف الثالث ، وهو Fluent Migrator.
نأمل أن تكونوا مهتمين بتجربتنا. على استعداد لقبول التعليقات والإجابة على الأسئلة ، ويلكوم!