پشتیبانی از JSON در SQL Server 2016
قابلیتهای نسخههای مختلف Sql Server Express 2008
نسخه / قابلیت | Database Engine | Management Studio Basic | Full-Text Search | Reporting Services |
Management Studio Basic | X | |||
Runtime Only | X | |||
with Tools | X | X | ||
with Advanced Services | X | X | X | X |
(SQL Server 2008 Express (Runtime Only
SQL Server 2008 Express with Tools
SQL Server 2008 Express with Advanced Services
دو حالت برای نصب SQL Server وجود دارد:
نصب SQL Server Express 2008 از طریق Command Prompt
Setup.exe /q /Action=Install /Hideconsole /Features=SQL,Tools /InstanceName=SQLExpress /SQLSYSADMINACCOUNTS="Builtin\Administrators" /SQLSVCACCOUNT="<DomainName\UserName>" /SQLSVCPASSWORD="<StrongPassword>
باتوجه به سیاستها نصب میتوانید از پارامترها دیگری نیز استفاده کنید. بعنوان مثال پارامترهای زیر برای نصب روی سیستمی که نام کاربری و پسورد انرا نداریم مناسب است:
setup.exe /q /Action=Install /Features=SQL /InstanceName=SQLExpress /SECURITYMODE=SQL /SAPWD="1234567" /SQLSYSADMINACCOUNTS="Builtin\Administrators" /SQLSVCACCOUNT="NT AUTHORITY\SYSTEM" /SQLSVCSTARTUPTYPE="Automatic" /TCPENABLED=1
Setup.exe /q /Hideconsole /ACTION=upgrade /INSTANCENAME=SQLExpress
برای مشاهده دیگر پارامترها به مستندات MSDN مراجعه کنید. همچنین میتوان نصب از طریق فایل Configuration را نیز انجام داد.
متد زیر هستهی اصلی عملیات است و کلیهی نگاشتهای لازم را انجام میدهد. این متد وظیفهی تبدیل نگاشتها را دارد. نگاشتهایی که با Attributes مشخص شدهاند:
public static void Initialize(Assembly assembly) { //register global convertors. AutoMapper.Mapper.CreateMap<DateTime, string>().ConvertUsing<DateTimeToPersianDateTimeConverter>(); var typesToMap = from t in assembly.GetTypes() let attr = t.GetCustomAttribute<MapFromAttribute>() where attr != null select new {SourceType = attr.SourceType, Destination = t, Attribute = attr}; foreach (var map in typesToMap) { AutoMapper.Mapper.CreateMap(map.SourceType, map.Destination) .DoMapForMemberAttribute() // for different property names in source and destination .DoIgnoreMapAttribute()// ignore specified properties .DoUseValueResolverAttribute()// set value resolvers .DoIgnoreAllNonExisting()// its have to be the latest. ; } //endeach AutoMapper.Mapper.AssertConfigurationIsValid(); }
در سطر اول، اقدام به رجیستر کردن کلیهی مبدلهای سراسری میکنیم. در این سطر مبدل تاریخ به کوچی خورشیدی مورد استفاده قرار گرفته است. سپس در اسمبلی داده شده، کلیه نوعهایی که ویژگی MapFromAttribute را دارند، یافته و جدا میکنیم. در حلقهی foreach ابتدا نگاشت نوع مبدأ و مقصد را انجام میدهیم. خروجی این متد از نوع IMappingExpression است. گر چه این اینترفیس برای تغییر بسته است، ولی قابل توسعه میباشد و عملیات را توسط متدهای الحاقی انجام میدهیم(اصل OCP).
اگر به نحوهی نامگذاری متدهای الحاقی تعریف شده دقت کرده باشید، تنها کلمهی Do به ابتدای نام ویژگیها اضافه شده است.
متد الحاقی DoMapFormMemberAttribute
public static IMappingExpression DoMapForMemberAttribute(this IMappingExpression expression) { var ok = from p in expression.TypeMap.DestinationType.GetProperties() let attr = p.GetCustomAttribute<MapForMemberAttribute>() where attr != null select new {AttributeValue = attr, PropertyName = p.Name}; foreach (var property in ok) { expression.ForMember(property.PropertyName, opt => opt.MapFrom(property.AttributeValue.MemberToMap)); } return expression; }
متد الحاقی DoIgnoreMapAttribute
public static IMappingExpression DoIgnoreAttribute(this IMappingExpression expression) { foreach (var property in expression.TypeMap.DestinationType.GetProperties() .Where(x => x.GetCustomAttribute<IgnoreMapAttribute>() != null)) { expression.ForMember(property.Name, opt => opt.Ignore()); } return expression; }
متد الحاقی DoUseValueResolverAttribute
public static IMappingExpression DoUseValueResolverAttribute(this IMappingExpression expression) { var ok = from p in expression.TypeMap.DestinationType.GetProperties() let attr = p.GetCustomAttribute<UseValueResolverAttribute>() where attr != null select new {AttributeValue = attr, PropertyName = p.Name}; foreach (var property in ok) { expression.ForMember(property.PropertyName, opt => opt.ResolveUsing(property.AttributeValue.ValueResolver)); } return expression; }
متد الحاقی DoIgnoreAllNonExisting
public static IMappingExpression DoIgnoreAllNonExisting(this IMappingExpression expression) { var attr = expression.TypeMap.DestinationType.GetCustomAttribute<MapFromAttribute>(); if (attr?.IgnoreAllNonExistingProperty == false)//instead of if(attr == null || attr.IgnoreAllNonExistingProperty == false) return expression; foreach (var property in expression.TypeMap.GetUnmappedPropertyNames()) { expression.ForMember(property, opt => opt.Ignore()); } return expression; }
توضیح تکمیلی: پس از تنظیم کلیهی نگاشتها در automapper جهت اطمینان از صحت تنظیمات، فراخوانی متد AutoMapper.Mapper.AssertConfigurationIsValid الزامی است. یکی از عواملی که باعث شکست این متد میشود، وجود پروپرتیهایی در نوع مقصد است، بطوریکه معادل اسمی در نوع مبدأ نداشته باشند و یا تنظیمی جهت مشخص سازی نگاشت آن انجام نشده باشد (پروپرتی که قابل نگاشت نباشد). در حقیقت این شکست بسیار مفید است. به این صورت که اگر این شکست صورت نگیرد در حین نگاشت مقادیر، باید از null یا مقدار default بدون اطلاع برنامه نویس برای مقداردهی پروپرتی استفاده کند و این یک حالت نامعلوم شیء است. اگر میخواهید این پروپرتیها مقدار پیشفرضی بگیرند و همچنین باعث شکست عملیات هم نشوند، باید بطور صریح این موضوع را اعلام کنید. این اعلام یا باید به همین روش صورت بگیرد یا باید از ویژگی IgnorMapAttribute استفاده شود. تنها تفاوت این دو، نحوهی اعمال تنظیم میباشد. IgnorMapAttribute باید روی تک تک پروپرتیهای مدنظر قرار گیرد، ولی در روش اول تنها کافیست که مقدار true تنظیم گردد. بهنظر استفاده از IgnoreMapAttribute باعث طولانی شدن کدها میشود؛ اما توصیه میشود که از همین شیوه استفاده کنید.
تا اینجا کدهای مورد نیاز نوشته شدند. در ادامه به ارائهی یک مثال برای نگاشت اشیاء در Automapper توسط Attributeها میپردازم.
مدل سادهی زیر را در نظر بگیرید:
public class Student { public virtual int Id { set; get; } public virtual string Name { set; get; } public virtual string Family { set; get; } public virtual string Email { set; get; } public virtual DateTime RegisterDateTime { set; get; } public virtual ICollection<Book> Books { set; get; } } public class Book { public virtual int Id { set; get; } public virtual string Name { set; get; } public virtual DateTime BorrowDateTime { set; get; } public virtual DateTime ExpiredDateTime { set; get; } public virtual decimal Price { set; get; } [ForeignKey("StudentIdFk")] public virtual Student Student { set; get; } public virtual int StudentIdFk { set; get; } }
[MapFrom(typeof (Student), ignoreAllNonExistingProperty: true, alsoCopyMetadata: true)] public class AdminStudentViewModel { // [IgnoreMap] public int Id { set; get; } [MapForMember("Name")] public string FirstName { set; get; } [MapForMember("Family")] public string LastName { set; get; }
[IgnoreMap] public string Email { set; get; } [MapForMember("RegisterDateTime")] public string RegisterDateTimePersian { set; get; } [UseValueResolver(typeof (BookCountValueResolver))] public int BookCounts { set; get; } [UseValueResolver(typeof (BookPriceValueResolver))] public decimal TotalBookPrice { set; get; } };
public class BookCountValueResolver : ValueResolver<Student, int> { protected override int ResolveCore(Student source) => source.Books.Count; }; public class BookPriceValueResolver : ValueResolver<Student, decimal> { protected override decimal ResolveCore(Student source) => source.Books.Sum(b => b.Price); };
static void Main(string[] args) { var assemblyToLoad = Assembly.GetAssembly(typeof (AdminStudentViewModel));//get assembly global::AttributesForAutomapper.Configuration.Initialize(assemblyToLoad);//init automaper IList<Student> lst; using (var context = new MySampleContext()) { lst = context.Students.Include(x => x.Books).ToList(); } foreach (var student in lst) { WriteLine( $"[{student.Id}]*\n{student.Name} {student.Family}.\nmailto:{student.Email}.\nRegistered at'{student.RegisterDateTime}'"); foreach (var book in student.Books) WriteLine($"\tBook name:{book.Name}, Book price:{book.Price}"); } var lstViewModel = AutoMapper.Mapper.Map<IList<Student>, IList<AdminStudentViewModel>>(lst); foreach (var adminStudentViewModel in lstViewModel) { WriteLine( $"[{adminStudentViewModel.Id}]*\n\t{adminStudentViewModel.FirstName} {adminStudentViewModel.LastName}.\n\t" + $"mailto:{adminStudentViewModel.Email}.\n\tRegistered at'{adminStudentViewModel.RegisterDateTimePersian}'\n\t" + $"Book Counts: {adminStudentViewModel.BookCounts} with total price of {adminStudentViewModel.TotalBookPrice}"); } WriteLine("Press any key to exit..."); ReadKey(); }
نمونهی خروجی:
[1]* Morteza Raeisi. mailto:MrRaeisi@outlook.com. Registered at'23/08/1392 19:11:43' // I'm using Windows 10 with Persian calendar as default, On other OS or calendar settings, this value is different. Book name:AutoMapper Attr, Book price:1000.00 Book name:Second Book, Book price:2500.00 Book name:Hungry Book, Book price:2500.00 ... [1]* Morteza Raeisi. //MapForMemebers mailto:. // IgnoreMap Registered at'1392/08/23 19:11' // Convert using Book Counts: 3 with total price of 6000.00 // Value resolvers ...
فعال سازی سطح دوم کش در Fluent NHibernate
کلا سطح دوم کش در NH بر اساس 4 مکانیزم در طول یک سشن فکتوری عمل میکند:
- کش مربوط به موجودیتها (entities cache) که بر اساس متد session.Get یا Load فعال میشود
و همچنین Collections cache (متدهای List و Enumerable)
- کش مربوط به کوئریها (queries cache) با اعمال متد Cacheable به کوئری مورد نظر.
- timestamp cache که به معنای آخرین زمان نوشتن در یک جدول میباشد (و این timestamp فقط و فقط بر اساس وجود تراکنشها عمل میکند). به این ترتیب در زمان insert/update/delete به صورت خودکار کش موجود منقضی اعلام میشود تا اطلاعات قدیمی به کاربر تحویل داده نشود و کش سطح دوم جهت کوئریهای بعدی بازسازی خواهد شد.
و در مثال شما:
- در کوئری دوم هم باید متد Cacheable ذکر شود اگر نشود به صورت متداول با آن برخورد خواهد شد.
- زمانیکه از متد Cacheable استفاده میشود، حالت queries cache فعال میشود. چون در مثال شما دو کوئری مختلف داریم، پس به کش مربوط به کوئری اول مراجعه نخواهد شد. این کوئری کش، با تغییر مقادیر پارامترهای یک کوئری هم مجددا به روز میشود. (این حالت برای کوئریهایی که با پارامترهای یکسان به طور متناوب فراخوانی میشوند، بسیار مناسب است)
- سطح دوم کش فقط پس از commit یک تراکنش معنا پیدا میکند. بنابراین اگر جهت آزمایش داخل یک تراکنش، پشت سر هم کوئریها را نوشتهاید ... در این لحظه از سطح دوم کش بیبهره خواهید بود (فقط سطح اول کش فعال است) و کوئریهای پس از پایان تراکنش جاری، از نتیجه کش آن میتوانند استفاده کنند.
- در مورد کش مربوط به موجودیتها و تفاوت آن با کش کوئریها در بالا صحبت شد (شما در یک جا کش کوئری را فعال کردهاید در جای دیگر کش entities را طلب میکنید که نمیشود).
تقسیم جدول در Entity Framework Code First
INSERT INTO tableName VALUES ( '{ "name": "User1", "age": 41 }' );
INSERT INTO tableName VALUES ( JSON_ARRAY( JSON_OBJECT( "id", 1, "name", "User1", "age", 31, "skills", JSON_ARRAY("JS", "DB", "Git"), "address", JSON_OBJECT( "country", "Iran", "city", "Tehran") ), JSON_OBJECT( "id", 2, "name", "User2", "age", 31, "skills", JSON_ARRAY("C#"), "address", JSON_OBJECT( "country", "Iran", "city", "Sanandaj" ) ) ) );
UPDATE experiments.tableName SET jsonData = JSON_ARRAY_APPEND(jsonData, '$[1].skills', 'JS', '$[1].skills', 'DB', '$[1].skills', 'Kotlin' ) -- ["C#", "JS", "DB", "Kotlin"]
UPDATE experiments.tableName SET jsonData = JSON_ARRAY_INSERT(jsonData, '$[1].skills[4]', 'TS') -- ["C#", "JS", "DB", "Kotlin", "TS"]
UPDATE experiments.tableName SET jsonData = JSON_INSERT(jsonData, '$[1].address.location', JSON_OBJECT('phone', 8989898))
UPDATE experiments.tableName SET jsonData = JSON_REPLACE(jsonData, '$[1].address.location.phone', 12345656)
UPDATE experiments.tableName SET jsonData = JSON_REMOVE(jsonData, '$[1].address')
UPDATE experiments.tableName SET jsonData = JSON_SET(jsonData, '$[1].address', JSON_OBJECT('country', 'Iran', 'city', '-', 'phone', 12345 )); /* { location: { "city": "-", "phone": 12345, "country": "Iran" } } */ UPDATE experiments.tableName SET jsonData = JSON_SET(jsonData, '$[1].address.city', 'Tehran'); /* { location: { "city": "-", "phone": 12345, "country": "Iran" } } */ UPDATE experiments.tableName SET jsonData = JSON_SET(jsonData, '$[1].address.postcode', '0098'); /* { location: {"city": "Tehran", "phone": 12345, "country": "Iran", "postcode": '0098' } } */
SELECT JSON_EXTRACT(jsonData, '$[1].address.city') FROM experiments.tableName; -- "Tehran" SELECT JSON_UNQUOTE(JSON_EXTRACT(jsonData, '$[1].address.city')) FROM experiments.tableName; -- Tehran
SELECT jsonData -> '$[1].address.city' FROM experiments.tableName; -- "Tehran"
SELECT jsonData ->> '$[1].address.city' FROM experiments.tableName; -- Tehran
SELECT jsonData ->> '$[1].address.city' FROM experiments.tableName WHERE jsonData ->> '$[1].address.city' = 'Tehran';
SELECT JSON_MERGE_PRESERVE('{ "id": "1", "name": "Product One", "price": 12.45, "discount": 10, "rating": 4, "category": ["fashion", "men"], "tags": ["fashion", "men", "jacket", "full sleeve"] }', '{ "id": "2", "name": "Product Two", "price": 30, "discount": 0, "rating": 3, "category": ["fashion", "men"], "tags": ["fashion", "men", "jacket", "full sleeve"] }');
{ "id": ["1", "2"], "name": ["Product One", "Product Two"], "tags": [ "fashion", "men", "jacket", "full sleeve", "fashion", "men", "jacket", "full sleeve" ], "price": [12.45, 30], "rating": [4, 3], "category": ["fashion", "men", "fashion", "men"], "discount": [10, 0] }
SELECT JSON_MERGE_PATCH('{ "id": "1", "name": "Product One", "price": 12.45, "discount": 10, "rating": 4, "category": ["fashion", "men"], "tags": ["fashion", "men", "jacket", "full sleeve"], "sku": "asdf123" }', '{ "id": "2", "name": "Product Two", "price": 30, "discount": 0, "rating": 3, "category": ["fashion", "men"], "tags": ["fashion", "men", "jacket", "full sleeve"] }');
{ "id": "2", "sku": "asdf123", "name": "Product Two", "tags": ["fashion", "men", "jacket", "full sleeve"], "price": 30, "rating": 3, "category": ["fashion", "men"], "discount": 0 }
;WITH cteBed ([Counter], id_doc , [Year] ,id_Total , date_duc ,Number_Temp , number_fix , sumbed , sumbes , row_no ) AS ( SELECT [Counter], d.id_doc , d.[Year] ,r.id_Total , d.date_duc ,d.Number_Temp ,d.number_fix , SUM( r.Mablagh_bed) OVER(PARTITION BY d.[Year] ,r.id_Total , d.Number_Temp) AS sumbed , sumbes= 0, ROW_NUMBER() OVER (PARTITION BY d.[Year] ,r.id_Total , d.date_duc , d.Number_Temp , d.number_fix ORDER BY d.date_duc )AS row_no FROM tbl_Records r JOIN tbl_Documents d ON d.id_doc = r.id_doc ) , cteBes ([Counter], id_doc , [Year] ,id_Total , date_duc ,Number_Temp , number_fix , sumbed , sumbes , row_no) AS ( SELECT [Counter], d.id_doc , d.[Year] ,r.id_Total , d.date_duc ,d.Number_Temp ,d.number_fix , sumbed = 0 , SUM( r.Mablagh_bes ) OVER(PARTITION BY d.[Year] ,r.id_Total , d.Number_Temp ) AS sumbes, ROW_NUMBER() OVER (PARTITION BY d.[Year] ,r.id_Total , d.date_duc ,d.Number_Temp , d.number_fix ORDER BY d.date_duc )AS row_no FROM tbl_Records r JOIN tbl_Documents d ON d.id_doc = r.id_doc ) SELECT [Counter], id_doc , [Year] ,id_Total , date_duc ,Number_Temp , number_fix , sumbed , sumbes , amountBed ,amountBes ,SUM(amountBed)OVER( ORDER BY [Year] ,id_Total , date_duc , number_Temp, number_Fix ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS bed ,SUM(amountBes)OVER( ORDER BY [Year] ,id_Total , date_duc , number_Temp, number_Fix ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS bes FROM ( SELECT [Counter], id_doc , [Year] ,id_Total , date_duc ,Number_Temp , number_fix , sumbed , sumbes , amountBed = CASE WHEN id_Total LIKE '1%' OR Id_Total LIKE '2%' OR Id_Total LIKE '7%' OR Id_Total LIKE '8%' THEN (tt.sumbed-tt.sumbes) ELSE 0 END , amountBes=CASE WHEN Id_Total LIKE '3%' OR Id_Total LIKE '4%' OR Id_Total LIKE '5%' OR Id_Total LIKE '6%' OR Id_Total LIKE '9%' THEN (tt.sumbes-tt.sumbed)ELSE 0 END , ROW_NUMBER() OVER (PARTITION BY [Year] ,id_Total , date_duc , Number_Temp , number_fix ORDER BY date_duc )AS row_no FROM ( SELECT * FROM cteBed cb WHERE cb.row_no = 1 UNION ALL SELECT * FROM cteBes cb WHERE cb.row_no = 1 ) AS tt ([Counter], id_doc , [Year] ,id_Total , date_duc ,Number_Temp , number_fix , sumbed , sumbes,row_no ) WHERE not(sumbed = 0 AND sumbes = 0) ) AS rr
Roslyn #3
زمانیکه صحبت از Syntax میشود، منظور نمایش متنی سورس کدها است. برای بررسی و آنالیز آن، نیاز است این نمایش متنی، به ساختار دادهای ویژهای به نام Syntax tree تبدیل شود و این Syntax tree مجموعهای است از tokenها. Tokenها بیانگر المانهای مختلف یک زبان، شامل کلمات کلیدی، عملگرها و غیره هستند.
در تصویر فوق، مراحل تبدیل یک قطعه کد #C را به مجموعهای از tokenهای معادل آن مشاهده میکنید. علاوه بر اینها، Roslyn syntax tree شامل موارد ویژهای به نام Trivia نیز هست. برای مثال در حین نوشتن کدها، در ابتدای سطرها تعدادی space یا tab وجود دارند و یا در این بین ممکن است کامنتی نوشته شود. هرچند این موارد از دیدگاه یک کامپایلر بیمعنا هستند، اما ابزارهای Refactoring ایی که به Trivia دقت نداشته باشند، خروجی کد به هم ریختهای را تولید خواهند کرد و سبب سردرگمی استفاده کنندگان میشوند.
در تصویر فوق، اشارهگر ادیتور پس از تایپ semicolon قرار گرفتهاست. در این حالت میتوانید دو نوع trivia مخصوص فضای خالی و کامنتها را در syntax visualizer، مشاهده کنید.
به علاوه پس از هر token بازهای از اعداد را مشاهده میکنید که بیانگر محل قرارگیری آنها در سورس کد هستند. این محلها جهت ارائهی خطاهای دقیق مرتبط با آن نقاط، بسیار مفید هستند.
یک Syntax tree از مجموعهای از syntax nodes تشکیل میشود و هر node شامل مواردی مانند تعاریف، عبارات و امثال آن است. در افزونهی Syntax visualizer نودهایی که رنگ قرمز متمایل به قهوهای دارند، بیانگر نودهای Trivia، نودهای آبی، Syntax nodes و نودهای سبز، Syntax token هستند.
مفاهیم این رنگها را با کلیک بر روی دکمهی Legend هم میتوان مشاهده کرد.
تفاوت Syntax با Semantics
در Roslyn امکان کار با Syntax و Semantics کدها وجود دارد.
یک Syntax، از گرامر زبان خاصی پیروی میکند. در Syntax اطلاعات بسیار زیادی وجود دارند که معنای برنامه را تغییر نمیدهند؛ مانند کامنتها، فضاهای خالی و فرمت ویژهی کدها. البته فضاهای خالی در زبانهایی مانند پایتون دارای معنا هستند؛ اما در سیشارپ خیر. همچنین در Syntax، توافق نامهای وجود دارد که بیانگر تعدادی واژهی از پیش رزرو شده، مانند کلمات کلیدی هستند.
اما Semantics در نقطهی مقابل Syntax قرار میگیرد و بیانگر معنای سورس کد است. برای مثال در اینجا تقدم و تاخر عملگرها مفهوم پیدا میکنند و یا اینکه Type system چیست و چه نوعهایی را میتوان به دیگری نسبت داد و تبدیل کرد. عملیات Binding در این مرحله رخ میدهد و مفهوم identifierها را مشخص میکند. برای مثال x در این قسمت از سورس کد، به چه معنایی است و به کجا اشاره میکند؟
خواص ویژهی Syntax tree در Roslyn
- تمام اجزای کد را شامل عناصر سازندهی زبان و همچنین Trivia، به همراه دارد.
- API آن توسط کتابخانههای ثالث قابل دسترسی است.
- Immutable طراحی شدهاست. به این معنا که زمانیکه syntax tree توسط Roslyn ایجاد شد، دیگر تغییر نمیکند. به این ترتیب امکان دسترسی همزمان و موازی به آن بدون نیاز به انواع قفلهای مسایل همزمانی وجود دارد. اگر کتابخانهی ثالثی به Syntax tree ارائه شده دسترسی پیدا میکند، میتواند کاملا مطمئن باشد که این اطلاعات دیگر تغییری نمیکنند و نیازی به قفل کردن آنها نیست. همچنین این مساله امکان استفادهی مجدد از sub treeها را در حین ویرایش کدها میسر میکند. به آنها mutating trees نیز گفته میشود.
- مقاوم است در برابر خطاها. اگر از قسمت اول به خاطر داشته باشید، Roslyn میبایستی جایگزین کامپایلر دومی به نام کامپایلر پس زمینهی ویژوال استودیو که خطوط قرمزی را ذیل سطرهای مشکل دار ترسیم میکند، نیز میشد. فلسفهی طراحی این کامپایلر، مقاوم بودن در برابر خطاهای تایپی و هماهنگی آن با تایپ کدها توسط برنامه نویس بود. Syntax tree در Roslyn نیز چنین خاصیتی را دارد و اگر مشغول به تایپ شوید، باز هم کار کرده و اینبار خطاهای موجود را نمایش میدهد که میتواند توسط ابزارهای نمایش دهندهی ویژوال استودیو یا سایر ابزارهای ثالث استفاده شود.
برای نمونه در تصویر فوق، تایپ semicolon فراموش شدهاست؛ اما همچنان Syntax tree در دسترس است و به علاوه گزارش میدهد که semicolon مفقود است و تایپ نشدهاست.
Parse سورس کد توسط Roslyn
ابتدا یک پروژهی کنسول سادهی دات نت 4.6 را در VS 2015 آغاز کنید. سپس از طریق خط فرمان نیوگت، دستور ذیل را صادر نمائید:
PM> Install-Package Microsoft.CodeAnalysis
سپس کدهای ذیل را به آن اضافه کنید:
using System; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Roslyn01 { class Program { static void Main(string[] args) { parseText(); } static void parseText() { var tree = CSharpSyntaxTree.ParseText("class Foo { void Bar(int x) {} }"); Console.WriteLine(tree.ToString()); Console.WriteLine(tree.GetRoot().NormalizeWhitespace().ToString()); var res = SyntaxFactory.ClassDeclaration("Foo") .WithMembers(SyntaxFactory.List<MemberDeclarationSyntax>(new[] { SyntaxFactory.MethodDeclaration( SyntaxFactory.PredefinedType( SyntaxFactory.Token(SyntaxKind.VoidKeyword) ), "Bar" ) .WithBody(SyntaxFactory.Block()) })) .NormalizeWhitespace(); Console.WriteLine(res); } } }
کار Parse سورس کد دریافتی، بر اساس سرویسهای زبان متناظر با آنها آغاز میشود. برای مثال سرویسهایی مانند VisualBasicSyntaxTree و یا CSharpSyntaxTree مثال فوق که سورس کد مورد آنالیز آن، از نوع سیشارپ است.
این کلاسهای Factory، دارای دو متد Create و ParseText هستند. کار متد ParseText آن مشخص است؛ یک قطعهی متنی از کد را آنالیز کرده و معادل Syntax Tree آنرا تولید میکند. متد Create آن، اشیایی مانند نودهای Syntax visualizer را دریافت کرده و بر اساس آنها یک Syntax tree را تولید میکند.
کار با متد Create آنچنان ساده نیست. به همین جهت یکی از اعضای تیم Roslyn برنامهای را به نام Roslyn Quoter ایجاد کردهاست که نسخهی آنلاین آنرا در اینجا و سورس کد آنرا در اینجا میتوانید بررسی کنید.
جهت آزمایش، همان قطعهی متنی سورس کد مثال فوق را در نسخهی آنلاین آن جهت آنالیز و تولید ورودی متد Create، وارد کنید. خروجی آنرا میتوان مستقیما در متد Create بکار برد.
فرمت کردن خودکار کدها به کمک Roslyn
اگر بر روی tree حاصل، متد ToString را فراخوانی کنیم، خروجی آن مجددا سورس کد مورد آنالیز است. اگر علاقمند بودید که Roslyn به صورت خودکار کدهای ورودی را فرمت کند و تمام آنها را در یک سطر نمایش ندهد، متد NormalizeWhitespace را بر روی ریشهی Syntax tree فراخوانی کنید:
tree.GetRoot().NormalizeWhitespace().ToString()
class Foo { void Bar(int x) { } }
کوئری گرفتن از سورس کد توسط Roslyn
در ادامه قصد داریم با سه روش مختلف کوئری گرفتن از Syntax tree، آشنا شویم. برای این منظور متد ذیل را به پروژهای که در ابتدای برنامه آغاز کردیم، اضافه کنید:
static void querySyntaxTree() { var tree = CSharpSyntaxTree.ParseText("class Foo { void Bar() {} }"); var node = (CompilationUnitSyntax)tree.GetRoot(); // Using the object model foreach (var member in node.Members) { if (member.Kind() == SyntaxKind.ClassDeclaration) { var @class = (ClassDeclarationSyntax)member; foreach (var member2 in @class.Members) { if (member2.Kind() == SyntaxKind.MethodDeclaration) { var method = (MethodDeclarationSyntax)member2; // do stuff } } } } // Using LINQ query methods var bars = from member in node.Members.OfType<ClassDeclarationSyntax>() from member2 in member.Members.OfType<MethodDeclarationSyntax>() where member2.Identifier.Text == "Bar" select member2; var res = bars.ToList(); // Using visitors new MyVisitor().Visit(node); }
روش اول کوئری گرفتن از Syntax tree، استفاده از object model آن است. در اینجا هربار، نوع و Kind هر نود را بررسی کرده و در نهایت به اجزای مدنظر خواهیم رسید. شروع کار هم با دریافت ریشهی syntax tree توسط متد GetRoot و تبدیل نوع آن نود به CompilationUnitSyntax میباشد.
روش دوم استفاده از روش LINQ است؛ با توجه به اینکه ساختار یک Syntax tree بسیار شبیه است به LINQ to XML. در اینجا یک سری نود، ریشه و فرزندان آنها را داریم که با روش LINQ بسیار سازگار هستند. برای نمونه در مثال فوق، در ریشهی Parse شده، در تمام کلاسهای آن، به دنبال متد یا متدهایی هستیم که نام آنها Bar است.
و در نهایت روش مرسوم و متداول کار با Syntax trees، استفاده از الگوی Visitors است. همانطور که در کدهای دو روش قبل مشاهده میکنید، باید تعداد زیادی حلقه و if و else نوشت تا به جزء و المان مدنظر رسید. راه سادهتری نیز برای مدیریت این پیچیدگی وجود دارد و آن استفاده از الگوی Visitor است. کار این الگو ارائهی متدهایی قابل override شدن است و فراخوانی آنها، در طی حلقههایی پشت صحنه که این Visitor را اجرا میکنند، صورت میگیرد. بنابراین در اینجا دیگر برای رسیدن به یک متد، حلقه نخواهید نوشت. تنها کاری که باید صورت گیرد، override کردن متد Visit المانی خاص در Syntax tree است.
هر نود در syntax tree دارای متدی است به نام Accept که یک Visitor را دریافت میکند. همچنین Visitorهای نوشته شده نیز دارای متد Visit یک نود هستند.
نمونهای از این Visitors را در کلاس ذیل مشاهده میکنید:
class MyVisitor : CSharpSyntaxWalker { public override void VisitMethodDeclaration(MethodDeclarationSyntax node) { if (node.Identifier.Text == "Bar") { // do stuff } base.VisitMethodDeclaration(node); } }
کلاس پایهی CSharpSyntaxWalker از کلاس CSharpSyntaxVisitor مشتق شدهاست و به تمام امکانات آن دسترسی دارد. علاوه بر آنها، کلاس CSharpSyntaxWalker به Tokens و Trivia نیز دسترسی دارد.
نحوهی استفاده از Visitor سفارشی نوشته شده نیز به صورت ذیل است:
new MyVisitor().Visit(node);
انجام عملیات Pivot توسط MDX :
برای این منظور کافی است فقط جای سطر و ستون را با هم عوض کنیم.برای مثال، کوئریهای زیر را اجرا نمایید.
Select [Date].[Calendar].[Calendar Year] on columns, [Product].[Product Categories].[Category] on rows From [Adventure Works] GO Select [Product].[Product Categories].[Category] on columns, [Date].[Calendar].[Calendar Year] on rows From [Adventure Works]
خروجی به صورت زیر میباشد.
چگونگی استفاده از ساختارهای سلسله مراتبی در محورهای مختلف :
برای روشن شدن مطلب چندین نمونه کوئری زیر را باهم اجرا میکنیم.
در ابتدا به خاطر داشته باشید که امکان استفاده از یک ساختار سلسله مراتبی در دو محور (Axis ) مجزا وجود ندارد. برای روشنتر شدن این مطلب کوئری زیر را اجرا کنید:
Select [Date].[Calendar].[Calendar Year] on columns, [Date].[Calendar].[Month] on rows From [Adventure Works]
در توضیح مثال بالا دقت داشته باشید که واکشی [Calendar Year] و [Month] از یک ساختار سلسله مراتبی یکسان به نام [Date].[Calendar] انجام شده است و این کار در MDX ها غیر مجاز میباشد.
البته استفاده از دو ساختار سلسله مراتبی متفاوت از یک دایمنشن در دو محور مجزا امکان پذیر میباشد برای مثال :
Select [Date].[Calendar].[Calendar Year] on columns, [Date].[Month of Year].[Month of Year] on rows From [Adventure Works]
در مثال بالا واکشی از دو ساختار سلسله مراتبی مختلف اما از یک دایمنشن در دو محور مجزا صورت گرفته است.
امکان استفاده از دو ساختار سلسله مراتبی یکسان در یک محور مجاز میباشد.
Select [Measures].[Internet Sales Amount] on columns, {[Date].[Calendar].[Calendar Year],[Date].[Calendar].[Month]} on rows From [Adventure Works]
همچنین استفاده از دو ساختار سلسله مراتبی مختلف از یک دایمنشن در یک محور مجاز نمیباشد.
Select [Measures].[Internet Sales Amount] on columns, {[Date].[Calendar].[Calendar Year],[Date].[Month of Year].[Month of Year]} on rows From [Adventure Works]
در مثال بالا از دو ساختار سلسله مراتبی [Date].[Calendar] و [Date].[Month of Year] استفاده شده است.
در صورتی میتوانیم از دو ساختار سلسله مراتبی متفاوت از یک دایمنشن در یک محور استفاده کنیم که آنها را باهم Cross Join کنیم.
Select [Measures].[Internet Sales Amount]on columns, crossjoin( [Date].[Calendar].[Calendar Year], [Date].[Month of Year].[Month of Year] )on rows From [Adventure Works]
دقت داشته باشید که امکان استفاده از دو ساختار سلسله مراتبی یکسان از یک دایمنشن در Cross Join وجود ندارد همان گونه که در دو مثال قبل مشاهده نمودید. (و البته نیازی هم به استفاده از Cross Join وجود ندارد)
Select [Measures].[Internet Sales Amount] on columns, crossjoin( [Date].[Calendar].[Calendar Year], [Date].[Calendar].[Month] )on rows From [Adventure Works]
به طور کلی استفاده از Cross Join مانند استفاده از () می باشد . بنابر این می توان کلمه ی Cross Join را ننوشت .
Select {[Measures].[Internet Sales Amount],[Measures].[Reseller Sales Amount]} on columns, crossjoin( [Date].[Calendar].[Calendar Year], [Date].[Month of Year].[Month of Year] )on rows From [Adventure Works] GO Select {[Measures].[Internet Sales Amount],[Measures].[Reseller Sales Amount]} on columns, ( [Date].[Calendar].[Calendar Year], [Date].[Month of Year].[Month of Year] )on rows From [Adventure Works]
دو کوئری بالا مشابه هم میباشند.
می توان از دو Cross Join در دو محور مختلف نیز استفاده کرد.
Select crossjoin( [Product].[Product Categories].[Category], {[Measures].[Internet Sales Amount],[Measures].[Reseller Sales Amount]} )on columns, crossjoin( [Date].[Calendar].[Calendar Year], [Date].[Month of Year].[Month of Year] )on rows From [Adventure Works]
دقت داشته باشید نوع محصول در ستونها بالای سر ستون [ Internet Sales Amount] و [Reseller Sales Amount] قرار گرفته است.
استفاده از Cross Join های تودر تو نیز وجود دارد.
Select crossjoin( [Sales Territory].[Sales Territory].[Country], crossjoin( [Product].[Product Categories].[Category], { [Measures].[Internet Order Count], [Measures].[Reseller Order Count] } ) )on columns , crossjoin( [Date].[Calendar].[Calendar Year], [Date].[Month of Year].[Month of Year] ) on rows From [Adventure Works]
در کوئری زیر به جای Cross Join از () استفاده شده است
Select crossjoin( [Sales Territory].[Sales Territory].[Country], [Product].[Product Categories].[Category], { [Measures].[Internet Order Count], [Measures].[Reseller Order Count] } ) on columns, ([Date].[Calendar].[Calendar Year],[Date].[Month of Year].[Month of Year]) on rows From [Adventure Works]
استفاده از عملگر * مانند استفاده از Cross Join می باشد.
Select crossjoin( [Sales Territory].[Sales Territory].[Country], [Product].[Product Categories].[Category], { [Measures].[Internet Order Count], [Measures].[Reseller Order Count] } ) on columns, [Date].[Calendar].[Calendar Year] * [Date].[Month of Year].[Month of Year] on rows From [Adventure Works]
در مقالات بعدی آموزش MDX Query را ادامه خواهیم داد.