محدودیتهای DAX با روابط غیرفعال Inactive و Row Level Security (RLS)
توابع USERELATIONSHIP
یکی از توابع رایج و مفید در DAX هستند که زمانی استفاده میشوند که بین جداول مختلف روابط متعددی وجود دارد و توسعهدهندگان باید تصمیم بگیرند که کدام رابطه را استفاده کنند. با این حال، در برخی سناریوها، این تابع رایج خطای آزاردهندهای ایجاد میکند:
توابع
UseRelationship()
وCrossFilter()
ممکن است زمانی که در حال پرسوجو از جدول ‘Sales’ هستید استفاده نشوند، زیرا این جدول محدود به امنیت ردیف سطحی است.
همانطور که در تمامی پیامهای خطا، این مسئله نیاز به درک و توضیح بیشتر دارد. علاوه بر این، راهحل جایگزین به راحتی پیدا میشود. با این حال، این راهحل محدودیتهای ظریفی دارد که باید به خوبی درک شوند.
معرفی رابطه غیرفعال
ما یک مدل معنایی داریم که تاریخ به عنوان بعد نقش در نظر گرفته شده است: جدول تاریخ به هر دو فیلد “تاریخ سفارش” و “تاریخ تحویل” در جدول فروش (Sales) ارجاع دارد. بنابراین، دو رابطه بین جدول تاریخ و جدول فروش وجود دارد.

ما میخواهیم یک گزارش ساده تولید کنیم که برای هر سال، مقدار سفارش دادهشده و مقدار تحویل دادهشده را نشان دهد. بنابراین، ابتدا دو معیار (measure) زیر را تعریف میکنیم:

مژر Delivered Amount به درستی کار میکند: این معیار از تابع USERELATIONSHIP
برای فعالسازی رابطه غیرفعال بین جدول فروش (Sales) و جدول تاریخ (Date) استفاده میکند، به طوری که رابطه براساس فیلد Sales[Delivery Date] به جای Sales[Order Date] محاسبه میشود.

قبل از ادامه، لطفاً توجه داشته باشید که از نسخهای کمی تغییر یافته از پایگاه داده استاندارد Contoso استفاده میکنیم. ما تمام سفارشهای ثبتشده در سال 2020 را حذف کردهایم و زمان تحویل را افزایش دادهایم. در واقع، هیچ فروشی در سال 2020 وجود ندارد، با وجود اینکه چندین سفارش تحویل داده شدهاند. اینها سفارشهایی هستند که در سال 2019 ثبت شدهاند و در سال 2020 تحویل داده شدهاند. این جزئیات بعداً در مقاله مفید خواهد بود. به یاد داشته باشید که ما در این مثال از جدول تاریخ (Date) استفاده میکنیم، اما همین مشکل ممکن است زمانی که امنیت در جداول دیگر مانند محصول (Product) و مشتری (Customer) اعمال میشود نیز پیش بیاید.
معرفی مدل امنیتی
ما ممکن است با یک مشکل جدی مواجه شویم اگر امنیت ردیف سطحی (RLS) را در مدل معنایی Semantic فعال کنیم. برای نمایش این موضوع، ما الزامی اضافه کردیم که همه کاربران مجاز به مشاهده دادهها برای تمام سالها نباشند. هر کاربر با مجموعهای از سالهای مجاز به مشاهده دادهها مرتبط است. ما میتوانیم این الزامات را به راحتی از طریق اضافه کردن دو جدول اضافی پیادهسازی کنیم.

ما جدول Users را ایمن میکنیم به طوری که فقط کاربرانی نمایش داده شوند که UserId آنها برابر با USERPRINCIPALNAME باشد و از UserYears برای فیلتر کردن جدول تاریخ (Date) استفاده میکنیم.

جدول UserYears به ما کمک میکند تا یک کاربر را به سالهای متعدد مرتبط کنیم. در مثال ما، هم Marco و هم Alberto میتوانند دو سال را مشاهده کنند.

بیایید به سرعت تنظیمات امنیتی را مرور کنیم. جدول Users ایمن شده است؛ فیلتر از جدول Users به UserYears از طریق یک رابطه معمولی منتقل میشود و سپس از UserYears به Date از طریق یک رابطه محدود چند به چند منتقل میشود. در نهایت، کاربر سال را فیلتر میکند و بنابراین تاریخها محدود میشوند.
ما میتوانیم امنیت را با استفاده از ویژگی View As در Power BI آزمایش کنیم.

متأسفانه، گزارش دیگر کار نمیکند و خطایی نمایش داده میشود.

امنیت مبتنی بر نقش مستقیماً فروش (Sales) را فیلتر نمیکند؛ بلکه فروش را بهطور غیرمستقیم از طریق زنجیره روابطی که در بالا توضیح داده شد فیلتر میکند. مشکل این است که ما از رابطه بین جدول تاریخ (Date) و Sales[Order Date] برای اعمال امنیت استفاده میکنیم و در معیار Delivered Amount، رابطه فعال را تغییر میدهیم.
اگر DAX به توسعهدهندگان اجازه میداد که رابطه فعال را تغییر دهند، این ممکن بود منجر به حفرههای امنیتی شود. بنابراین، اگر یک رابطه فیلتر امنیتی را انتقال دهد، آن رابطه نمیتواند از طریق USERELATIONSHIP بازنویسی شود.
همانطور که در هر تنظیمات امنیتی، موتور Power BI این محدودیت را اعمال میکند: هیچ راهی برای اجتناب از آن وجود ندارد. بهتر است ایمن باشیم تا پشیمان.
رفع خطا
با این حال، این به این معنی نیست که راههایی برای نوشتن معیار Delivered Amount وجود ندارد. انتقال فیلترها از یک جدول به جدول دیگر بدون نیاز به رابطه ممکن است. در واقع، ما تکنیکهای مختلفی را برای انتقال فیلترها در این مقاله توضیح دادیم، که یکی از آنها TREATAS در DAX است.
در سناریوی ما، میتوانیم Delivered Amount را با استفاده از TREATAS بنویسیم.

کد نیاز به توضیح دارد، زیرا – با وجود اینکه کوتاه است – برخی از ظرافتها را پنهان میکند که ارزش توصیف دارند.
TREATAS محتوای Date[Date] را در زمینه فیلتر فعلی از طریق VALUES میخواند و نسل آن را به Sales[Delivery Date] تغییر میدهد. با انجام این کار، فیلتر فعلی روی Date به Sales[Delivery Date] منتقل میشود، دقیقاً همانطور که یک رابطه عمل میکند.
این معیار همچنین شامل ALL روی Date است. دلیل این امر این است که رابطه بین Date و Sales هنوز فعال است و بر اساس Sales[Order Date] است. بنابراین، فیلتر روی Date به Sales[Order Date] منتقل میشود و – زیرا ما میخواهیم Sales[Delivery Date] را فیلتر کنیم به جای Sales[Order Date] – باید فیلتر خودکار انتشار را حذف کنیم. بنابراین، از ALL برای حذف فیلتر روی Date استفاده میکنیم و در نتیجه روی Sales[Order Date]. پس از حذف فیلتر، TREATAS روی Sales[Delivery Date] فیلتر اعمال میکند و نتیجه مورد نظر را برمیگرداند.

گزارش فقط دو سال را نمایش میدهد، چون کاربر Alberto فقط مجاز به مشاهده سالهای 2017 و 2018 است — همان گزارش، وقتی بهعنوان Marco مشاهده شود، سالهای متفاوتی را نشان میدهد.

احتمالاً در این مرحله متعجب شدید : یک چیز عجیب در حال رخ دادن است. همانطور که در مقدمه گفته شد، سفارشهایی در سال 2019 ثبت شدهاند و در سال 2020 تحویل داده شدهاند. اما گزارش، مبلغ تحویلشدهی این سفارشها را نشان نمیدهد.
اگر هدف این بود که فقط سفارشهایی که در سال 2020 ثبت شدهاند مخفی شوند، بدون اینکه مبلغ تحویلشده در همان سال پنهان شود، پس محدودیتهای امنیتی بهدرستی اعمال نشدهاند. این مشکل هیچ ارتباطی با DAX یا Power BI ندارد؛ بلکه مستقیماً به ساختار مدل داده مربوط میشود.
زمانی که فیلتر امنیتی را روی ستون Year در جدول Date قرار میدهیم، در واقع جدول Date را محدود میکنیم — نه فقط تاریخ سفارش را. باید توجه داشت که Date و Order Date یک موجودیت واحد نیستند. آنها فقط در صورتی یکسان تلقی میشدند که تنها یک رابطه بین Date و Sales وجود داشت. اما از آنجایی که دو رابطه وجود دارد، Date معانی مختلفی پیدا میکند: ممکن است Order Date یا Delivery Date را فیلتر کند، بسته به اینکه در کدام معیار استفاده شده باشد.
این مقاله تمرکزش بر طراحی درست امنیت نیست، بنابراین نمیخواهیم بهصورت جزئی وارد این بحث شویم. با این حال، ارائهی یک راهحل ساده بیانصافی خواهد بود — چرا که میتواند انگیزهی خوانندگان را برای مطالعهی بیشتر دربارهی مدلسازی داده و امنیت افزایش دهد.
رویکردهای جایگزین برای فیلترهای امنیتی
یک راهحل ممکن این است که فیلتر امنیتی مستقیماً روی جدول Sales و بهطور خاص روی ستون Sales[Order Date] اعمال شود. این نوع فیلتر از نظر عملکرد (performance) کارآمد نیست، اما صرفاً نمونهای از یک رویکرد متفاوت در اعمال فیلتر امنیتی است.

با اعمال این فیلتر روی جدول Sales و غیرفعال کردن رابطه بین UserYears و Date، گزارش زیر برای Marco بهدست میآید

اکنون، امنیت مبتنی بر نقش (Role-based security) ستون Sales[Order Date] را فیلتر میکند و گزارش، تحویلها در سال 2020 را نشان میدهد — حتی با اینکه Marco به سفارشهایی که در سال 2020 ثبت شدهاند دسترسی ندارد.
علاوه بر این، چون فیلتر امنیتی اکنون در جای درستی قرار گرفته است، دیگر نیازی به فیلتر کردن جدول Sales از طریق جدول Date نیست، و معیار اصلی Delivered Amount نیز بدون مشکل کار میکند.

موضوع بهینهسازی عملکرد در این زمینه نکات فراوانی دارد، اما جای پرداختن به آن در این مقاله نیست. در واقع، هدف این مقاله تمرکز بر روی تابع USERELATIONSHIP و خطای ناشی از Row-Level Security (RLS) است.
وقتی امنیت در سطح سطر (RLS) روی یک بُعد که دارای چندین رابطه است اعمال شود، ممکن است هنگام فعالسازی یک رابطه غیرفعال، مشکلاتی ایجاد شود. ما میتوانیم با استفاده از TREATAS یا سایر روشهای انتقال فیلتر بین جداول از بروز خطا جلوگیری کنیم. با این حال، باید کاملاً مراقب باشیم که هنگام استفاده از TREATAS، معنای امنیتی RLS بهدرستی حفظ شود.
در سناریوی ما، فیلترسازی با استفاده از TREATAS منجر به گزارشهای ناقص شد و مقادیری را پنهان کرد که باید نمایش داده میشدند.
امنیت موضوعی بسیار مهم است که نیاز به مهارت در مدلسازی داده دارد. در چنین سناریوهایی، خطای DAX تنها یک نشانه است از اینکه ممکن است مشکل جدیتری وجود داشته باشد — مثلاً افشای دادههایی که باید محرمانه باقی بمانند. با این حال، TREATAS میتواند راهحلی مناسب برای خطای DAX باشد، البته به شرطی که درستی معنای امنیتیای که ایجاد میکند را اعتبارسنجی کرده باشید.
دیدگاهتان را بنویسید