An unhandled exception of type 'System.NotSupportedException' occurred in EntityFramework.dll Additional information: Unable to determine the DbProviderFactory type for connection of type 'System.Data.SqlClient.SqlConnection'. Make sure that the ADO.NET provider is installed or registered in the application config.
الف) دریافت کدهای EFTracingProvider اصلی: (+)
از کدهای دریافتی این مجموعه، فقط به دو پوشه EFTracingProvider و EFProviderWrapperToolkit آن نیاز است.
ب) اصلاح کوچکی در کدهای این پروایدر جهت بررسی نال بودن شیءایی که باید dispose شود
در فایل DbConnectionWrapper.cs، متد Dispose را یافته و به نحو زیر اصلاح کنید (بررسی نال نبودن wrappedConnection اضافه شده است):
protected override void Dispose(bool disposing) { if (disposing) { if (this.wrappedConnection != null) this.wrappedConnection.Dispose(); } base.Dispose(disposing); }
ج) ساخت یک کلاس پایه Context با قابلیت لاگ فرامین SQL صادره، جهت میسر سازی استفاده مجدد از کدهای آن
د) رفع خطای The given key was not present in the dictionary در حین استفاده از EFTracingProvider
در ادامه کدهای کامل این دو قسمت به همراه یک مثال کاربردی را ملاحظه میکنید:
using System; using System.Configuration; using System.Data; using System.Data.Common; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Data.Entity.Migrations; using System.Diagnostics; using System.Linq; using EFTracingProvider; namespace Sample { public class Person { public int Id { get; set; } public string Name { get; set; } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { var className = this.ContextType.FullName; var connectionStringData = ConfigurationManager.ConnectionStrings[className]; if (connectionStringData == null) throw new InvalidOperationException(string.Format("ConnectionStrings[{0}] not found.", className)); TargetDatabase = new DbConnectionInfo(connectionStringData.ConnectionString, connectionStringData.ProviderName); AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { for (int i = 0; i < 7; i++) context.Users.Add(new Person { Name = "name " + i }); base.Seed(context); } } public class MyContext : MyLoggedContext { public DbSet<Person> Users { get; set; } } public abstract class MyLoggedContext : DbContext { protected MyLoggedContext() : base(existingConnection: createConnection(), contextOwnsConnection: true) { var ctx = ((IObjectContextAdapter)this).ObjectContext; ctx.GetTracingConnection().CommandExecuting += (s, e) => { Console.WriteLine("{0}\n", e.ToTraceString()); }; } private static DbConnection createConnection() { var st = new StackTrace(); var sf = st.GetFrame(2); // Get the derived class Type in a base class static method var className = sf.GetMethod().DeclaringType.FullName; var connectionStringData = ConfigurationManager.ConnectionStrings[className]; if (connectionStringData == null) throw new InvalidOperationException(string.Format("ConnectionStrings[{0}] not found.", className)); if (!isEFTracingProviderRegistered()) EFTracingProviderConfiguration.RegisterProvider(); EFTracingProviderConfiguration.LogToFile = "log.sql"; var wrapperConnectionString = string.Format(@"wrappedProvider={0};{1}", connectionStringData.ProviderName, connectionStringData.ConnectionString); return new EFTracingConnection { ConnectionString = wrapperConnectionString }; } private static bool isEFTracingProviderRegistered() { var data = (DataSet)ConfigurationManager.GetSection("system.data"); var providerFactories = data.Tables["DbProviderFactories"]; return providerFactories.Rows.Cast<DataRow>() .Select(row => (string)row.ItemArray[1]) .Any(invariantName => invariantName == "EF Tracing Data Provider"); } } public static class Test { public static void RunTests() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); using (var ctx = new MyContext()) { var users = ctx.Users.AsEnumerable(); if (users.Any()) { foreach (var user in users) { Console.WriteLine(user.Name); } } var rnd = new Random(); var user1 = ctx.Users.Find(1); user1.Name = "test user " + rnd.Next(); ctx.SaveChanges(); } } } }
توضیحات:
تعریف TargetDatabase در Configuration سبب میشود تا خطای The given key was not present in the dictionary در حین استفاده از این پروایدر جدید برطرف شود. به علاوه همانطور که ملاحظه میکنید اطلاعات رشته اتصالی بر اساس قراردادهای توکار EF Code first به نام کلاس Context تنظیم شده است.
کلاس MyLoggedContext، کلاس پایهای است که تنظیمات اصلی «EF Tracing Data Provider» در آن قرار گرفتهاند. برای استفاده از آن باید رشته اتصالی مخصوصی تولید و در اختیار کلاس پایه DbContext قرار گیرد (توسط متد createConnection ذکر شده).
به علاوه در اینجا توسط خاصیت EFTracingProviderConfiguration.LogToFile میتوان نام فایلی را که قرار است عبارات SQL تولیدی در آن درج شوند، ذکر نمود. همچنین یک روش دیگر دستیابی به کلیه عبارات SQL تولیدی را با مقدار دهی CommandExecuting در سازنده کلاس مشاهده میکنید.
اکنون که این کلاس پایه تهیه شده است، تنها کافی است Context معمولی برنامه به نحو زیر تعریف شود:
public class MyContext : MyLoggedContext
insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 0" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 1" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 2" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 3" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 4" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 5" insert [dbo].[People]([Name]) values (@0) select [Id] from [dbo].[People] where @@ROWCOUNT > 0 and [Id] = scope_identity() -- @0 (dbtype=String, size=-1, direction=Input) = "name 6" SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name] FROM [dbo].[People] AS [Extent1] SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name] FROM [dbo].[People] AS [Extent1] name 0 name 1 name 2 name 3 name 4 name 5 name 6 update [dbo].[People] set [Name] = @0 where ([Id] = @1) -- @0 (dbtype=String, size=-1, direction=Input) = "test user 1355460609" -- @1 (dbtype=Int32, size=0, direction=Input) = 1
قسمتی از این خروجی مرتبط است به متد Seed تعریف شده که تعدادی رکورد را در بانک اطلاعاتی ثبت میکند.
دو select نیز در انتهای کار قابل مشاهده است. اولین مورد به علت فراخوانی متد Any صادر شده است و دیگری به حلقه foreach مرتبط میباشد (چون از AsEnumerable استفاده شده، هربار ارجاع به شیء users، یک رفت و برگشت به بانک اطلاعاتی را سبب خواهد شد. برای رفع این حالت میتوان از متد ToList استفاده کرد.)
در پایان کار، متد update مربوط است به فراخوانی متدهای find و save changes ذکر شده. این خروجی در فایل sql.log نیز در کنار فایل اجرایی برنامه ثبت شده و قابل مشاهده میباشد.
کاربردها
اطلاعات این مثال میتواند پایه نوشتن یک برنامه entity framework profiler باشد.
- تنظیمات اولیه ASP.NET Identity 2.1 با Web API
- ایجاد Account Confirmation به وسیله Identity به همراه تنظیمات policy برای user name و password
- توسعه OAuth Json Web Token Authentication به کمک Web API و Identity
- ایجاد یک سیستم Role Based و تایید صلاحیتهای مربوط به آن
- توسعه Web API Claims Authorization در Identity 2.1
- توسعه بخش front-end با AngularJs
تنظیمات اولیه ASP.NET Identity 2.1 با Web API
1. تنظیمات ASP.Net Identity 2.1
1-1. ساخت یک پروژه Web API
در ابتدا ما یک empty solution را با نام "AspNetIdentity" همانند شکل مقابل میسازیم.
یک ASP.NET Web Application با نام "AspNetIdentity.WebApi" را به این solution اضافه مینماییم. در بخش select template ما empty template را انتخاب میکنیم. همچنین در قسمت add folders and core references for: نیز هیچیک از گزینهها را انتخاب نمیکنیم.
1-2. نصب packageهای مورد نیاز از Nuget
در زیر packageهای مورد نیاز برای ASP.NET Web API و Owin را مشاهده میکنید. همچنین packageهای مربوط به ASP.Net Identity 2.1 نیز در زیر قرار داده شدهاند.
Install-Package Microsoft.AspNet.Identity.Owin -Version 2.1.0 Install-Package Microsoft.AspNet.Identity.EntityFramework -Version 2.1.0 Install-Package Microsoft.Owin.Host.SystemWeb -Version 3.0.0 Install-Package Microsoft.AspNet.WebApi.Owin -Version 5.2.2 Install-Package Microsoft.Owin.Security.OAuth -Version 3.0.0 Install-Package Microsoft.Owin.Cors -Version 3.0.0
1-3. اضافه کردن user class و database context
حال که تمامی پکیجهای مورد نیاز را به پروژه خود اضافه نمودیم، قصد داریم تا اولین کلاس EF را با نام "ApplicationUser" به پروژه اضافه کنیم. این کلاس، کاربری را که قصد ثبت نام در membership system، دارد را نمایش میدهد. برای این کار ما یک کلاس جدید را به نام "ApplicationUser" میسازیم و کلاس "Microsoft.AspNet.Identity.EntityFramework.IdentityUser" را در آن به ارث میبریم.
برای این کار ما یک پوشهی جدید را در برنامه با نام "Infrastructure" میسازیم و درون آن کلاس ذکر شده را اضافه میکنیم:
public class ApplicationUser : IdentityUser { [Required] [MaxLength(100)] public string FirstName { get; set; } [Required] [MaxLength(100)] public string LastName { get; set; } [Required] public byte Level { get; set; } [Required] public DateTime JoinDate { get; set; } }
پس از افزودن کلاس User، نوبت به اضافه نمودن Db Context است. این کلاس وظیفهی ارتباط با پایگاه داده را بر عهده دارد. ما یک کلاس جدید را با نام ApplicationDbContext، به پوشهی Infrastructure اضافه مینماییم. کد مربوط به این کلاس به صورت زیر است:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { Configuration.ProxyCreationEnabled = false; Configuration.LazyLoadingEnabled = false; } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } }
همانطور که ملاحظه میکنید این کلاس از IdentityDbContext ارث بری نموده است. این کلاس یک نسخهی جدیدتر از DbContext است که تمامی نگاشتهای Entity Framework Code First را انجام میدهد. در ادامه ما یک Connection String را با نام DefaultConnection در فایل web.config اضافه مینماییم.
همچنین متد static ایی را با نام Create که در تکه کد فوق از سوی Owin Startup class فراخوانی میگردد که در ادامه به شرح آن نیز خواهیم پرداخت.
ConnectionString قرار داده شده در فایل web.config در قسمت زیر قرار داده شده است:
<connectionStrings> <add name="DefaultConnection" connectionString="Data Source=.\sqlexpress;Initial Catalog=AspNetIdentity;Integrated Security=SSPI;" providerName="System.Data.SqlClient" /> </connectionStrings>
قدم 4: ساخت پایگاه داده و فعال سازی DB Migration
حال ما باید EF CodeFirst Migration را برای آپدیت کردن دیتابیس، بجای دوباره ساختن آن به ازای هر تغییری، فعال نماییم. برای این کار بایستی در قسمت NuGet Package Manager Console عبارت زیر را وارد نماییم:
enable-migrations add-migration InitialCreate
اگر تا به اینجای کار تمامی مراحل به درستی صورت گرفته باشند، پوشهی Migration با موفقیت ساخته میشود. همچنین پایگاه داده متشکل از جداول مورد نیاز برای سیستم Identity نیز ایجاد میشود. برای اطلاعات بیشتر میتوانید مقالات مرتبط با Code First را مطالعه نمایید. ساختار جداول ما باید به صورت زیر باشد:
در بخش بعدی، کلاسهای مربوط به UserManager را ایجاد خواهیم کرد و پس از آن فایل Startup Owin را برای مدیریت کاربران، تشریح میکنیم.
یک مثال: بررسی تنظیمات نمایش خطاهای Includeهای صرفنظر شده
موجودیتهای Blog و Postهای آنرا درنظر بگیرید:
public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } }
به همراه Context آن که به صورت ذیل تعریف شدهاست:
public class BloggingContext : DbContext { public BloggingContext() { } public BloggingContext(DbContextOptions options) : base(options) { } public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Demo.Includes;Trusted_Connection=True;"); optionsBuilder.ConfigureWarnings(warnings => warnings.Log(CoreEventId.IncludeIgnoredWarning)); optionsBuilder.UseLoggerFactory(new LoggerFactory().AddConsole((message, logLevel) => { return true; /*return logLevel == LogLevel.Debug && message.StartsWith("Microsoft.EntityFrameworkCore.Database.Command");*/ })); } } }
همچنین ذکر UseLoggerFactory، به نحوی که مشاهده میکنید، یکی دیگر از روشهای لاگ کردن خروجیهای EF است. در اینجا اگر true تنظیم شود، تمام اتفاقات مرتبط با EF Core لاگ میشوند. اگر میخواهید تنها کوئریهای SQL آنرا مشاهده کنید، روش فیلتر کردن آنها در سطر بعدی نمایش داده شدهاست.
در این حالت اگر کوئری ذیل را اجرا کنیم:
var blogs = context.Blogs .Include(blog => blog.Posts) .Select(blog => new { Id = blog.BlogId, Url = blog.Url }) .ToList();
warn: Microsoft.EntityFrameworkCore.Query[100106] The Include operation for navigation '[blog].Posts' is unnecessary and was ignored because the navigation is not reachable in the final query results. See https://go.microsoft.com/fwlink/?linkid=850303 for more information.
SELECT [blog].[BlogId] AS [Id], [blog].[Url] FROM [Blogs] AS [blog]
یک نکته: اگر تنظیم نمایش IncludeIgnoredWarning را به نحو ذیل به Throw تنظیم کنیم:
optionsBuilder.ConfigureWarnings(warnings => warnings.Throw(CoreEventId.IncludeIgnoredWarning));
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: IgnoredIncludes.zip
مدیریت پیشرفتهی حالت در React با Redux و Mobx - قسمت سوم - روش اتصال Redux به برنامههای React
نصب پیشنیازها
میتوان همانند قسمت قبل، تمام کارها را با کتابخانهی redux انجام داد و یا میتوان قسمت به روز رسانی UI آنرا و همچنین مدیریت state را به کتابخانهی ساده کنندهی دیگری به نام react-redux واگذار کرد. به همین جهت در ادامهی همان برنامهی قسمت قبل، دو کتابخانهی redux و همچنین react-redux را به همراه types آن نصب میکنیم (نصب types، سبب ارائهی intellisense بهتری در VSCode میشود؛ حتی اگر نخواهیم با TypeScript کار کنیم).
برای این منظور پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save redux react-redux > npm install --save-dev @types/react-redux
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
معرفی ساختار ابتدایی برنامه
برنامهای را که در این قسمت بررسی میکنیم، ساختار بسیار سادهای را داشته و به همراه دو دکمهی افزایش و کاهش مقدار یک شمارشگر است؛ به همراه دکمهی برای به حالت اول در آوردن آن. هدف اصلی دنبال شدهی در اینجا نیز نحوهی برپایی redux و همچنین react-redux و اتصال آنها به برنامهی React جاری است:
به همین جهت ابتدا کامپوننت جدید src\components\counter.jsx را به نحو زیر تشکیل میدهیم تا markup ابتدایی فوق را به همراه سه دکمه و یک span، برای نمایش مقدار شمارشگر، رندر کند:
import React, { Component } from "react"; class Counter extends Component { render() { return ( <section className="card mt-5"> <div className="card-body text-center"> <span className="badge m-2 badge-primary">0</span> </div> <div className="card-footer"> <div className="d-flex justify-content-center align-items-center"> <button className="btn btn-secondary btn-sm">+</button> <button className="btn btn-secondary btn-sm m-2">-</button> <button className="btn btn-danger btn-sm">Reset</button> </div> </div> </section> ); } } export default Counter;
import "./App.css"; import React from "react"; import Counter from "./components/counter"; function App() { return ( <main className="container"> <Counter /> </main> ); } export default App;
پوشه بندی مخصوص برنامههای مبتنی بر Redux
هدف ما در ادامه ایجاد یک store مخصوص redux است و سپس اتصال آن به کامپوننت شمارشگر برنامه. به همین جهت نیاز به 4 پوشهی جدید، برای مدیریت بهتر برنامه خواهیم داشت:
- پوشه constants: برای اینکه نام رشتهای نوع اکشنهای مختلف را بتوانیم در قسمتهای مختلف برنامه استفاده کنیم، بهتر است فایل جدید src\actions\index.js را ایجاد کرده و این ثوابت را داخل آن export کنیم.
- پوشهی actions: در فایل جدید src\actions\index.js، تمام متدهای ایجاد کنندهی شیء خاص action، که در قسمت قبل در مورد آن بحث شد، قرار میگیرند. نمونهی آن، متد createAddAction قسمت قبل است.
- پوشهی reducers: تمام توابع reducer برنامه را در فایلهای مجزایی در پوشهی reducers قرار میدهیم. سپس در فایل src\reducers\index.js با استفاده از متد combineReducer آنها را یکی کرده و به متد createStore ارسال میکنیم.
- پوشهی containers: این پوشه جائی است که کار فراخوانی متد connect کتابخانهی react-redux به ازای هر کامپوننت استفاده کنندهی از redux store، صورت میگیرد.
ایجاد نام نوع اکشن متناظر با دکمهی افزودن مقدار
میخواهیم با کلیک بر روی دکمهی +، مقدار شمارشگر افزایش یابد. به همین جهت نیاز به یک نام وجود دارد تا در تابع Reducer متناظر و قسمتهای دیگر برنامه، بتوان بر اساس آن، این اکشن خاص را شناسایی کرد و سپس عکس العمل نشان داد. به همین جهت فایل جدید src\constants\ActionTypes.js را ایجاد کرده و به صورت زیر تکمیل میکنیم:
export const Increment = "Increment";
ایجاد متد Action Creator
در قسمت قبل مشاهده کردیم که شیء ارسالی به یک reducer از طریق dispatch یک action خاص، دارای فرمت ویژهی زیر است:
{ type: "ADD", payload: { amount // = amount: amount }, meta: {} }
import * as types from "../constants/ActionTypes"; export const incrementValue = () => ({ type: types.Increment });
ایجاد تابع reducer مخصوص افزودن مقدار
ابتدا فایل جدید src\reducers\counter.js را با محتوای زیر ایجاد میکنیم:
import * as types from "../constants/ActionTypes"; const initialState = { count: 0 }; export default function counterReducer(state = initialState, action) { if (action.type === types.Increment) { return { count: state.count + 1 }; } return state; }
- سپس میخواهیم رویداد کلیک بر روی دکمه + را مدیریت کنیم. به همین جهت نیاز به یک اکشن جدید به نام Increment داریم که توسط مقدار ثابت رشتهای types.Increment، از فایل مجزای src\constants\ActionTypes.js، تامین میشود.
- پس از مشخص کردن نوع action ای که قرار است مدیریت شود و همچنین ایجاد متدی برای تولید شیء حاوی اطلاعات آن که در فایل src\actions\index.js قرار دارد، اکنون میتوان متد reducer را که state و action را دریافت میکند و سپس state جدیدی را بر اساس action.type دریافتی و در صورت نیاز بازگشت میدهد، ایجاد کرد. این متد بررسی میکند که آیا action.type رسیده همان ثابت Increment است؟ اگر بله، بجای تغییر مستقیم state.count، یک شیء جدید را بازگشت میدهد. البته روش صحیحتر اینکار را در قسمت اول این سری با معرفی روشهایی برای کپی اشیاء و آرایهها، بررسی کردیم. در اینجا جهت سادگی بیشتر، یک شیء کاملا جدید را دستی ایجاد میکنیم. در آخر اگر action.type رسیده قابل پردازش نبود، همان state ابتدایی دریافتی را بازگشت میدهیم تا در صورت وجود چندین reducer تعریف شدهی در سیستم، زنجیرهی آنها قابل پردازش باشد. این مورد را در قسمت قبل، ذیل عنوان «بررسی تابع combineReducers با یک مثال» بیشتر بررسی کردیم.
پس از ایجاد reducer اختصاصی عمل افزودن مقدار شمارشگر، فایل جدید src\reducers\index.js را نیز با محتوای زیر ایجاد میکنیم:
import { combineReducers } from "redux"; import counterReducer from "./counter"; const rootReducer = combineReducers({ counterReducer }); export default rootReducer;
ایجاد store مخصوص Redux
تا اینجا رسیدیم به یک rootReducer متشکل از تمام reducerهای سفارشی برنامه. اکنون بر اساس آن در فایل src\index.js، یک store جدید را ایجاد میکنیم:
import { createStore } from "redux"; import reducer from "./reducers"; //... const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); //...
نکته 2: در اینجا روش فعالسازی افزونهی redux-devtools را نیز ملاحظه میکنید. ابتدا بررسی میشود که آیا متد ویژهی فراخوانی این افزونه وجود دارد یا خیر؟ اگر بله، فراخوانی میشود. بدون این پارامتر دوم، افزونهی redex dev tools، هیچ خروجی را نمایش نخواهد داد.
اتصال React به Redux
کتابخانهی react-redux تنها به همراه دو شیء مهم connect و Provider است. شیء Provider آن شبیه به Context API خود React است و هدف آن، ارسال ارجاعی از store ایجاد شده، به برنامهی React است. پس از ایجاد store در فایل src\index.js، اکنون نوبت به اتصال آن به برنامهی React ای جاری است. به همین جهت در بالاترین سطح برنامه، ابتدا شیء کامپوننت App را با شیء Provider محصور میکنیم:
import { Provider } from "react-redux"; import { createStore } from "redux"; import reducer from "./reducers"; // ... const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
تامین state کامپوننت شمارشگر از طریق props
همانطور که عنوان شد، کامپوننت Counter به همراه state نیست و ما قصد نداریم در آن از state خود React استفاده کنیم؛ البته فلسفهی آنرا در قسمت اول این سری بررسی کردیم و همچنین اگر کامپوننتی نیاز به اشتراک گذاری اطلاعات خودش را با لایههای زیرین یا بالاتر از خود ندارد، شاید اصلا نیازی به Redux نداشته باشد و همان state استاندارد React برای آن کافی است. بنابراین میتوان برنامهای را داشت که ترکیبی از state استاندارد React، در کامپوننتهای متکی به خود و Redux، در کامپوننتهایی که باید اطلاعاتی را با هم به اشتراک بگذارند، باشد. برای مثال، کامپوننت مثال جاری، واقعا نیازی را به Redux، برای مدیریت حالت خود، ندارد؛ هدف ما در اینجا بررسی نحوهی برقراری ارتباطات یک سیستم مبتنی بر Redux، در برنامههای React است.
بنابراین در اینجا و کامپوننتی که قرار است از Redux برای مدیریت حالت خود استفاده کند، هر اطلاعاتی که به آن از طریق react-redux store وارد میشود، از طریق props به آن ارسال خواهد شد. برای مثال در اینجا مقدار count، از طریق props خوانده میشود و همچنین امکان ارسال action ای خاص به متد reducer تعریف شده نیز باید تعریف شود. بنابراین در ادامه نیاز داریم تا یک کامپوننت React را به redux store متصل کنیم. برای این منظور فایل جدید src\containers\Counter.js را با محتوای زیر ایجاد میکنیم:
import { connect } from "react-redux"; import { incrementValue } from "../actions"; import Counter from "../components/counter"; const mapStateToProps = state => { return state; }; const mapDispatchToProps = dispatch => { return { increment() { dispatch(incrementValue()); } }; }; export default connect(mapStateToProps, mapDispatchToProps)(Counter);
زمانیکه در مورد store در redux صحبت میشود، داخل آن یک شیء بزرگ state قرار گرفتهاست که حاوی کل state برنامهاست. اما شاید هر کامپوننت به تمام آن نیازی نداشته باشد. برای مثال شاید کامپوننت شمارشگر، اهمیتی به اطلاعات خطاهای سیستم و یا کاربر وارد شدهی به سیستم که در شیء کلی state موجود در store وجود دارند، ندهد. به همین جهت متد mapStateToProps، کل state برنامه را دریافت کرده و به ما اجازه میدهد تا تنها اطلاعاتی را که از آن نیاز داریم، به صورت props دریافت کنیم. به این ترتیب از رندر مجدد این کامپوننت نیز جلوگیری خواهد شد؛ چون این کامپوننت دیگر وابستهی به تغییرات سایر اجزای کل state برنامه، نخواهد بود و اگر آنها تغییر کردند، این کامپوننت رندر مجدد نخواهد شد.
بنابراین میتوان متد mapStateToProps را به صورت کلی زیر نیز تعریف کرد:
const mapStateToProps = (state) => { return state };
یک نکته: اگر کامپوننتی نیاز به تامین state خود را از طریق props نداشت و فقط کارش صدور رخدادها است، میتوان پارامتر اول متد connect را نال وارد کرد.
پارامتر dispatch متد mapDispatchToProps، به متد store.dispatch اشاره میکند. بنابراین توسط آن امکان ارسال actions را میسر کرده و میتوان state را توسط reducerهای تعریف شده، تغییر داد که در نتیجهی آن props جدیدی به کامپوننت منتقل میشوند. این تابع نیز یک شیء را باز میگرداند. این شیء را فعلا با یک متد دلخواه مقدار دهی میکنیم که توسط پارامتر dispatch رسیدهی به آن، متد action creator تعریف شدهی در فایل src\actions\index.js را به نام incrementValue، فراخوانی میکند؛ دقیقا عملی شبیه به فراخوانی store.dispatch(createAddAction(2)) در قسمت قبل که از آن برای ارسال یک اکشن، به reducer متناظری استفاده شد.
یک نکته: اگر کامپوننتی کار صدور رخدادها را انجام نمیدهد، میتوان پارامتر دوم متد connect را بطور کامل حذف کرد و قید نکرد.
استفاده از کامپوننت جدید خروجی متد connect، جهت تامین props کامپوننت شمارشگر
در انتهای فایل src\components\counter.jsx، چنین سطری درج شدهاست:
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
import "./App.css"; import React from "react"; import CounterContainer from "./containers/Counter"; function App() { return ( <main className="container"> <CounterContainer /> </main> ); } export default App;
اکنون کامپوننت شمارشگر src\components\counter.jsx، دو شیء را از طریق props دریافت میکند؛ یکی کل state است که خاصیت count داخل آن قرار دارد و از طریق mapStateToProps تامین میشود. دیگری متد increment ای است که در متد mapDispatchToProps تعریف کردیم و کار صدور رخدادی را به reducer متناظر، انجام میدهد. به همین جهت تغییرات ذیل را در کامپوننت Counter اعمال میکنیم:
import React, { Component } from "react"; class Counter extends Component { render() { console.log("props", this.props); const { counterReducer: { count }, increment } = this.props; return ( <section className="card mt-5"> <div className="card-body text-center"> <span className="badge m-2 badge-primary">{count}</span> </div> <div className="card-footer"> <div className="d-flex justify-content-center align-items-center"> <button className="btn btn-secondary btn-sm" onClick={increment}> + </button> <button className="btn btn-secondary btn-sm m-2">-</button> <button className="btn btn-danger btn-sm">Reset</button> </div> </div> </section> ); } } export default Counter;
به همین جهت، خاصیت تو در توی this.props.counterReducer.count و همچنین اشارهگر به متد increment، توسط Object Destructuring به صورت زیر از this.props دریافتی، تجزیه شدهاند:
const { counterReducer: { count }, increment } = this.props;
جزئیات کار با Redux store را نیز میتوان در افزونهی redux dev tools مشاهده کرد:
این افزونه در نوار ابزار پایین آن، امکان export کل state و سپس import و بازیابی آنرا نیز به همراه دارد.
دریافت props از طریق کامپوننت دربرگیرنده و ارسال آن به کامپوننت اصلی
فرض کنید نیاز باشد تا اطلاعاتی را به صورت متداول React از طریق props، به کامپوننت دربرگیرندهی کامپوننت شمارشگر ارسال کرد:
function App() { const prop1 = 123 return ( <main className="container"> <CounterContainer prop1={prop1} /> </main> ); }
const mapStateToProps = (state, ownProps) => { console.log("mapStateToProps", { state, ownProps }); return state; };
پیاده سازی دکمهی کاهش مقدار شمارشگر
پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux، پیاده سازی دکمهی کاهش مقدار شمارشگر بسیار سادهاست و شامل مراحل زیر است:
1) ایجاد نام نوع اکشن متناظر با دکمهی کاهش مقدار
به فایل src\constants\ActionTypes.js، نوع جدید کاهشی را اضافه میکنیم:
export const Decrement = "Decrement";
در فایل src\actions\index.js، متد ایجاد کنندهی شیء اکشن ارسالی به reducer متناظری را تعریف میکنیم تا بتوان بر اساس نوع آن در reducer کاهشی، منطق کاهش را پیاده سازی کرد:
export const decrementValue = () => ({ type: types.Decrement });
اکنون در فایل src\reducers\counter.js، بر اساس نوع شیء رسیده، تصمیم به کاهش یا افزایش مقدار موجود در state گرفته میشود:
export default function counterReducer(state = initialState, action) { // ... if (action.type === types.Decrement) { return { count: state.count - 1 }; } return state; }
در ادامه نیاز است بتوان اکشن کاهش را به این reducer ارسال کرد. به همین جهت به کامپوننت دربرگیرندهی کامپوننت شمارشگر در فایل src\containers\Counter.js مراجعه کرده و به شیء خروجی متد mapDispatchToProps، متد کاهش را اضافه میکنیم:
import { decrementValue, incrementValue } from "../actions"; // ... const mapDispatchToProps = dispatch => { return { // ... decrement() { dispatch(decrementValue()); } }; };
در آخر به فایل src\components\counter.jsx مراجعه کرده و اشارهگر به متد decrement را از طریق this.props دریافت میکنیم:
const { // ... decrement } = this.props;
<button className="btn btn-secondary btn-sm m-2" onClick={decrement} > - </button>
به عنوان تمرین، پیاده سازی دکمهی Reset را نیز انجام دهید که جزئیات آن بسیار شبیه به دو مثال قبلی افزودن و کاهش مقدار شمارشگر است.
بهبود کیفیت کدهای کامپوننت دربرگیرندهی کامپوننت Counter
متد mapDispatchToProps فایل src\containers\Counter.js اکنون چنین شکلی را پیدا کردهاست:
const mapDispatchToProps = dispatch => { return { increment() { dispatch(incrementValue()); }, decrement() { dispatch(decrementValue()); } }; };
import { bindActionCreators } from "redux"; // ... const mapDispatchToProps = dispatch => { return bindActionCreators( { incrementValue, decrementValue }, dispatch ); };
به همین جهت نیاز است در متد رندر کامپوننت src\components\counter.jsx، نامهایی را که به متدهای action creator اشاره میکنند، به صورت زیر تغییر داد:
const { counterReducer: { count }, incrementValue, decrementValue } = this.props;
روش دوم: در نگارشهای اخیر react-redux میتوان متد mapDispatchToProps را به صورت زیر نیز خلاصه و تعریف کرد که بسیار سادهتر است:
const mapDispatchToProps = { incrementValue, decrementValue };
همچنین بجای بازگشت کل state در متد mapStateToProps، میتوان تنها خواص مدنظر را بازگشت داد:
const mapStateToProps = state => { //return state; return { count: state.counterReducer.count }; };
بنابراین باید در متد رندر کامپوننت شمارشگر، خاصیت count را به صورت معمولی دریافت کرد:
const { //counterReducer: { count }, count, incrementValue, decrementValue } = this.props;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: state-management-redux-mobx-part03.zip
اگر SQL Server و MySQL بر روی سیستم شما نصب است، روشی ساده برای انتقال اطلاعات بین این دو وجود دارد که نیازی به دخالت هیچ نوع برنامهی جانبی نداشته و با امکانات موجود قابل مدیریت است.
ایجاد یک Linked server
برای اینکه SQL Server را به MySQL متصل کنیم میتوان بین این دو یک Linked server تعریف کرد و سپس دسترسی به بانکهای اطلاعاتی MySQL همانند یک بانک اطلاعاتی محلی SQL Server خواهد شد که شرح آن در ادامه ذکر میشود.
ابتدا نیاز است تا درایور ODBC مربوط به MySQL دریافت و نصب شود. آنرا میتوانید از اینجا دریافت کنید : (+)
سپس management studio را گشوده و در قسمت Server objects ، بر روی گزینهی Linked servers کلیک راست نمائید. از منوی ظاهر شده، گزینهی New linked server را انتخاب کنید:
در ادامه، باید تنظیمات زیر را در صفحهی باز شده وارد کرد:
در قسمت Linked server و Product name ، نام دلخواهی را وارد کنید.
Provider انتخابی باید از نوع Microsoft OLE DB Provider for ODBC Drivers باشد.
مهمترین تنظیم آن، قسمت Provider string است که باید به صورت زیر وارد شود (در غیر اینصورت کار نمیکند):
DRIVER={MySQL ODBC 5.1 Driver}; SERVER=localhost; DATABASE=testdb; USER=root; PASSWORD=mypass; OPTION=3;PORT=3306; CharSet=UTF8;
پس از انجام این تنظیمات بر روی دکمهی Ok کلیک کنید تا Linked server ساخته شود:
اگر لیست بانکهای اطلاعاتی را مشاهده نمودید، یعنی اتصال به درستی برقرار شده است.
تنظیمات ثانویه:
تا اینجا اس کیوال سرور به MySQL متصل شده است، اما برای استفاده بهینه از امکانات موجود نیاز است تا یک سری تغییرات دیگر را هم اعمال کرد.
تنظیم MSDASQL Provider :
در همان قسمت Linked provider ، ذیل قسمت Providers ، گزینهی MSDASQL را انتخاب کرده و بر روی آن کلیک راست نمائید. سپس صفحهی خواص آنرا انتخاب کنید تا بتوان تنظیمات زیر را به آن اعمال کرد. این پروایدر جهت اتصال به MySQL مورد استفاده قرار میگیرد.
فعال سازی RPC :
برای اینکه بتوان از طریق SQL Server رکوردی را در یکی از جداول بانکهای اطلاعاتی MySQL متصل شده ثبت نمود، میتوان از دستور زیر استفاده کرد:
EXECUTE('insert into testdb.testtable(f1,f1) values(1,''data'')') at mysql
اینجا testdb نام بانک اطلاعاتی اتصالی MySQL است و testTable هم نام جدول مورد نظر. MySQL ایی که در آخر عبارت ذکر شده همان نام linked server ایی است که پیشتر تعریف کردیم.
به محض سعی در اجرای این کوئری خطای زیر ظاهر میشود:
Server 'mysql' is not configured for RPC.
برای رفع این مشکل، مجددا به صفحهی خواص همان liked server ایجاد شده مراجعه کنید. در قسمت Server options دو گزینه مرتبط به RPC باید فعال شوند:
و اکنون برای کوئری گرفتن از اطلاعات ثبت شده هم از عبارت زیر میتوان استفاده کرد:
SELECT * FROM OPENQUERY(mysql, 'SELECT * FROM testdb.testtable')
در این کوئری، MySQL نام Linked server ثبت شده است و testdb هم یکی از بانکهای اطلاعاتی MySQL مورد نظر.
انتقال تمام اطلاعات یک جدول از بانک اطلاعاتی MySQL به SQL Server
پس از برقراری اتصال، اکنون import کامل یک جدول MySQL به SQL Server به سادگی اجرای کوئری زیر میباشد:
SELECT * INTO MyDb.dbo.testtable FROM openquery(MYSQL, 'SELECT * FROM testdb.testtable')
در این کوئری، MySQL همان Linked server تعریف شده است. MyDB نام بانک اطلاعاتی موجود در SQL Server جاری است و testtable هم جدولی است که قرار است اطلاعات testdb.testtable بانک اطلاعاتی MySQL به آن وارد شود.
با اطلاعات فارسی هم (در سمت SQL Server) مشکلی ندارد. همانطور که مشخص است، در اطلاعات provider string ذکر شده، مقدار charset به utf8 تنظیم شده و همچنین اگر نوع collation فیلدهای تعریف شده در MySQL نیز به utf8_persian_ci تنظیم شده باشد، با مشکل ثبت اطلاعات فارسی به صورت ???? مواجه نخواهید شد.
نکته:
اگر بانک اطلاعاتی MySQL شما بر روی local host نصب نیست، جهت فعال سازی دسترسی ریموت به آن، میتوان به یکی از نکات زیر مراجعه کرد و سپس این اطلاعات جدید باید در همان قسمت provider string مرتبط با تعریف linked server وارد شوند:
مطالب مشابه:
هیچکدام از این روشها قابل استفاده نبودند چون provider string صحیحی را نهایتا تولید نمیکنند. همچنین تمام این روشها مبتنی است بر ایجاد DSN در کنترل پنل که اصلا نیازی به آن نیست و اضافی است.
ILogger: is responsible to write a log message of a given Log Level.
ILoggerProvider: is responsible to create an instance of ILogger
(you are not supposed to use ILoggerProvider
directly to create a logger)
ILoggerFactory: you can register one or more ILoggerProvider
s with the factory, which in turn uses all of them to create an instance of ILogger
. ILoggerFactory
holds a collection of ILoggerProviders
.
- آیا اطلاعات، یک بار نوشته خواهند شد و به کرّات واکشی میشوند؟ (مانند پرفایل کاربران یا اطلاعات یک کالا در یک فروشگاه اینترنتی)
Cache-Aside
میتوان این روش را یکی از متداولترین و یا آشناترین روشهای caching دانست و شاید حداقل یک بار، کارکردن با آن را تجربه کردهایم.
نحوهی کار به این صورت میباشد که:
1- برنامه ابتدا cache را بررسی میکند میکند و اگر اطلاعات مورد نظر در cache یافت شود، اطلاعات به کاربر برگشت داده میشوند.
2- اگر اطلاعات مورد نظر در cache یافت نشود، برنامه همان درخواست را به دیتابیس میفرستد و اطلاعات را به کاربر برمیگرداند؛ همچنین موظف است اطلاعات دریافتی از دیتابیس را در cache ذخیره کند تا در دفعات بعدی آنرا از cache، واکشی کند.
public object GetMyEntity(int key) { // Try to get the entity from the cache. var value = cache.StringGet(key); if (value == null) // Cache miss { // If there's a cache miss, get the entity from the original store and cache it. value = db.StringGet(key); // Avoid caching a null value. if (value != null) { // Put the item in the cache with a custom expiration time that cache.StringSetAsync(key, JsonConvert.SerializeObject(value)); } } return value; }
Read-Through Cache
در این حالت دیتابیس و cache به صورت پشت سر هم (in-line) قرار دارند و نحوه کار به این صورت است که همیشه درخواستها در ابتدا به cache ارسال میشوند. تنها تفاوت این روش با روش قبل این است که برنامهی ما همیشه با cache صحبت میکند.
مزایا و معایب
Write-Through Cache
در این حالت اطلاعات ابتدا در cache ذخیره خواهند شد و بعد از آن در دیتابیس قرار خواهند گرفت. همچنین همانند روش Read-Through، برنامه همیشه با cache صحبت میکند.
این روش همهی مزایای روش Read-Through را دارد به علاوه رفع معایب آن از جمله:
- ناسازگاری دادهها نمیتوانند اتفاق بیفتند؛ زیرا اطلاعات همیشه ابتدا در cache نوشته خواهند شد و بعد در دیتابیس؛ به همین خاطر اطلاعات در هر دو نسخه یکسان هستند.
- بعد از درج اطلاعات جدید، نیازی به warming نیست. به این دلیل که در ابتدا در cache ذخیره خواهند شد.