مطالب دوره‌ها
استفاده از Factories برای حذف Service locators در برنامه‌های WinForms
یک برنامه‌ی WinForms را درنظر بگیرید که از دو فرم تشکیل شده است.
فرم اول کار نمایش فرم 2 را به عهده دارد.
فرم دوم کار ارسال ایمیل را انجام می‌دهد. این ایمیل نیز از طریق سرویس ذیل فراهم می‌شود:
namespace WinFormsIoc.Services
{
    public interface IEmailsService
    {
        void SendEmail(string from, string to, string title, string message);
    }
}

namespace WinFormsIoc.Services
{
    public class EmailsService : IEmailsService
    {
        public void SendEmail(string from, string to, string title, string message)
        {
            //todo: ...
        }
    }
}
پیاده سازی متد SendEmail در اینجا مدنظر نیست. نکته‌ی مهم، مدیریت تامین و تزریق وابستگی‌های تعریف شده در سازنده‌ی آن است:
    public partial class Form2 : Form
    {
        private readonly IEmailsService _emailsService;
        public Form2(IEmailsService emailsService)
        {
            _emailsService = emailsService;
            InitializeComponent();
        }
احتمالا شاید عنوان کنید که در فرم اول، زمانیکه نیاز است فرم دوم نمایش داده شود، می‌نویسیم new Form2 و در پارامتر آن با استفاده از متد ObjectFactory.GetInstance سازنده‌ی آن‌را فراهم می‌کنیم:
 var form2 = new Form2(ObjectFactory.GetInstance<IEmailsService>());
form2.Show();
و یا اگر مدتی با IoC Containers کار کرده باشید، شاید پیشنهاد دهید که فقط بنویسید:
 var form2 = ObjectFactory.GetInstance<Form2>();
form2.Show();
و همین! به صورت خودکار اگر n پارامتر تزریق شده هم در سازنده‌ی فرم دوم وجود داشته باشند، بر اساس تنظیمات اولیه‌ی IoC Container مورد استفاده، نمونه سازی شده و برنامه بدون مشکل کار خواهد کرد.

مشکل! این دو راه حل هیچکدام به عنوان تزریق وابستگی‌ها شناخته نمی‌شوند و به الگوی Service locator معروف هستند. مشکل آن‌ها این است که کدهای ما در حال حاضر وابستگی مستقیمی به IoC container مورد استفاده پیدا کرده‌اند. در حالت اول ما خودمان دستی درخواست داده‌ایم که کدام وابستگی باید وهله سازی شود و در حالت دوم همانند حالت اول، کدهای ObjectFactory.GetInstance، مختص به یک IoC Container خاص است. نحوه‌ی صحیح کار با IoC Container‌ها باید به این نحو باشد که یکبار در آغاز برنامه تنظیم شوند و در ادامه سایر کلاس‌های برنامه طوری کار کنند که انگار IoC Container ایی وجود خارجی ندارد.


راه حل: ObjectFactory.GetInstance را کپسوله کنید.

using System.Windows.Forms;

namespace WinFormsIoc.IoC
{
    public interface IFormFactory
    {
        T Create<T>() where T : Form;
    }
}

using System.Windows.Forms;
using StructureMap;

namespace WinFormsIoc.IoC
{
    public class FormFactory : IFormFactory
    {
        public T Create<T>() where T : Form
        {
            return ObjectFactory.GetInstance<T>();
        }
    }
}
در اینجا یک اینترفیس را تعریف کرده‌ایم که متد ایجاد وهله‌ا‌ی از یک فرم را ارائه می‌دهد. پیاده سازی آن در برنامه‌‌ای که از StructureMap استفاده می‌کند، مطابق کلاس FormFactory است. اگر IoC Container دیگری باشد، فقط باید این پیاده سازی را تغییر دهید و نه کل برنامه را. اکنون برای استفاده از آن، IFormFactory را در سازنده‌ی کلاسی که نیاز دارد فرم‌های دیگر را نمایش دهد، تزریق می‌کنیم:
using System;
using System.Windows.Forms;
using WinFormsIoc.IoC;

namespace WinFormsIoc
{
    public partial class Form1 : Form
    {
        private readonly IFormFactory _formFactory;
        public Form1(IFormFactory formFactory)
        {
            _formFactory = formFactory;
            InitializeComponent();
        }

        private void btnShowForm2_Click(object sender, EventArgs e)
        {
            var form2 = _formFactory.Create<Form2>();
            form2.Show();
        }
    }
}
در کدهای فوق، فرم اول برنامه را ملاحظه می‌کنید که قرار است فرم دوم را نمایش دهد. IFormFactory در سازنده‌ی آن تزریق شده‌است. با فراخوانی متد Create آن، فرم دوم برنامه به همراه تمام وابستگی‌های تزریق شده‌ی در سازنده‌ی آن وهله سازی می‌شوند.
نکته‌ی مهم این کدها عدم وابستگی مستقیم آن به هیچ نوع IoC Container خاصی است. این فرم اصلا نمی‌داند که IoC Container ایی در برنامه وجود دارد یا خیر.


مشکل! با تغییر سازنده‌ی Form1 برنامه دیگر کامپایل نمی‌شود!
اگر فایل Program.cs را باز کنید، یک چنین سطری را دارد:
 Application.Run(new Form1());
چون سازنده‌ی فرم یک، اکنون پارامتر جدیدی پیدا کرده‌است، در اینجا می‌توان ObjectFactory.GetInstance را مستقیما بکار برد (در این حالت خاص که مرتبط است به کلاس آغازین برنامه، با توجه به اینکه وهله سازی آن مستقیما و خارج از کنترل ما انجام می‌شود، دیگر چاره‌ای نداریم و مجبور هستیم از الگوی Service locator استفاده کنیم).
 Application.Run(ObjectFactory.GetInstance<Form1>());

مثال کامل این بحث را از اینجا می‌توانید دریافت کنید
WinFormsIoc.zip
مطالب
پردازش داده‌های جغرافیایی به کمک SQL Server و Entity framework
پشتیبانی SQL Server از Spatial data

از SQL Server 2008 به بعد، نوع داده جدیدی به نام geography به نوع‌های قابل تعریف ستون‌ها اضافه شده‌است. در این نوع ستون‌ها می‌توان طول و عرض جغرافیایی یک نقطه را ذخیره کرد و سپس به کمک توابع توکاری از آن‌ها کوئری گرفت.


در اینجا نمونه‌ای از نحوه‌ی تعریف و همچنین مقدار دهی این نوع ستون‌ها را مشاهده می‌کنید:
 CREATE TABLE [Geo](
[id] [int] IDENTITY(1,1) NOT NULL,
[Location] [geography] NULL
)

 insert into Geo( Location , long, lat ) values
( geography::STGeomFromText ('POINT(-121.527200 45.712113)', 4326))
متد geography::STGeoFromText یک SQL CLR function است. این متد در مثال فوق، مختصات یک نقطه را دریافت کرده‌است. همچنین نیاز دارد بداند که این نقطه توسط چه نوع سیستم مختصاتی ارائه می‌شود. عدد 4326 در اینجا یک SRID یا Spatial Reference System Identifier استاندارد است. برای نمونه اطلاعات ارائه شده توسط Google و یا Bing توسط این استاندارد ارائه می‌شوند.
در اینجا متدهای توکار دیگری مانند geography::STDistance برای یافتن فاصله مستقیم بین نقاط نیز ارائه شد‌ه‌اند. خروجی آن بر حسب متر است.


پشتیبانی از Spatial Data در Entity framework

پشتیبانی از نوع مخصوص geography، در EF 5 توسط نوع داده‌ای DbGeography ارائه شد. این نوع داده‌ای immutable است. به این معنا که پس از نمونه سازی، دیگر مقدار آن قابل تغییر نیست.
در اینجا برای نمونه مدلی را مشاهده می‌کنید که از نوع داده‌ای DbGeography استفاده می‌کند:
using System.Data.Entity.Spatial;

namespace EFGeoTests.Models
{
    public class GeoLocation
    {
        public int Id { get; set; }
        public DbGeography Location { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }

        public override string ToString()
        {
            return string.Format("Name:{0}, Location:{1}", Name, Location);
        }
    }
}
به همراه یک Context، تا کلاس GeoLocation در معرض دید EF قرار گیرد:
using System;
using System.Data.Entity;
using EFGeoTests.Models;

namespace EFGeoTests.Config
{
    public class MyContext : DbContext
    {
        public DbSet<GeoLocation> GeoLocations { get; set; }

        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }
    }
}
برای مقدار دهی خاصیت Location از نوع DbGeography می‌توان از متد ذیل استفاده کرد که بسیار شبیه به متد geography::STGeoFromText عمل می‌کند:
   private static DbGeography createPoint(double longitude, double latitude,  int coordinateSystemId = 4326)
  {
       var text = string.Format(CultureInfo.InvariantCulture.NumberFormat,"POINT({0} {1})", longitude, latitude);
       return DbGeography.PointFromText(text, coordinateSystemId);
  }


تهیه منبع داده‌ی جغرافیایی

برای تدارک یک مثال واقعی جغرافیایی، نیاز به اطلاعاتی دقیق داریم. این نوع اطلاعات عموما توسط یک سری فایل مخصوص به نام Shapefiles که حاوی اطلاعات برداری جغرافیایی هستند ارائه می‌شوند. برای نمونه اطلاعات جغرافیایی به روز ایران را از آدرس ذیل می‌توانید دریافت کنید:
http://download.geofabrik.de/asia/iran.html
http://download.geofabrik.de/asia/iran-latest.shp.zip

پس از دریافت این فایل، به تعدادی فایل با پسوندهای shp، shx و dbf خواهیم رسید.
فایل‌های shp بیانگر فرمت اشکال ذخیره شده هستند. فایل‌های shx یک سری ایندکس بوده و فایل‌های dbf از نوع بانک اطلاعاتی dBase IV می‌باشند.
همچنین اگر فایل‌های prj را باز کنید، یک چنین اطلاعاتی در آن موجودند:
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
نکته‌ی مهمی که در اینجا باید مدنظر داشت، استاندارد GCS_WGS_1984 آن است. این استاندارد معادل است با استاندارد EPSG 4326. عدد 4326 آن جهت ثبت این اطلاعات در یک بانک اطلاعاتی SQL Server حائز اهمیت است (پارامتر coordinateSystemId در متد createPoint) و ممکن است از هر فایلی به فایل دیگر متفاوت باشد.



خواند‌ن فایل‌های shp در دات نت

پس از دریافت فایل‌های shp و بانک‌های اطلاعاتی مرتبط با اطلاعات جغرافیایی ایران، اکنون نوبت به پردازش این فایل‌های مخصوص با فرمت بانک اطلاعاتی فاکس پرو مانند، رسیده‌است. برای این منظور می‌توان از پروژه‌ی سورس باز ذیل استفاده کرد:

این پروژه در خواندن فایل‌های shp بدون نقص عمل می‌کند اما توانایی خواندن نام‌های فارسی وارد شده در این نوع بانک‌های اطلاعاتی را ندارد. برای رفع این مشکل، سورس آن را از Codeplex دریافت کنید. سپس فایل Shapefile.cs را گشوده و ابتدای خاصیت Current آن‌را به نحو ذیل تغییر دهید:
        /// <summary>
        /// Gets the current shape in the collection
        /// </summary>
        public Shape Current
        {
            get 
            {
                if (_disposed) throw new ObjectDisposedException("Shapefile");
                if (!_opened) throw new InvalidOperationException("Shapefile not open.");
               
                // get the metadata
                StringDictionary metadata = null;
                if (!RawMetadataOnly)
                {
                    metadata = new StringDictionary();
                    for (int i = 0; i < _dbReader.FieldCount; i++)
                    {
                        string value = _dbReader.GetValue(i).ToString();
                        if (_dbReader.GetDataTypeName(i) == "DBTYPE_WVARCHAR")
                        {
                            // برای نمایش متون فارسی نیاز است
                            value = Encoding.UTF8.GetString(Encoding.GetEncoding(720).GetBytes(value));
                        }
                        metadata.Add(_dbReader.GetName(i),
                            value);
                    }
                }
در اینجا فقط سطر استفاده از Encoding خاصی با شماره 720 و تبدیل آن به UTF8 اضافه شده‌است. پس از آن بدون مشکل می‌توان برچسب‌های فارسی را از فایل‌های dBase IV این نوع بانک‌های اطلاعاتی استخراج کرد (اصلاح شده‌ی آن در فایل پیوست مطلب موجود است).
using System.Collections.Generic;
using System.Linq;
using Catfood.Shapefile;

namespace EFGeoTests
{
    public class MapPoint
    {
        public Dictionary<string, string> Metadata { set; get; }
        public double X { set; get; }
        public double Y { set; get; }
    }

    public static class ShapeReader
    {
        public static IList<MapPoint> ReadShapeFile(string path)
        {
            var results = new List<MapPoint>();

            using (var shapefile = new Shapefile(path))
            {
                foreach (var shape in shapefile)
                {
                    if (shape.Type != ShapeType.Point)
                        continue;

                    var shapePoint = shape as ShapePoint;
                    if (shapePoint == null)
                        continue;


                     var metadataNames = shape.GetMetadataNames();
                    if(!metadataNames.Any())
                        continue;

                    var metadata = new Dictionary<string, string>();
                    foreach (var metadataName in metadataNames)
                    {
                        metadata.Add(metadataName,shape.GetMetadata(metadataName));
                    }

                    results.Add(new MapPoint
                    {
                        Metadata = metadata,
                        X = shapePoint.Point.X,
                        Y = shapePoint.Point.Y
                    });
                }
            }

            return results;
        }
    }
}
در کدهای فوق به کمک کتابخانه‌ی C# Esri Shapefile Reader، اطلاعات نقاط بانک اطلاعاتی shape files را خوانده و به صورت لیست‌هایی از MapPoint بازگشت می‌دهیم. نکته‌ی مهم آن، Metadata است که از هر فایلی به فایل دیگر می‌توان متفاوت باشد. به همین جهت این اطلاعات را به شکل ویژگی‌های key/value در این نوع بانک‌های اطلاعاتی ذخیره می‌کنند.


افزودن اطلاعات جغرافیایی به بانک اطلاعاتی SQL Server به کمک Entity framework

فایل places.shp را در مجموعه فایل‌هایی که در ابتدای بحث عنوان شدند، می‌توانید مشاهده کنید. قصد داریم اطلاعات نقاط آن‌را به مدل GeoLocation انتساب داده و سپس ذخیره کنیم:
            var points = ShapeReader.ReadShapeFile("IranShapeFiles\\places.shp");
            using (var context = new MyContext())
            {
                context.Configuration.AutoDetectChangesEnabled = false;
                context.Configuration.ProxyCreationEnabled = false;
                context.Configuration.ValidateOnSaveEnabled = false;

                if (context.GeoLocations.Any())
                    return;

                foreach (var point in points)
                {
                    context.GeoLocations.Add(new GeoLocation
                    {
                        Name = point.Metadata["name"],
                        Type = point.Metadata["type"],
                        Location = createPoint(point.X, point.Y)
                    });
                }

                context.SaveChanges();
            }
تعریف متد createPoint را که بر اساس X و Y نقاط، معادل قابل پذیرش آن‌را جهت SQL Server تهیه می‌کند، در ابتدای بحث مشاهده کردید.
در فایل‌های مرتبط با places.shp، متادیتا name، معادل نام شهرهای ایران است و type آن بیانگر شهر، روستا و امثال آن می‌باشد.
پس از اینکه اطلاعات مکان‌های ایران، در SQL Server ذخیره شدند، نمایش بصری آن‌ها را در management studio نیز می‌توان مشاهده کرد:



کوئری گرفتن از اطلاعات جغرافیایی

فرض کنید می‌خواهیم مکان‌هایی را با فاصله کمتر از 5 کیلومتر از تهران پیدا کنیم:
            var tehran = createPoint(51.4179604, 35.6884243);

            using (var context = new MyContext())
            {
                // find any locations within 5 kilometers ordered by distance
                var locations = context.GeoLocations
                    .Where(loc => loc.Location.Distance(tehran) < 5000)
                    .OrderBy(loc => loc.Location.Distance(tehran))
                    .ToList();

                foreach (var location in locations)
                {
                    Console.WriteLine(location.Name);
                }
            }
همانطور که پیشتر نیز عنوان شد، متد Distance بر اساس متر کار می‌کند. به همین جهت برای تعریف 5 کیلومتر به نحو فوق عمل شده‌است. همچنین نحوه‌ی مرتب سازی اطلاعات نیز بر اساس فاصله از یک مکان مشخص صورت گرفته‌است.
و یا اگر بخواهیم دقیقا بر اساس مختصات یک نقطه، مکانی را بیابیم، می‌توان از متد SpatialEquals استفاده کرد:
            var tehran = createPoint(51.4179604, 35.6884243);
            using (var context = new MyContext())
            {
                // find any locations within 5 kilometers ordered by distance
                var tehranLocation = context.GeoLocations.FirstOrDefault(loc => loc.Location.SpatialEquals(tehran));
                if (tehranLocation != null)
                {
                    Console.WriteLine(tehranLocation.Type);
                }
            }

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
EFGeoTests.zip
 
مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت سوم - جمع آوری اطلاعات آماری کوئری‌ها توسط DMO's
Extended events ای که در قسمت قبل بررسی شدند، جهت جمع آوری اطلاعات آماری تک کوئری‌ها مورد استفاده قرار می‌گیرند؛ اما Dynamic management objects یا به اختصار DMO's، تجمعی عمل می‌کنند (برای مثال جهت محاسبه‌ی میانگین logical reads چند کوئری مانند هم). متن یک کوئری و پلن آن، توسط DMO's مختلفی قابل استخراج هستند. متن یک کوئری توسط sys.dm_exec_sql_text قابل استخراج است و برای دسترسی به کوئری پلن‌ها از sys.dm_exec_query_plan، sys.dm_exec_cached_plans و sys.dm_exec_text_query_plan استفاده می‌شود. در این حالت برای دسترسی به اطلاعات آماری از sys.dm_exec_query_stats و sys.dm_exec_function_stats کمک گرفته خواهد شد.


استفاده از Dynamic management objects برای جمع آوری اطلاعات آماری کوئری‌ها

در ادامه در طی چند مثال، روش استخراج اطلاعات آماری کوئری‌ها را توسط DMO's بررسی می‌کنیم.

دریافت متن کوئری‌های در حال اجرا

توسط کوئری زیر که توسط تابع sys.dm_exec_sql_text اجرا می‌شود، می‌توان لیست کوئری‌های در حال اجرای بر روی بانک‌های اطلاعاتی جاری را بدست آورد:
SELECT
    [r].[session_id],
    DB_NAME([r].[database_id]) [DatabaseName],
    [t].[text]
FROM sys.dm_exec_requests [r]
CROSS APPLY sys.dm_exec_sql_text([r].sql_handle) [t];
GO
در اینجا text، همان متن کوئری است و هربار که این کوئری اجرا می‌شود، نتیجه‌ی متفاوتی را بر اساس کوئری‌هایی که در آن لحظه در حال اجرا هستند، دریافت خواهیم کرد.
تابع sys.dm_exec_sql_text برای اجرا نیاز به یک sql_handle دارد که آن‌را از طریق sys.dm_exec_requests می‌توان تامین کرد.


دریافت پلن کوئری‌های در حال اجرا

توسط کوئری زیر که توسط تابع sys.dm_exec_query_plan اجرا می‌شود، می‌توان لیست پلن کوئری‌های در حال اجرای بر روی بانک‌های اطلاعاتی جاری را بدست آورد:
SELECT
    [r].[session_id],
    DB_NAME([r].[database_id]) [DatabaseName],
    [t].[text],
    [p].[query_plan]
FROM sys.dm_exec_requests [r]
CROSS APPLY sys.dm_exec_sql_text([r].sql_handle) [t]
CROSS APPLY sys.dm_exec_query_plan([r].[plan_handle]) [p];
GO
تابع sys.dm_exec_query_plan برای اجرا نیاز به یک plan_handle دارد که آن‌را از طریق sys.dm_exec_requests می‌توان تامین کرد.
حاصل این کوئری، به همراه text یا اصل متن کوئری‌های در حال اجرا و همچنین query_plan، یا همان اطلاعات XML ای پلن که در قسمت اول، نمونه‌ای از آن‌را بررسی کردیم، می‌باشد که با کلیک بر روی هر کدام در management studio، نمایش گرافیکی آن‌ها ظاهر خواهد شد. البته این پلن‌ها، تنها تخمین‌ها را به همراه دارند؛ چون از کش خوانده می‌شوند.


دریافت لیست پلن‌های کش شده

توسط Viewای به نام sys.dm_exec_cached_plans می‌توان به لیست پلن‌های کش شده‌ی در سیستم دسترسی یافت:
SELECT *
FROM sys.dm_exec_cached_plans;
البته خروجی آن، آنچنان جالب نیست. چون یکی از ستون‌های آن، فقط حاوی همان plan_handle ای است که در مثال قبل بررسی کردیم و به خودی خود، حاوی اطلاعات قابل مشاهده‌ای نیست. به همین جهت اگر بخواهیم آن‌را با کوئری‌هایی که تاکنون نوشتیم، ترکیب کنیم به کوئری زیر خواهیم رسید:
SELECT
    [r].[session_id],
    DB_NAME([r].[database_id]) [DatabaseName],
    [cp].[objtype],
    [cp].[size_in_bytes],
    [t].[text],
    [p].[query_plan]
FROM sys.dm_exec_requests [r]
CROSS APPLY sys.dm_exec_sql_text([r].sql_handle) [t]
CROSS APPLY sys.dm_exec_query_plan([r].[plan_handle]) [p]
    JOIN sys.dm_exec_cached_plans [cp]
    ON [r].[plan_handle] = [cp].[plan_handle];
GO
مزیت این کوئری نسبت به موارد قبلی، وجود ستون‌های جدید objtype و size_in_bytes است که بیانگر نوع کوئری، مانند AdHoc و اندازه‌ی پلن در کش هستند.


دریافت متن پلن‌های تو در تو و عمیق

با استفاده از تابع sys.dm_exec_text_query_plan می‌توان به متن پلن‌های عمیق دسترسی یافت. در این حالت خروجی کوئری در management studio به صورت یک لینک قابل کلیک ظاهر نمی‌شود و صرفا یک متن قابل کپی است که می‌توان آن‌را با پسوند sqlplan برای بررسی‌های بعدی، ذخیره کرد:
SELECT
    [r].[session_id],
    DB_NAME([r].[database_id]) [DatabaseName],
    [tq].[query_plan]
FROM sys.dm_exec_requests [r]
CROSS APPLY sys.dm_exec_text_query_plan([r].plan_handle, 0, -1) [tq];
GO
در اینجا اعداد 0 و 1- به معنای ابتدا و انتهای batch هستند.


دریافت اطلاعات آماری کوئری‌های درحال اجرا

توسط viewای به نام sys.dm_exec_query_stats می‌توان به اطلاعات آماری کوئری‌های در حال اجرا دسترسی یافت:
SELECT *
FROM sys.dm_exec_query_stats;
GO
این کوئری تعداد ستون‌های قابل توجهی را به همراه دارد مانند Physical reads، logical reads و .... به همین جهت نیاز است اطلاعات مفید آن‌را فیلتر کرد:
SELECT
    [qs].[last_execution_time],
    [qs].[execution_count],
    [qs].[total_logical_reads]/[qs].[execution_count] [AvgLogicalReads],
    [qs].[max_logical_reads],
    [t].[text],
    [p].[query_plan]
FROM sys.dm_exec_query_stats [qs]
CROSS APPLY sys.dm_exec_sql_text([qs].sql_handle) [t]
CROSS APPLY sys.dm_exec_query_plan([qs].[plan_handle]) [p]
WHERE [qs].[execution_count] > 25
    OR [qs].[total_logical_reads] > 10000
ORDER BY [qs].[total_logical_reads]/[qs].[execution_count] DESC;
GO
این کوئری در حقیقت ترکیبی است از کوئری‌هایی که تاکنون نوشتیم و در آن text و query_plan از sys.dm_exec_sql_text و sys.dm_exec_query_plan تامین شده‌اند، به همراه تعدادی ستون مفید sys.dm_exec_query_stats مانند last_execution_time و AvgLogicalReads. به علاوه در اینجا کوئری‌هایی که بیشتر از 25 بار اجرا شده‌اند و یا total_logical_reads آن‌ها بیش از 10 هزار بوده، در خروجی ظاهر خواهند شد (مفهوم تجمعی بودن DMO's).

از SQL Server 2016 به بعد، امکان دریافت اطلاعات آماری توابع نیز میسر شده‌است:
SELECT *
FROM sys.dm_exec_function_stats;
GO

یک نکته: قابلیت جدیدی تحت عنوان Query Store از زمان SQL Server 2016 معرفی شد‌ه‌است و کار آن دریافت تمام اطلاعاتی است که تاکنون بررسی کردیم و تفاوت آن، در ذخیره شده بودن آن است. یعنی این اطلاعات را داخل بانک اطلاعاتی در حال بررسی ذخیره می‌کند که شامل متن و پلن کوئری و همچنین اطلاعات آماری آن است که توسط DMO's تهیه می‌شود.
اشتراک‌ها
رفتار ایندکس‌های یکتا در SQL Server
چند روز پیش در شرکتمون به یک مشکل برخوردم. موقع ثبت یک ردیف در SQL Server، سیستم پیغام خطای زیر را میداد:

[Cannot insert duplicate key row in object 'dbo.[X]' with unique index 'UniqueIndex'. The duplicate key value is [x
بعد از کمی بررسی دو تا نکته را فهمیدم:
  • وقتی روی یک ستون متنی NVARCHAR ایندکس یکتا میگذارید، مقادیر یکسان با حروف بزرگ و کوچک برای SQL یکسان هستند و امکان ثبت را به شما نمی‌دهد؛ یعنی a=A
  • همینطور SQL بین دو مقدار که فقط با فاصله در انتها متفاوت باشند فرقی قائل نمی‌شود؛ یعنی ' a'='a'
رفتار ایندکس‌های یکتا در SQL Server
نظرات مطالب
افزودن یک DataType جدید برای نگه‌داری تاریخ خورشیدی - 2
مقاله‌ای هست اینجا در مورد کارآیی CLR در SQL Server. به نظر می‌رسه سریعتر است حدود 11 درصد نسبت به T-SQL معمولی. برای پارتشین بندی می‌تونید اینکار رو انجام بدید فقط این نوع خاص قابل انتخاب نیست. مابقی فیلدها رو می‌تونید انتخاب کنید.
اشتراک‌ها
PostgreSQL 17 منتشر شد
PostgreSQL 17 Released — The big one is here. It's the newest major version of Postgres, and it takes a bigger step forward than even v16. Some of what’s new:
  • Overhauled memory management for vacuuming, resulting in significantly lower memory usage and running time. More on this here.
  • Incremental backup support.
  • Faster B-tree index scans.
  • MERGE enhancements, including view support.
  • New functions to extract elements from UUIDs.
  • WAL improvements – up to 2x write throughput on some workloads.
  • Improvements to SQL/JSON support, including JSON_TABLE.
  • Bulk loading improvements and perf improvements for COPY which gains the ON_ERROR ignore option to ignore errors.
  • Identity columns on partitioned tables.
PostgreSQL 17 منتشر شد
مطالب
کوئری های پیشرفته، Error Handling و Data Loader در GraphQL
در قسمت قبل یادگرفتیم که چگونه GraphQL را با ASP.NET Core یکپارچه کنیم و اولین GraphQL query را ایجاد و داده‌ها را از سرور بازیابی کردیم. البته ما به این query ‌های ساده بسنده نخواهیم کرد. در این قسمت می‌خواهیم یاد بگیریم که چگونه query ‌های پیشرفته‌ی GraphQL را بنویسیم و در زمان انجام این کار، نمایش دهیم که چگونه خطا‌ها را  مدیریت کنیم و علاوه بر این با queries, aliases, arguments, fragments نیز کار خواهیم کرد.

Creating Complex Types for GraphQL Queries 
اگر نگاهی به owners و query (در پایان قسمت قبل)  بیندازیم، متوجه خواهیم شد که یک لیست از خصوصیات مدل Owner که در OwnerType معرفی شده‌اند، نسبت به کوئری برگشت داده می‌شود. OwnerType شامل فیلد‌های Id , Name  و Address می‌باشد. یک Owner می‌تواند چندین account مرتبط با خود را داشته باشد. هدف این است که در owners ،query لیست account ‌های مربوط به هر owner را بازگشت دهیم. 
قبل از اضافه کردن فیلد Accounts در کلاس OwnerType نیاز است کلاس AccountType را ایجاد کنیم. در ادامه یک کلاس را به نام AccountType در پوشه GraphQLTypes ایجاد می‌کنیم. 
public class AccountType : ObjectGraphType<Account>
{
    public AccountType()
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the account object.");
        Field(x => x.Description).Description("Description property from the account object.");
        Field(x => x.OwnerId, type: typeof(IdGraphType)).Description("OwnerId property from the account object.");
    }
}

 همانطور که مشخص است، خصوصیت Type را از کلاس Account، معرفی نکرده‌ایم (در ادامه اینکار را انجام خواهیم داد). در ادامه، واسط IAccountRepository و کلاس AccountRepository را باز کرده و آن را مطابق زیر ویرایش می‌کنیم: 
public interface IAccountRepository
{
    IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId);
}

public class AccountRepository : IAccountRepository
{
    private readonly ApplicationContext _context;
 
    public AccountRepository(ApplicationContext context)
    {
       _context = context;
    }
 
    public IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId) => _context.Accounts
        .Where(a => a.OwnerId.Equals(ownerId))
        .ToList();
}

اکنون می‌توان لیست account‌ها را به نتیجه owners ، query اضافه کنیم. پس کلاس OwnerType را باز کرده و آن را مطابق زیر ویرایش می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType(IAccountRepository repository)
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object.");
        Field(x => x.Name).Description("Name property from the owner object.");
        Field(x => x.Address).Description("Address property from the owner object.");
        Field<ListGraphType<AccountType>>(
            "accounts",
            resolve: context => repository.GetAllAccountsPerOwner(context.Source.Id)
        );
    }
}

چیز خاصی در اینجا وجود ندارد که ما تا کنون ندیده باشیم. به همان روش که یک فیلد را در کلاس AppQuery  ایجاد کردیم، یک فیلد را با نام  accounts در کلاس OwnerType ایجاد می‌کنیم. همچنین متد GetAllAccountsPerOwner نیاز به پارامتر id را دارد و این پارامتر را از طریق context.Source.Id فراهم می‌کنیم. زیرا context شامل خصوصیت Source است که در این حالت مشخص نوع Owner می‌باشد.

اکنون پروژه را اجرا کنید و به آدرس زیر بروید:
https://localhost:5001/ui/playground
سپس owners ، query را در UI.Playground به صورت زیر اجرا کنید که نتیجه آن علاوه بر owner‌ها، لیست account ‌های مربوط به هر owner هم می‌باشد:  
{
  owners{
    id,
    name,
    address,
    accounts{
      id,
      description,
      ownerId
    }
  }
}

Adding Enumerations in GraphQL Queries 
در کلاس AccountType  فیلد Type را اضافه نکرده‌ایم و این کار را عمدا انجام داده‌ایم. اکنون زمان انجام این کار می‌باشد. برای اضافه کردن گونه شمارشی به کلاس AccountType نیاز است تا در ابتدا یک کلاس تعریف شود که نسبت به type ‌های معمول در GraphQL متفاوت است. یک کلاس را به نام AccountTypeEnumType  در پوشه GraphQLTypes  ایجاد کرده و آن را مطابق زیر ویرایش می‌کنیم:  
public class AccountTypeEnumType : EnumerationGraphType<TypeOfAccount>
{
    public AccountTypeEnumType()
    {
        Name = "Type";
        Description = "Enumeration for the account type object.";
    }
}

کلاس AccountTypeEnumType باید از نوع جنریک کلاس EnumerationGraphType ارث بری کند و پارامتر جنریک آن، یک گونه شمارشی را دریافت می‌کند (که در قسمت قبل آن را ایجاد کردیم؛ TypeOfAccount).  همچنین مقدار خصوصیت Name نیز باید همان نام خصوصیت گونه شمارشی در کلاس Account باشد (نام آن در کلاس Account مساوی Type می‌باشد). سپس گونه شمارشی را در کلاس AccountType به صورت زیر اضافه می‌کنیم:
public class AccountType : ObjectGraphType<Account>
{
    public AccountType()
    {
        ...
        Field<AccountTypeEnumType>("Type", "Enumeration for the account type object.");
    }
}

اکنون پروژه را اجرا کنید و سپس owners ، query را در UI.Playground به صورت زیر اجرا کنید:
{
  owners{
    id,
    name,
    address,
    accounts{
      id,
      description,
      type,
      ownerId
    }
  }
}
که نتیجه آن اضافه شدن type  به هر account می‌باشد:


Implementing a Cache in the GraphQL Queries with Data Loader  
دیدم که query، نتیجه دلخواهی را برای ما بازگشت می‌دهد؛ اما این query هنوز به اندازه کافی بهینه نشده‌است. مشکل چیست؟
query  ایجاد شده به حالتی کار می‌کند که در ابتدا همه owner ‌ها را بازیابی می‌کند. سپس به ازای هر owner، یک Sql Query  را به سمت بانک اطلاعاتی ارسال می‌کند تا Account ‌های مربوط به آن Owner را بازگشت دهد که می‌توان log  آن را در Terminal مربوط به VS Code مشاهده کرد.
 


البته زمانیکه چند موجودیت owner را داشته باشیم، این مورد یک مشکل نمی‌باشد؛ ولی وقتی تعداد موجودیت‌ها زیاد باشد چطور؟
 owners ، query را می‌توان با استفاده از DataLoader که توسط GraphQL فراهم شده‌است، بهینه سازی کرد. جهت انجام اینکار در ابتدا واسط IAccountRepository و کلاس AccountRepository را همانند زیر ویرایش می‌کنیم:
public interface IAccountRepository
{
    ...
    Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds);
}
public class AccountRepository : IAccountRepository
{
    ...
    public async Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds)
    {
        var accounts = await _context.Accounts.Where(a => ownerIds.Contains(a.OwnerId)).ToListAsync();
        return accounts.ToLookup(x => x.OwnerId);
    }
}

 نیاز است که یک متد داشته باشیم که <<Task<ILookup<TKey, T  را برگشت می‌دهد؛ زیرا DataLoader نیازمند یک متد با نوع برگشتی که در امضایش عنوان شده است می‌باشد .
در ادامه کلاس OwnerType را مطابق زیر ویرایش می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType(IAccountRepository repository, IDataLoaderContextAccessor dataLoader)
    {
       ...
        Field<ListGraphType<AccountType>>(
            "accounts",
            resolve: context =>
            {
                var loader = dataLoader.Context.GetOrAddCollectionBatchLoader<Guid, Account>("GetAccountsByOwnerIds", repository.GetAccountsByOwnerIds);
                return loader.LoadAsync(context.Source.Id);
            });
    }
}

در کلاس OwnerType، واسط IDataLoaderContextAccessor را در سازنده کلاس تزریق می‌کنیم و سپس متد Context.GetOrAddCollectionBatchLoader را فراخوانی می‌کنیم که در پارامتر اول آن، یک کلید و در پارامتر دوم آن، متد GetAccountsByOwnerIds را از IAccountRepository معرفی می‌کنیم.
سپس باید DataLoader را در متد ConfigureServices موجود در کلاس Startup ثبت کنیم. در ادامه services.AddGraphQL را مطابق زیر ویرایش می‌کنیم: 
services.AddGraphQL(o => { o.ExposeExceptions = false; })
        .AddGraphTypes(ServiceLifetime.Scoped)
        .AddDataLoader(); 
اکنون پروژه را با دستور زیر اجرا کنید و سپس query قبلی را در UI.Playground اجرا کنید.
 اگر log  موجود در Terminal مربوط به  VS Code را مشاهده کنید، متوجه خواهید شد که در این حالت یک query برای تمام owner ‌ها و یک query برای تمام account ‌ها داریم.


Using Arguments in Queries and Handling Errors 
تا کنون ما  یک query را اجرا می‌کردیم که نتیجه آن بازیابی تمام owner ‌ها به همراه تمام account ‌های مربوط به هر owner بود. اکنون می‌خواهیم  براساس id، یک owner  مشخص را بازیابی کنیم. برای انجام این کار نیاز است که یک آرگومان را در query شامل کنیم.
در ابتدا واسط IOwnerRepository و کلاس OwnerRepository را همانند زیر ویرایش می‌کنیم:
public interface IOwnerRepository
{
    ...
    Owner GetById(Guid id);
}

public class OwnerRepository : IOwnerRepository
{
    ...
    Owner GetById(Guid id) => _context.Owners.SingleOrDefault(o => o.Id.Equals(id));
}
سپس کلاس AppQuery  را مطابق زیر ویرایش می‌کنیم:
public class AppQuery : ObjectGraphType
{
    public AppQuery(IOwnerRepository repository)
    {
        ...

        Field<OwnerType>(
            "owner",
            arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
            resolve: context =>
            {
                var id = context.GetArgument<Guid>("ownerId");
                return repository.GetById(id);
            }
        );
    }
}
در اینجا یک فیلد را ایجاد کرده‌ایم که مقدار برگشتی آن یک OwnerType می‌باشد. نام query را owner تعیین می‌کنیم و از بخش arguments، برای ایجاد کردن آرگومان‌های این query استفاده می‌کنیم. آرگومان این query نمی‌تواند NULL باشد و باید از نوع IdGraphType و با نام ownerId باشد و در نهایت بخش resolve است که کاملا گویا می‌باشد. 
اگر پارامتر id، از نوع Guid نباشد، بهتر است که یک پیام را به سمت کلاینت برگشت دهیم. جهت انجام این کار یک اصلاح کوچک در بخش resolve انجام میدهیم:  
Field<OwnerType>(
    "owner",
    arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
    resolve: context =>
    {
        Guid id;
        if (!Guid.TryParse(context.GetArgument<string>("ownerId"), out id))
        {
            context.Errors.Add(new ExecutionError("Wrong value for guid"));
            return null;
        }
 
         return repository.GetById(id);
     }
);

 اکنون پروژه را اجرا کنید و سپس یک query جدید را در  UI.Playground به صورت زیر ارسال  کنید: 
{
  owner(ownerId:"6f513773-be46-4001-8adc-2e7f17d52d83"){
    id,
    name,
    address,
    accounts{
      id,
      description,
      type,
      ownerId
    }
  }
که نتیجه آن بازیابی یک owner  با ( Id=6f513773-be46-4001-8adc-2e7f17d52d83 ) می‌باشد.

نکته: 
در صورتیکه قصد داشته باشیم علاوه بر id، یک name را هم ارسال کنیم، در بخش resolve به صورت زیر آن را دریافت می‌کنیم: 
 string name = context.GetArgument<string>("name");
و در زمان ارسال query:
{
  owner(ownerId:"53270061-3ba1-4aa6-b937-1f6bc57d04d2", name:"ANDY") {
   ...
  }
}


Aliases, Fragments, Named Queries, Variables, Directives 
می توانیم برای query ‌های ارسال شده از سمت کلاینت با معرفی aliases، یک سری تغییرات را داشته باشیم. وقتی‌که می‌خواهیم نام نتیجه دریافتی یا هر فیلدی را در نتیجه دریافتی تغییر دهیم، بسیار کاربردی می‌باشند. اگر یک query داشته باشیم که یک آرگومان را دارد و بخواهیم دو تا از این query داشته باشیم، برای ایجاد تفاوت بین query ‌ها می‌توان از aliases  استفاده کرد. 
جهت استفاده باید نام مورد نظر را در ابتدای query یا فیلد قرار دهیم:
{
  first:owners{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
  },
  second:owners{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
  }
}
اینبار در خروجی بجای ownerId ، id و بجای ownerName ، name و ... را مشاهده خواهید کرد.


همانطور که از مثال بالا مشخص است، دو query با فیلد‌های یکسانی را داریم. اگر بجای 2  query یکسان (مانند مثال بالا) ولی با آرگومان‌های متفاوت، اینبار 10 query یکسان با آرگومان‌های متفاوتی را داشته باشیم، در این حالت خواندن query ‌ها مقداری سخت می‌باشد. در این صورت می‌توان این مشکل را با استفاده از fragment‌ها برطرف کرد. Fragment‌‌ها این اجازه را به ما می‌دهند تا فیلد‌ها را با استفاده از کاما ( ، ) از یکدیگر جدا و تبدیل به یک بخش مجزا کنیم و سپس استفاده مجدد از آن بخش را در تمام query ‌ها داشته باشیم. Syntax آن به حالت زیر می‌باشد: 
fragment SampleName on Type{
  ...
}
تعریف یک fragment به نام ownerFields  و استفاده از آن : 
{
  first:owners{
    ...ownerFields
  },
  second:owners{
    ...ownerFields
  },
  ...
}


fragment ownerFields on OwnerType{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
}

برای ایجاد کردن یک named query، مجبور هستیم از کلمه کلیدی query در آغاز کل query استفاده کنیم؛ به همراه نام query، که بعد از کلمه کلیدی query قرار میگیرد. اگر نیاز داشته باشیم می‌توان آرگومان‌ها را به query ارسال کرد.
نکته مهمی که در رابطه با named query ‌ها وجود دارد این است که اگر یک query آرگومان داشته باشد نیاز است از پنجره QUERY VARIABLES برای تخصیص مقدار به آن آرگومان استفاده کنیم. 
query OwnerQuery($ownerId:ID!)
{
  owner(ownerId:$ownerId){
    id,
    name,
    address,
    accounts{
      id,
      description,
      type
    }
  }
}
و سپس در قسمت QUERY VARIABLES 
{
  "ownerId":"6f513773-be46-4001-8adc-2e7f17d52d83"
}
اکنون اجرا کنید و خروجی را مشاهده کنید .

در نهایت می‌توان بعضی فیلد‌ها را از نتیجه دریافتی با استفاده از directive‌ها در query حذف یا اضافه کرد. دو directive وجود دارد که می‌توان از آن‌ها استفاده کرد (include  و skip). 


در قسمت بعد در رابطه با GraphQL Mutations صحبت خواهیم کرد.  
کد‌های مربوط به این قسمت را از اینجا دریافت کنید .  ASPCoreGraphQL_2.zip  
مطالب
ایجاد سرویس چندلایه‎ی WCF با Entity Framework در قالب پروژه - 6
پروژه را اجرا کنید و در WCF Test Client به وسیله‌ی متد AddNews دو خبر جدید درج کنید. 

روی متدهای GetAllCategory و GetAllNews به صورت جداگانه کلیک کنید. متوجه خواهید شد که هرچند در کلاس tblNews شی‌ای از نوع tblCategory و در کلاس tblCategory شی‌ای از نوع مجموعه‌ی tblNews به صورت Virtual تعریف شده است ولی در بر خلاف انتظارمان اثری از آن در این‌جا دیده نمی‌شود. نتیجه‌ی مشاهده‌شده به خاطر است که در هر دو تعریف صفت DataMember را به ویژگی‌های ناوبری اختصاص نداده ایم و این می‌تواند راهبرد ما در طراحی WCF باشد. ولی اگر می‌خواهید ویژگی ناوبری میان موجودیت‌ها در متدهای ما هم دیده شود ادامه‌ی این درس را بخوانید وگرنه ممکن است تصمیم داشته باشید در صورت نیاز به پیوند میان موجودیت‌ها، متد جدیدی بنویسید و از دستورهای Linq استفاده کنید و یا برای این‌کار از Stored Procedured بهره ببرید.

در اینجا من این سناریو را دنبال می‌کنم که در صورتی که متد GetAllNews اجرا شود؛ بدون این‌که نیاز باشد برای دانستن نام دسته‌ی خبر از متد دیگری مانند GetAllCategory استفاده کنیم؛ رکورد وابسته موجودیت دسته در هر خبر نشان داده شود.

از Solution Explorer فایل MyNewsModel.tt را باز کنید و دنبال کد زیر بگردید:

  public string NavigationProperty(NavigationProperty navigationProperty)
    {
        var endType = _typeMapper.GetTypeName(navigationProperty.ToEndMember.GetEntityType());
        return string.Format(
            CultureInfo.InvariantCulture,
            "{0} {1} {2} {{ {3}get; {4}set; }}",
            AccessibilityAndVirtual(Accessibility.ForProperty(navigationProperty)),
            navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many ? ("ICollection<" + endType + ">") : endType,
            _code.Escape(navigationProperty),
            _code.SpaceAfter(Accessibility.ForGetter(navigationProperty)),
            _code.SpaceAfter(Accessibility.ForSetter(navigationProperty)));
    }

سپس آن‌را به صورت زیر ویرایش کنید:

  public string NavigationProperty(NavigationProperty navigationProperty)
    {
        var endType = _typeMapper.GetTypeName(navigationProperty.ToEndMember.GetEntityType());
        return string.Format(
            CultureInfo.InvariantCulture,
            "{0}{1} {2} {3} {{ {4}get; {5}set; }}",
navigationProperty.ToEndMember.RelationshipMultiplicity != RelationshipMultiplicity.Many ? "[DataMember]" + Environment.NewLine : "",
            AccessibilityAndVirtual(Accessibility.ForProperty(navigationProperty)),
            navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many ? ("ICollection<" + endType + ">") : endType,
            _code.Escape(navigationProperty),
            _code.SpaceAfter(Accessibility.ForGetter(navigationProperty)),
            _code.SpaceAfter(Accessibility.ForSetter(navigationProperty)));
    }  

پس از ذخیره‌ی فایل، خواهید دید که صفت DataMember در کلاس tblNews پیش از ویژگی tblCategory افزوده شده است. بار دیگر پروژه را اجرا کنید. روی متد GetAllNews کلیک کنید و روی دکمه Invoke بفشارید. خواهید دید که هرچند tblCategory در ویژگی‌های آن قرار گرفته است ولی مقدار آن Null است. برای حل این مشکل باید از Solution Explorer فایل MyNewsService.cs را باز کنید و به به جای کد مربوط به متدهای GetAllNews و GetNews کدهای زیر را قرار دهید:

       public List<tblNews> GetAllNews()
        {
            return dbMyNews.tblNews.Include(p=>p.tblCategory).Where(c=>c.IsDeleted == false).ToList(); 
        }

        public tblNews GetNews(int tblNewsId)
        {
            return dbMyNews.tblNews.Include(p => p.tblCategory).FirstOrDefault(p => p.tblNewsId == tblNewsId);
        }

این بار اگر پروژه را اجرا کنید با نتیجه‌ای مانند شکل زیر روبه‌رو خواهید شد:

در بخش هفتم پیرامون میزبانی WCF Library خواهم نوشت.

مطالب
اتصال SQL Server به MySQL

اگر 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;
در اینجا نام دیتابیس پیش فرض، نام کاربری اتصال به MySQL و Password و غیره را می‌توان تنظیم کرد.
پس از انجام این تنظیمات بر روی دکمه‌ی 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 در کنترل پنل که اصلا نیازی به‌ آن نیست و اضافی است.

نظرات مطالب
بررسی تغییرات Blazor 8x - قسمت چهارم - معرفی فرم‌های جدید تعاملی

یک نکته‌ی تکمیلی: روش طراحی binding دو طرفه در Blazor SSR

در نکته‌ی قبل عنوان شد که مقدمات طراحی binding دو طرفه، داشتن حداقل سه خاصیت زیر در یک کامپوننت سفارشی است:

[Parameter] public T? Value { set; get; }

[Parameter] public EventCallback<T?> ValueChanged { get; set; }

[Parameter] public Expression<Func<T?>> ValueExpression { get; set; } = default!;

اگر این خواص را به کامپوننت‌های توکار خود Blazor متصل کنیم (مانند InputBox آن و مابقی آن‌ها)، نیازی به کدنویسی بیشتری ندارند و کار می‌کنند. اما اگر قرار است از یک input ساده‌ی Html ای استفاده کنیم، نیاز است ValueChanged آن‌را اینبار در متد OnInitialized فراخوانی کنیم؛ چون در زمان post-back به سرور است که مقدار آن در اختیار مصرف کننده‌ی کامپوننت قرار می‌گیرد. این مورد، مهم‌ترین تفاوت نحوه‌ی طراحی binding دوطرفه در Blazor SSR با مابقی حالات و نگارش‌های Blazor است.

بررسی وقوع post-back به سرور به دو روش زیر میسر است:

الف) بررسی کنیم که آیا HttpPost ای رخ‌داده‌است؟ سپس در همین لحظه، متد ValueChanged.InvokeAsync را فراخوانی کنیم:

[CascadingParameter] internal HttpContext HttpContext { set; get; } = null!;

protected override void OnInitialized()
{
   base.OnInitialized();

   if (HttpContext.IsPostRequest())
   {
      SetValueCallback(Value);
   }
}

private void SetValueCallback(string value)
{
   if (!ValueChanged.HasDelegate)
   {
      return;
   }

   _ = ValueChanged.InvokeAsync(value);
}

در این مثال نحوه‌ی فعالسازی ارسال اطلاعات از یک کامپوننت سفارشی را به مصرف کننده‌ی آن ملاحظه می‌کنید. اینکار در قسمت OnInitialized و فقط در صورت ارسال اطلاعات به سمت سرور، فعال خواهد شد.

ب) می‌توان در قسمت OnInitialized، بررسی کرد که آیا درخواست جاری به همراه اطلاعات یک فرم ارسال شده‌ی به سمت سرور است یا خیر؟ روش کار به صورت زیر است:

protected override void OnInitialized()
{
   base.OnInitialized();

   if (HttpContext.Request.HasFormContentType &&
       HttpContext.Request.Form.TryGetValue(ValueField.HtmlFieldName, out var data))
   {
      SetValueCallback(data.ToString());
   }
}

در اینجا از ValueField.HtmlFieldName که در نکته‌ی قبلی معرفی BlazorHtmlField به آن اشاره شد، جهت یافتن نام واقعی فیلد ورودی، استفاده شده‌است.