- «تنظیم رشته اتصالی Entity Framework به بانک اطلاعاتی به وسیله کد»
- «استفاده از چندین بانک اطلاعاتی به صورت همزمان در EF Code First»
اما EF Core نه تنها این مشکل را پوشش را دادهاست، بلکه امکان تزریق وابستگیها و استفادهی از سرویسهای مختلف را نیز در این حین، پیش بینی کردهاست که در ادامه جزئیات آنرا مرور میکنیم.
نیاز به تغییر رشتهی اتصالی به بانک اطلاعاتی در زمان اجرا
دلایل نیاز به امکان تغییر رشتهی اتصالی در زمان اجرا شامل موارد زیر هستند:
- در برنامههایی کمی پیچیدهتر و سابقه دار، ممکن است عملیات تجاری یکسال را در بانک اطلاعاتی سال 98 و دیگری را در بانک اطلاعاتی سال 99 ثبت کنید. در این حالت کاربران باید بتوانند در زمان اجرا به هر بانک اطلاعاتی که پیشتر با آن کار کردهاند، متصل شده و از آن استفاده کنند.
- یکی از روشهای پیاده سازی برنامههای چند مستاجری، داشتن یک بانک اطلاعاتی مجزا، به ازای هر مستاجر است. در این حالت نیز تک برنامهی ما باید بتواند بر اساس Id مشتری، بانک اطلاعاتی متناظری را در زمان اجرا انتخاب کند.
- نیاز به داشتن چندین context در برنامه و کار با بانکهای اطلاعاتی متفاوت در زمان اجرا؛ مانند کار با SQL Server، اوراکل و یا SQLite
روش تغییر رشتهی اتصالی به بانک اطلاعاتی در EF Core در زمان اجرای برنامه
اگر به روش ثبت متداول سرویس DbContext برنامه و پروایدر آن دقت کنیم:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection") ));
یک نکته: چون این اطلاعات کش نمیشوند، اگر رشتهی اتصالی شما ثابت است (و نیازی به تغییر آن در زمان اجرای برنامه نیست)، محل تامین آنرا به پیش از سطر services.AddDbContext انتقال دهید و فقط نتیجهی محاسبه شدهی نهایی را استفاده کنید تا کارآیی برنامه افزایش یابد؛ در غیراینصورت فراخوانی Configuration.GetConnectionString مدام تکرار خواهد شد.
دریافت یک قالب قابل تغییر از تنظیمات برنامه و تغییر آن با هدرهای درخواست رسیدهی به آن
فرض کنید قالب رشتهی اتصالی برنامه در فایل appsettings.json به صورت زیر است:
"ConnectionStrings": { "ConnectionTemplate": "Data Source=.;Initial Catalog={db_Name};Integrated Security=True", }
بنابراین اکنون نیاز است به ازای هر درخواست رسیده بتوان به سرویس IHttpContextAccessor و شیء HttpContext.Request جاری دسترسی یافت و سپس از هدرهای رسیده، برای مثال هدر ویژهی tenantId و یا year را پردازش کرد؛ اما در تعریف services.AddDbContext فوق چگونه میتوان اینکار را انجام داد؟
خوشبختانه متد services.AddDbContext، دارای یک overload دیگر نیز هست که امکان دسترسی به تمام سرویسهای جاری سیستم را میسر میکند:
services.AddDbContext<ApplicationDbContext>((serviceProvider, dbContextBuilder) => { var connectionStringTemplate = Configuration.GetConnectionString("ConnectionTemplate"); var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>(); var dbName = httpContextAccessor.HttpContext.Request.Headers["tenantId"].First(); var connectionString = connectionStringTemplate.Replace("{db_Name}", dbName); dbContextBuilder.UseSqlServer(connectionString); });
همچنین در صورت نیاز میتوان UseSqlServer آنرا نیز در این action delegate به هر پروایدر دیگری در زمان اجرا تغییر داد و از این لحاظ محدودیتی وجود ندارد.
یک نکته: البته برنامه نباید هر tenantId ای را پردازش کند و این خودش میتواند تبدیل به یک نقیصهی امنیتی شود. به همین جهت برای مثال میتوان tenantId را در یک JWT قرار داد و در حین تعیین اعتبار آن و کاربر جاری، این مقدار را نیز بررسی کرد.
public class CustomerInfo { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } }
public interface IModelBinder { object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext); }
using System; using System.Web; using System.Web.Mvc; using ModelBinderExample.Models; using Persia; namespace ModelBinderExample.CustomModelBinder { // Article written for www.dotnettips.info public class CustomerInfoModelBinder : IModelBinder { public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { HttpRequestBase request = controllerContext.HttpContext.Request; string firstName = request.Form.Get("FirstName"); string lastName = request.Form.Get("LastName"); DateTime birthDate = this.GetMiladiDate(request); return new CustomerInfo() { FirstName = firstName, LastName = lastName, BirthDate = birthDate }; } private DateTime GetMiladiDate(HttpRequestBase request) { int day = int.Parse(request.Form.Get("Day")); int month = int.Parse(request.Form.Get("Month")); int years = int.Parse(request.Form.Get("Years")); //Convert shamsi to miladi return Persia.Calendar.ConvertToGregorian(years, month, day, DateType.Gerigorian); } } }
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); //Register New ModelBinder ModelBinders.Binders.Add(typeof(CustomerInfo), new CustomerInfoModelBinder()); }
[HttpPost] public ActionResult Create([ModelBinder(typeof (CustomerInfoModelBinder))] CustomerInfo customerInfo) { if (ModelState.IsValid) { ViewBag.FirstName = customerInfo.FirstName; ViewBag.LastName = customerInfo.LastName; ViewBag.BirthDate = customerInfo.BirthDate; } return View(); }
CSRF یا Cross Site Request Forgery به صورت خلاصه به این معنا است که شخص مهاجم اعمالی را توسط شما و با سطح دسترسی شما بر روی سایت انجام دهد و اطلاعات مورد نظر خود را استخراج کرده (محتویات کوکی یا سشن و امثال آن) و به هر سایتی که تمایل دارد ارسال کند. اینکار عموما با تزریق کد در صفحه صورت میگیرد. مثلا ارسال تصویری پویا به شکل زیر در یک صفحه فوروم، بلاگ یا ایمیل:
شخصی که این صفحه را مشاهده میکند، متوجه وجود هیچگونه مشکلی نخواهد شد و مرورگر حداکثر جای خالی تصویر را به او نمایش میدهد. اما کدی با سطح دسترسی شخص بازدید کننده بر روی سایت اجرا خواهد شد.
روشهای مقابله:
- هر زمانیکه کار شما با یک سایت حساس به پایان رسید، log off کنید. به این صورت بجای منتظر شدن جهت به پایان رسیدن خودکار طول سشن، سشن را زودتر خاتمه دادهاید یا برنامه نویسها نیز باید طول مدت مجاز سشن در برنامههای حساس را کاهش دهند. شاید بپرسید این مورد چه اهمیتی دارد؟ مرورگری که امکان اجازهی بازکردن چندین سایت با هم را به شما در tab های مختلف میدهد، ممکن است سشن یک سایت را در برگهای دیگر به سایت مهاجم ارسال کند. بنابراین زمانیکه به یک سایت حساس لاگین کردهاید، سایتهای دیگر را مرور نکنید. البته مرورگرهای جدید مقاوم به این مسایل شدهاند ولی جانب احتیاط را باید رعایت کرد.
- در برنامه خود قسمت Referrer header را بررسی کنید. آیا متد POST رسیده، از سایت شما صادر شده است یا اینکه صفحهای دیگر در سایتی دیگر جعل شده و به برنامه شما ارسال شده است؟ هر چند این روش آنچنان قوی نیست و فایروالهای جدید یا حتی بعضی از مرورگرها با افزونههایی ویژه، امکان عدم ارسال این قسمت از header درخواست را میسر میسازند.
- برنامه نویسها نباید مقادیر حساس را از طریق GET requests ارسال کنند. استفاده از روش POST نیز به تنهایی کارآمد نیست و آنرا باید با random tokens ترکیب کرد تا امکان جعل درخواست منتفی شود. برای مثال استفاده از ViewStateUserKey در ASP.Net . جهت خودکار سازی اعمال این موارد در ASP.Net، اخیرا HTTP ماژول زیر ارائه شده است:
تنها کافی است که فایل dll آن در دایرکتوری bin پروژه شما قرار گیرد و در وب کانفیگ برنامه ارجاعی به این ماژول را لحاظ نمائید.
کاری که این نوع ماژولها انجام میدهند افزودن نشانههایی اتفاقی ( random tokens ) به صفحه است که مرورگر آنها را بخاطر نمیسپارد و این token به ازای هر سشن و صفحه منحصر بفرد خواهد بود.
برای PHP نیز چنین تلاشهایی صورت گرفته است:
http://csrf.htmlpurifier.org/
مراجعی برای مطالعه بیشتر
Prevent Cross-Site Request Forgery (CSRF) using ASP.NET MVC’s AntiForgeryToken() helper
Cross-site request forgery
Top 10 2007-Cross Site Request Forgery
CSRF - An underestimated attack method
یک response از سه قسمت هدر، status code و بدنهی آن تشکیل میشود. بنابراین هر سه قسمت را باید آزمایش کرد تا بتوان توسط آن عملکرد یک Web API را بررسی نمود.
برای مثال فرض کنید که میخواهید برای متد Put، آزمایش بنویسید. این آزمایش باید شامل موارد زیر باشد:
بررسی موفقیت آمیز بودن عملیات
- آیا هدرهای درستی در response درج شدهاند؟
- آیا status code دریافتی از سرور برای مثال 200 یا 204 است؟
- آیا عملیات به روز رسانی، موجودیت مشخص و مورد انتظاری را به روز رسانی کردهاست یا خیر؟
- آیا تمام فیلدها به درستی به روز رسانی شدهاند؟
- آیا فیلدهایی که در درخواست ارسالی قید نشدهاند، با مقدار پیشفرض خود مقدار دهی شدهاند؟
بررسی شکست عملیات
- آیا به روز رسانی موجودیتی که وجود ندارد، خروجی 404 را به همراه دارد یا خیر؟
- آیا اگر هدر content-type ذکر نشود، شاهد status code=415 unsupported media type خواهیم بود؟
- آیا اگر هدر content-type نامربوطی ذکر شود، شاهد status code=415 unsupported media type خواهیم بود؟
- آیا اگر بدنهی درخواست خالی را ارسال کنیم، خروجی 400 bad request صادر میشود؟
- آیا اگر فیلدها را طوری تنظیم کنیم که سبب مشکلات اعتبارسنجی شوند، خروجی 422 unprocessable entity صادر میشود؟
بنابراین در حالت کلی:
- هر دو وضعیت موفقیت آمیز بودن و شکست عملیات باید بررسی شوند.
- مشکلات اعتبارسنجی ورودیها باید بررسی شوند.
- ورودیهای مورد انتظار را کم و زیاد کنید. اگر درخواستی قرار است کوئری استرینگی را بپذیرد، آنرا قید نکنید یا برعکس.
احراز هویت و اعتبارسنجی کاربران در برنامههای Angular - قسمت چهارم - به روز رسانی خودکار توکنها
نگاهی به محتوای JSON Web Token تولیدی
اگر مطلب قسمت قبل را پیگیری کرده باشید، پس از لاگین، یک چنین خروجی را در کنسول توسعه دهندگان مرورگر میتوان مشاهده کرد که همان return Ok(new { access_token = jwt }) دریافتی از سمت سرور است:
اکنون این رشتهی طولانی را در حافظه کپی کرده و سپس به سایت https://jwt.io/#debugger-io مراجعه و در قسمت دیباگر آن، این رشتهی طولانی را paste میکنیم تا آنرا decode کند:
برای نمونه payload آن حاوی یک چنین اطلاعاتی است:
{ "jti": "b2921057-32a4-fbb2-0c18-5889c1ab8e70", "iss": "https://localhost:5001/", "iat": 1576402824, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid N.", "DisplayName": "Vahid N.", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", "nbf": 1576402824, "exp": 1576402944, "aud": "Any" }
استخراج اطلاعات کاربر وارد شدهی به سیستم، از JSON Web Token دریافتی
همانطور که در payload توکن دریافتی از سرور نیز مشخص است، اطلاعات ارزشمندی از کاربر، به همراه آن ارائه شدهاند و مزیت کار با آن، عدم نیاز به کوئری گرفتن مداوم از سرور و بانک اطلاعاتی، جهت دریافت مجدد این اطلاعات است. بنابراین اکنون در برنامهی React خود، قصد داریم مشابه کاری را که سایت jwt.io انجام میدهد، پیاده سازی کرده و به این اطلاعات دسترسی پیدا کنیم و برای مثال DisplayName را در Navbar نمایش دهیم. برای این منظور فایل app.js را گشوده و تغییرات زیر را به آن اعمال میکنیم:
- میخواهیم اطلاعات کاربر جاری را در state کامپوننت مرکزی App قرار دهیم. سپس زمانیکه کار رندر کامپوننت NavBar درج شدهی در متد رندر آن فرا میرسد، میتوان این اطلاعات کاربر را به صورت props به آن ارسال کرد؛ و یا به هر کامپوننت دیگری در component tree برنامه.
- بنابراین ابتدا کامپوننت تابعی بدون حالت App را تبدیل به یک کلاس کامپوننت استاندارد مشتق شدهی از کلاس پایهی Component میکنیم. اکنون میتوان state را نیز به آن اضافه کرد:
class App extends Component { state = {};
- برای decode کردن توکن، نیاز به نصب کتابخانهی زیر را داریم:
> npm install --save jwt-decode
import jwtDecode from "jwt-decode"; // ... class App extends Component { state = {}; componentDidMount() { try { const jwt = localStorage.getItem("token"); const currentUser = jwtDecode(jwt); console.log("currentUser", currentUser); this.setState({ currentUser }); } catch (ex) { console.log(ex); } }
- اکنون میتوان شیء currentUser را به صورت props، به کامپوننت NavBar ارسال کرد:
render() { return ( <React.Fragment> <ToastContainer /> <NavBar user={this.state.currentUser} /> <main className="container">
نمایش اطلاعات کاربر وارد شدهی به سیستم در NavBar
پس از ارسال شیء کاربر به صورت props به کامپوننت src\components\navBar.jsx، کدهای این کامپوننت را به صورت زیر جهت نمایش نام کاربر جاری وارد شدهی به سیستم تغییر میدهیم:
const NavBar = ({ user }) => {
سپس میتوان لینکهای Login و Register را به صورت شرطی رندر کرد و نمایش داد:
{!user && ( <React.Fragment> <NavLink className="nav-item nav-link" to="/login"> Login </NavLink> <NavLink className="nav-item nav-link" to="/register"> Register </NavLink> </React.Fragment> )}
شبیه به همین حالت را برای هنگامیکه کاربر، تعریف شدهاست، جهت نمایش نام او و لینک به Logout، نیاز داریم:
{user && ( <React.Fragment> <NavLink className="nav-item nav-link" to="/logout"> Logout </NavLink> <NavLink className="nav-item nav-link" to="/profile"> {user.DisplayName} </NavLink> </React.Fragment> )}
فعلا تا پیش از پیاده سازی Logout، برای آزمایش آن، به کنسول توسعه دهندگان مرورگر مراجعه کرده و توکن ذخیره شدهی در ذیل قسمت application->storage را دستی حذف کنید. سپس صفحه را ریفرش کنید. اینبار لینکهای به Login و Register نمایان میشوند.
یک مشکل! در این حالت (زمانیکه توکن حذف شدهاست)، از طریق قسمت Login به برنامه وارد شوید. هرچند این قسمتها به درستی کار خود را انجام میدهند، اما هنوز در منوی بالای سایت، نام کاربری و لینک به Logout ظاهر نشدهاند. علت اینجا است که در کامپوننت App، کار دریافت توکن در متد componentDidMount انجام میشود و این متد نیز تنها یکبار در طول عمر برنامه فراخوانی میشود. برای رفع این مشکل به src\components\loginForm.jsx مراجعه کرده و بجای استفاده از history.push برای هدایت کاربر به صفحهی اصلی برنامه، نیاز خواهیم داشت تا کل برنامه را بارگذاری مجدد کنیم. یعنی بجای:
this.props.history.push("/");
window.location = "/";
پیاده سازی Logout کاربر وارد شدهی به سیستم
برای logout کاربر تنها کافی است توکن او را از local storage حذف کنیم. به همین جهت مسیریابی جدید logout را که به صورت لینکی به NavBar اضافه کردیم:
<NavLink className="nav-item nav-link" to="/logout"> Logout </NavLink>
import Logout from "./components/logout"; // ... class App extends Component { render() { return ( // ... <Switch> // ... <Route path="/logout" component={Logout} />
import { Component } from "react"; class Logout extends Component { componentDidMount() { localStorage.removeItem("token"); window.location = "/"; } render() { return null; } } export default Logout;
بهبود کیفیت کدهای نوشته شده
اگر به کامپوننت App دقت کنید، کلید token استفاده شدهی در آن، در چندین قسمت برنامه مانند login و logout، تکرار و پراکنده شدهاست. بنابراین بهتر است جزئیات پیاده سازی مرتبط با اعتبارسنجی کاربران، به ماژول مختص به آنها (src\services\authService.js) منتقل شود تا سایر قسمتهای برنامه، به صورت یکدستی از آن استفاده کنند و اگر در این بین نیاز به تغییری بود، فقط یک ماژول نیاز به تغییر، داشته باشد.
برای این منظور، ابتدا متد login قبلی را طوری تغییر میدهیم که کار ذخیره سازی توکن را نیز در authService.js انجام دهد:
const tokenKey = "token"; export async function login(email, password) { const { data: { access_token } } = await http.post(apiEndpoint + "/login", { email, password }); console.log("JWT", access_token); localStorage.setItem(tokenKey, access_token); }
const { data } = this.state; await auth.login(data.username, data.password); window.location = "/";
همینکار را برای logout نیز در authService انجام داده:
export function logout() { localStorage.removeItem(tokenKey); }
import * as auth from "../services/authService"; class Logout extends Component { componentDidMount() { auth.logout();
import jwtDecode from "jwt-decode"; //... export function getCurrentUser() { try { const jwt = localStorage.getItem(tokenKey); const currentUser = jwtDecode(jwt); console.log("currentUser", currentUser); return currentUser; } catch (ex) { console.log(ex); return null; } }
import * as auth from "./services/authService"; class App extends Component { state = {}; componentDidMount() { const currentUser = auth.getCurrentUser(); this.setState({ currentUser }); }
جای دیگری که از localStorage استفاده شده، متد doSumbit کامپوننت ثبت نام کاربران است. این قسمت را نیز به صورت زیر به authService اضافه میکنیم:
export function loginWithJwt(jwt) { localStorage.setItem(tokenKey, jwt); }
import * as auth from "../services/authService"; // ... const response = await userService.register(this.state.data); auth.loginWithJwt(response.headers["x-auth-token"]);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-27-backend.zip و sample-27-frontend.zip
CREATE TABLE accounts ( user_id INTEGER PRIMARY KEY, balance INTEGER NOT NULL );
INSERT INTO accounts(user_id, balance) VALUES (1, 300);
DECLARE @amount INT; SET @amount = ( SELECT balance FROM accounts WHERE user_id = 1 ); SELECT @amount as 'balance' UPDATE accounts SET balance = @amount - 100 WHERE user_id = 1; SELECT balance as 'balance after shopping' FROM accounts WHERE user_id = 1
- در اینجا مقدار متغیر amount در ابتدای کار، مساوی 300 است که مربوط به همان insert ابتدایی است.
- سپس از این مقدار در کوئری دومی (برای مثال حاصل از خرید شماره یک)، 100 واحد کم میشود (برای مثال قیمت کل خرید است).
- در این حالت نتیجهی آن یا همان موجودی جدید کاربر، 200 خواهد بود.
معادل این عملیات در EF-Core چنین دستورات متداولی است:
var account1 = context.Accounts.First(x => x.UserId == 1); account1.Balance -= 100; context.SaveChanges();
سؤال: اگر کوئریهای فوق را در یک برنامهی ذاتا چند ریسمانی وب، دوبار به صورت همزمان اجرا کنیم، یعنی دو عمل خرید موازی را شبیه سازی کنیم، چه اتفاقی رخ میدهد؟ آیا موجودی نهایی اینبار برای مثال 100 میشود (با فرض 300 بودن موجودی ابتدایی)؟
پاسخ خیر است! و آنرا میتوانید در تصویر زیر مشاهده کنید:
در اینجا برای شبیه سازی اجرای موازی دو کوئری، از دستور WAITFOR TIME استفاده شدهاست که برای برای آزمایش آن میتوانید مقدار آنرا به یک دقیقه بعد تنظیم کرده و سپس آنرا در دو پنجرهی SQL server management studio اجرا کنید.
همانطور که مشاهده میکنید، با اجرای موازی این دو کوئری، یعنی دوبار خرید کردن همزمان، 100 واحد گم شدهاست ! به این مشکل همزمانی read و سپس update رخ داده، یک «race condition» گفته میشود و این روزها که مطالب منتشر شدهی از آسیب پذیریهای برنامههای وب ایرانی را بررسی میکنم، این مورد در صدر آنها قرار دارد!
علت اینجا است که عموما برنامه نویسها، برنامههای وب را در یک تک سشن باز شدهی توسط مرورگر خود آزمایش میکنند و در این حالت، همه چیز خوب است و اعمال آن به ترتیب پیش میروند. اما فراموش میکنند که میتوان قسمتهای مختلف برنامههای وب را به صورت همزمان، موازی و چندباره نیز اجرا کرد؛ حتی اگر آن قسمت متعلق به یک کاربر باشد.
سؤال: آیا استفاده تراکنشها این مشکل را حل نمیکنند؟!
عموما برنامه نویسها تصور میکنند که میتوانند تمام اینگونه مشکلات را با تراکنشها حل کنند:
همانطور که مشاهده میکنید، اینبار هرچند هر دو عملیات خرید داخل BEGIN TRAN و COMMIT TRAN قرار گرفتهاند، اما ... مشکل همزمانی هنوز پابرجا است! چون نوع پیشفرض تراکنش مورد استفاده، READ COMMITTED isolation level است و عدم دقت به آن ممکن است این تصور را ایجاد کند که با تعریف تراکنشها، تمام مشکلات همزمانی برطرف میشوند.
راهحلهای پیشنهادی جهت حل مشکل همزمانی عملیات read/update
برای حل مشکلات مرتبط با race condition و همزمانی درخواستهای read/update، میتوان از یکی از روشهای زیر استفاده کرد:
الف) بجای اینکه یکبار کوئری read و یکبار کوئری update به صورت جداگانه صادر شوند، فقط یکبار کوئری update داشته باشیم.
ب) پیاده سازی Row level locking؛ در صورت پشتیبانی بانک اطلاعاتی مورد استفاده از آن
ج) استفاده از تراکنشهایی از نوع SERIALIZABLE
د) پیاده سازی optimistic locking
این موارد را در ادامه با توضیحات بیشتری بررسی میکنیم.
الف) پرهیز از خواندن و به روز رسانی جداگانه
بجای اینکه مانند اعمال فوق، یکبار select داشته باشیم و یکبار update، بهتر است فقط یک دستور update بکارگرفته شود:
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
اینبار با خلاصه شدن دو دستور select و update به یک دستور update، دیگر پس از دو خرید همزمان، 100 واحد گم شده مشاهده نمیشود (!) و موجودی نهایی صحیح است.
ب) پیاده سازی Row level locking
همیشه امکان تغییر عملیات مورد نیاز، به سادگی حالت الف نیست. در یک چنین حالتهایی جهت حداقل شدن تغییرات مورد نیاز، میتوان از row level locking استفاده کرد:
WAITFOR TIME '13:47:00'; SET NOCOUNT, XACT_ABORT ON; BEGIN TRAN; DECLARE @amount INT; SET @amount = ( SELECT balance FROM accounts WITH (UPDLOCK, HOLDLOCK) WHERE user_id = 1 ); SELECT @amount as 'initial user''s balance' UPDATE accounts SET balance = @amount - 100 WHERE user_id = 1; SELECT balance as 'user''s balance after shopping 1' FROM accounts WHERE user_id = 1; COMMIT TRAN;
در اینجا اضافه شدن WITH (UPDLOCK, HOLDLOCK) را به Select تعریف شده، مشاهده میکنید که به آنها locking hints هم گفته میشود و داخل BEGIN TRAN و COMMIT TRAN عمل میکنند (که نوع پیشفرض آن READ COMMITTED isolation level است). کار UPDLOCK، تبدیل shared lock پیشفرض، به update lock است و کار HOLDLOCK، نگه داشتن قفل صورت گرفته تا پایان کار تراکنش تعریف شدهاست.
با این تغییرات، هر تراکنش همزمان دیگری، تا زمانیکه قفل صورت گرفتهی بر روی ردیف select، رها نشود (یعنی تا زمانیکه تراکنش قفل کننده، به COMMIT TRAN برسد)، نمیتواند آنرا تغییر دهد. به همین جهت است که در تصویر فوق، هرچند هر دو عملیات همزمان اجرا شدهاند، اما یکی موجودی ابتدایی 300 را میبیند و دیگری پس از صبر کردن تا پایان تراکنش و رها شدن قفل، موجودی تغییر یافتهی جدیدی را مشاهده کرده و از آن استفاده میکند. به این ترتیب دیگر 100 واحدی که در اولین تصویر این مطلب مشاهده کردید، گم نشدهاست.
ج) استفاده از تراکنشهایی از نوع SERIALIZABLE
بجای استفاده از روش row level locking یاد شده، روش دیگری را که میتوان استفاده کرد، تغییر نوع پیشفرض تراکنش مورد استفادهاست. برای مثال اگر از یک SERIALIZABLE transaction استفاده کنیم؛ یعنی SET TRANSACTION ISOLATION LEVEL SERIALIZABLE را در ابتدای کار ذکر کنیم و برای مثال دو تراکنش همزمان را اجرا کنیم، اگر در تراکنش اول اطلاعاتی خوانده شود، در هیچ تراکنش دیگری نمیتوان این اطلاعات خوانده شده را تا پایان کار تراکنش اول، تغییر داد:
د) پیاده سازی optimistic locking
پیاده سازی optimistic locking و یا Optimistic concurrency control عموما در سمت برنامه رخ میدهد و توسط ORMها زیاد مورد استفاده قرار میگیرد؛ مانند اضافه کردن ستون اضافی version و یا timestamp به جداول تعریف شده. در این حالت تمام updateها به همراه یک where اضافی هستند تا بررسی کنند که آیا version دریافتی در حین خواندن ردیف در حال به روز رسانی، تغییر کردهاست یا خیر؟ اگر تغییر کردهاست، تراکنش را با خطایی خاتمه خواهند داد. این روش برخلاف حالتهای ب و ج، حتی خارج از یک تراکنش نیز کار میکند و مشکلات قفل کردن طولانی مدت رکوردها توسط آنها را به همراه ندارد.
Blog ID BlogID int Title nvarchar(250) Tags nvarchar(500)
Blog Tags Tbl BlogID int TagID int
در مورد کد دوم هم شاید بتوان به انباشته شدن زیاد سطرها و یا عدم ساختار مدرن اشاره کرد.
CREATE TABLE sal_emp ( name text, pay_by_quarter integer[], schedule text[][] );
راجع به نوع داده array در postgres در بیشتر مطالعه کنید.
سایر مزیتهای postgres را میتوانید از زبان shayro jan sky، در کانال دات نت و در ویدیو لینک شده مشاهده کنید.
مزیت بزرگ آن که باعث میشود تا از آن بتوانیم در پروژههای خود استفاده کنیم، سازگاری آن با ef core میباشد. یعنی اگر کل برنامهی شما با ef core پیاده سازی شده باشد، با عوض کردن متد UseSqlServer به UseNpgsql در کلاس program، مشکلی در برنامه رخ نخواهد داد و اپلیکیشن شما بجای استفاده از sql server به راحتی از postgres استفاده خواهد کرد.
متد نام برده شده در پکیج زیر قابل دسترسی میباشد:
Npgsql.EntityFrameworkCore.PostgreSQL
کارها تماما مانند ef و حتی با کتابخانههای مربوط به آن انجام خواهد شد و تنها تغییر در کدها، همین متد UseNpgsql میباشد که provider را عوض خواهد کرد.
در قسمت بعد به نصب و راه اندازی postrgress و دشبردهای مدیریتی آن از طریق داکر و پیاده سازی CRUD خواهیم پرداخت.
هدایت خودکار کاربر به صفحه لاگین در حین اعمال Ajax ایی
Angular Interceptors
ابتدا مشکل و هدف را بیان میکنیم:
مشکل: کاربر در صفحهای حضور دارد که نیاز به اعتبارسنجی داشته و مدت اعتبار کاربر نیز تمام شده است، ولی هنوز در صفحهای که نباید حضور داشته باشد، حضور دارد و بدتر از آن این است که میتواند درخواستهای بی نتیجهای را نیز ارسال کند.
هدف: کاربر را سریعا به صفحهای که به آن تعلق دارد هدایت کنیم ( یعنی صفحهی ورود به سیستم ).
و حالا از ابتدا پروسه را دنبال میکنیم. یک Controller سمت سرور داریم به این صورت :
[Authorize(Roles = AuthorizeRole.SuperAdministrator)] public partial class HomeController : Controller { [HttpPost] [AngularValidateAntiForgeryToken] public virtual JsonResult GetUserInfo() { var userInfoViewModel = _applicationUserManager.GetUserInfoById(User.Identity.GetUserId()); return Json(userInfoViewModel); } }
یعنی با کدی همانند کد زیر:
$scope.getUserInfo = function() { $http({ method: 'POST', url: 'Home/GetUserInfo', headers: $scope.getHeaders() }). success(function(data, status, headers, config) { $scope.userInfo = data; }). error(function(data, status, headers, config) { }).then(function(res) { }); }
و نتیجهی کدهای بالا به صورت زیر درخواهد آمد :
همانطور که میبینید دادههای اولیه کاربر پس از ورود به سیستم، بدون هیچ مشکلی دریافت میشوند.
نکته : زمانیکه status برابر با 200 هست، یعنی درخواست OK میباشد. ( در پیوست ، لیست تمامی کدها قرار داده شده است )
حالا فرض کنید کاربر در صفحه حضور دارد و به هر دلیلی اعتبار حضور کاربر منقضی شده است و حالا پس از مدتی کاربر درخواستی را به سرور ارسال میکند و میخواهد اطلاعات خودش را مشاهده کند.
درخواست کاربر با همان کدهای اولیه ارسال میشود و خروجی اینبار به صورت زیر در خواهد آمد :
همانطور که میبینید وضعیت اینبار نیز OK میباشد، ولی هیچ دادهای از سرور دریافت نشده است. کاربر قطعا در اینجا دچار سردرگمی میشود. چون هیچ چیزی را مشاهده نمیکند و به هیچ مسیر دیگری نیز هدایت نمیشود و هرکار دیگری نیز انجام دهد، پاسخی مشاهده نمیکند.
راه حل چیست ؟
ابتدا باید برای درخواستهای Ajax ایی اعتبارسنجی را اعمال کنیم. برای این کار باید یک Attribute جدید بسازیم. یعنی به این صورت :
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] public class MyCustomAuthorize : AuthorizeAttribute { protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (filterContext.HttpContext.Request.IsAjaxRequest()) { // یعنی اعتبارسنجی نشده است filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; filterContext.HttpContext.Response.End(); } base.HandleUnauthorizedRequest(filterContext); } }
توجه : کد کاملتر همراه با توضیحات در بخش پیش نیازها آمده است.
حالا در سمت کلاینت نیز به این صورت عمل میکنیم :
$http({ method: 'POST', url: 'Home/GetUserInfo', headers: $scope.getHeaders() }). success(function(data, status, headers, config) { $scope.userInfo = data; }). error(function(data, status, headers, config) { if (status === 401) { // you are not authorized } }).then(function(res) { });
حالا دیگر متوجه خواهیم شد که کاربر اعتبارسنجی نشده است، یا اعتبار آن منقضی شده است و میتوانیم کاربر را به مسیر "ورود به سیستم" هدایت کنیم.
در console مرورگر نیز خطای زیر رخ میدهد :
POST http://localhost:000000/Administrator/Home/GetUserInfo 401 (Unauthorized)
حالا باید از یک Interceptor استفاده و درخواستهای HTTP خودمان را مدیریت کنیم. برای این منظور ما یک Interceptor جدید را همانند کدهای زیر مینویسیم:
factory('AuthorizedInterceptor', function ($q) { return { response: function(response) { return response || $q.when(response); }, responseError: function(rejection) { if (rejection.status === 401) { // you are not authorized } return $q.reject(rejection); } }; })
همانطور که مشاهده میکنید کدهای خطای درخواست http$ را در Interceptor قرار دادیم. حالا کاربر درخواستی را ارسال میکند و ما با توجه به این Interceptor متوجه خواهیم شد که آیا اعتبار آن منقضی شده است یا خیر و در صورت منقضی شدن، کاربر را به صفحهی ورود هدایت خواهیم کرد. یعنی بدین صورت :
در خروجی بالا میبینید که کاربر، اعتبارسنجی نشده است و آن را به مسیر ورود هدایت خواهیم کرد.
با Interceptor بالا دیگر نیازی نیست برای هر درخواستی این وضعیت را چک کنیم و به صورت خودکار این چک کردن رخ خواهد داد.
پیوست : http_status_codes.rar