
معرفی توابع تعریفشده توسط کاربر (UDF) یا user-defined functions در DAX
0 دقیقه
زمان مطالعه
0 رای
۲۳۸
بازدید
۱۴۰۵/۵/۱۳
تاریخ انتشار
توابع تعریفشده توسط کاربر (User-defined functions) یک ویژگی جدید و هیجانانگیز در DAX هستند. در این مقاله، مفاهیم کلیدی را که باید پیش از استفاده از آنها در پروژههای Power BI درک کنید، بیان میکنیم. اگرچه DAX یک زبان تابعی (functional) است، اما پیش از این امکان تعریف توابع شخصی را به کاربران نمیداد. از نسخه سپتامبر ۲۰۲۵، تعریف توابع امکانپذیر شده است؛ این توابع، expressionهای پارامتربندیشدهای هستند که میتوانند در سراسر semantic model مجددا استفاده شوند. این مقاله نحوه کارکرد این توابع را توضیح میدهد. برای مشاهده رابط کاربریِ تعریف توابع، میتوانید ویدئوی مربوطه را تماشا کنید. از توابع میتوان برای به اشتراکگذاری یک منطق تجاری (business logic) مشترک در یک semantic model یا حتی بین مدلهای مختلف استفاده کرد. شما میتوانید کتابخانههای توابع تعریفشده توسط کاربر در DAX را از آدرس https://daxlib.org/ دریافت کنید؛ یک مخزن رایگان و منبعباز (open-source) از توابع مستقل از مدلِ DAX که میتوانید به راحتی آنها را وارد کرده (import) و در مدلهای خود استفاده نمایید. سادهترین راه برای یادگیری توابع، تعریف آنها در یک کوئری و بررسی فوری نتیجه آنها است. در اینجا اولین مثال را میبینیم:
| Value |
|---|
| 30 |
تعریف تابع شامل امضای تابع یا signature (نام و پارامترها: A، B) و بدنه تابع (A + B) است که با علامت => از هم جدا شدهاند. برای فراخوانی تابع، درست مانند هر تابع بومی (native) در DAX، از نام تابع استفاده کرده و به دنبال آن پارامترها را قرار دهید. برای جلوگیری از سردرگمی، ما همیشه از فرمت Pascal case برای توابع تعریفشده توسط کاربر استفاده میکنیم. این کار به ما اجازه میدهد تا بین توابع از پیشتعریفشده DAX که همیشه با حروف بزرگ نوشته میشوند و توابع تعریفشده توسط کاربر که از ساختار Pascal case استفاده میکنند، تمایز قائل شویم. هنگام تعریف پارامترها، این امکان را داریم که type (نوع)، subtype (زیرنوع) و parameter-passing mode (حالت ارسال پارامتر) را انتخاب کنیم. حیاتیترین بخش، حالت ارسال پارامتر است؛ ما در ادامه همین مقاله، بخش ویژهای را به این موضوع اختصاص دادهایم. دو حالت برای ارسال پارامتر وجود دارد و انتخاب آن به طور قابل توجهی بر رفتار تابع تاثیر میگذارد. در مقابل، نوع و زیرنوع پارامتر از اهمیت کمتری برخوردارند. حالتهای ارسال پارامتر عبارتند از:
- VAL: مخفف Value. نشاندهنده پارامتری است که قبل از فراخوانی تابع، در evaluation context (بستر ارزیابیِ) فراخواننده (caller) ارزیابی میشود. یک پارامتر VAL در طول اجرای بدنه تابع، دارای یک مقدار واحد و مشخص است. ارزیابیهای متعدد از یک پارامتر یکسان، همیشه نتیجه یکسانی به همراه دارد.
- EXPR: مخفف Expression. نشاندهنده پارامتری است که به صورت یک عبارت، در evaluation contextای که در بدنه تابع در آن استفاده میشود، ارزیابی میگردد. ارزیابیهای متعدد از یک پارامتر EXPR ممکن است (و اغلب اینطور است) به نتایج متفاوتی منجر شود.
حالت ارسال پارامتر را میتوان با یک علامت دونقطه (:) به پارامترها اضافه کرد، مانند تعریف زیر:
توسعهدهندگان همچنین میتوانند نوع پارامترها را با انتخاب از میان طیف وسیعی از گزینهها که در جدول زیر فهرست شدهاند، مشخص کنند.
| Type | Subtype | Passing mode | Remarks |
|---|---|---|---|
| ANYVAL | VAL | بدون محدودیت، از هر expressionای میتوان استفاده کرد. این حالت پیشفرض است. | |
| SCALAR | VAL / EXPR | هر مقدار اسکالر (scalar)، مانند یک عدد صحیح یا رشته؛ از جداول نمیتوان استفاده کرد. | |
| VARIANT | VAL / EXPR | عبارت اسکالر، هر نوع دادهای قابل قبول است. | |
| INT64 | VAL / EXPR | عبارت اسکالر، نوع داده باید عدد صحیح (Integer) باشد. | |
| DECIMAL | VAL / EXPR | عبارت اسکالر، نوع داده باید Fixed Decimal باشد. | |
| DOUBLE | VAL / EXPR | عبارت اسکالر، نوع داده باید Decimal باشد. | |
| STRING | VAL / EXPR | عبارت اسکالر، نوع داده باید String باشد. | |
| DATETIME | VAL / EXPR | عبارت اسکالر، نوع داده باید Date یا DateTime باشد. | |
| BOOLEAN | VAL / EXPR | عبارت اسکالر، نوع داده باید Boolean باشد. | |
| NUMERIC | VAL / EXPR | عبارت اسکالر، نوع داده باید Integer باشد. | |
| TABLE | VAL / EXPR | هر table expression (عبارت جدولی)، مقادیر اسکالر مجاز نیستند. | |
| ANYREF | EXPR | ارجاع (reference) به هر جدول، ستون، measure یا تقویم. |
نوع پارامتر را میتوان به همراه حالت ارسال پارامتر اضافه کرد، مانند مثال زیر:
توابع از تبدیل نوع (casting) خودکار پارامترها استفاده میکنند، به این معنی که expressionهای استفاده شده برای مقداردهی پارامترها، به نوع داده مورد نیاز تبدیل (cast) میشوند. کستینگ خودکار ممکن است باعث سردرگمی شود و نیاز به توضیح بیشتری دارد. بیایید تابعی تعریف کنیم که نوع داده مورد انتظار پارامتر آن یک عدد صحیح (integer) باشد. زمانی که تابع با اعداد صحیح ارزیابی میشود، همه چیز مطابق انتظار کار میکند. به عنوان مثال، ارسال سه و دو، نتیجه مورد انتظار یعنی پنج را تولید میکند:
| Value |
|---|
| 5 |
با این حال، اگر تابع با ارسال دو عدد اعشاری فراخوانی شود، به دلیل کستینگ خودکار هیچ خطایی رخ نمیدهد. در واقع، قبل از ارزیابی تابع، هر آرگومان به طور خودکار به نوع داده مورد نیاز تبدیل میشود:
| Value |
|---|
| 5 |
| 5 |
با وجود اینکه مجموع اعداد اعشاری باید ۵.۸ باشد (که در صورت گرد شدن به یک عدد صحیح، ۶ میشود)، اما از آنجایی که هر پارامتر به صورت جداگانه به یک عدد صحیح تبدیل (cast) میشود، حاصل ۳+۲ برابر با پنج است.
درک حالتهای ارسال پارامتر (Parameter-passing modes)
حالت ارسال پارامتر را میتوان با یک علامت دونقطه (:) به پارامترها اضافه کرد، مانند تعریف زیر:
| Value |
|---|
| 30 |
در تابع تعریفشده در بالا، هم a و هم b پارامترهای مقداری (value parameters) هستند. به طور پیشفرض، آرگومانهای توابع از نوع value parameters هستند. با این حال، میتوان از EXPR استفاده کرد تا با آنها به عنوان عبارت (expression) رفتار شود. درک این تفاوت بسیار مهم است، زیرا استفاده از حالت ارسال پارامترِ اشتباه، به احتمال زیاد باعث ایجاد مشکلاتی در کد شما میشود که دیباگ (عیبیابی) آنها دشوار خواهد بود. فراخواننده (caller)، value parameterها را قبل از اجرای تابع ارزیابی میکند. تابع، مقدارِ پارامتر را به عنوان یک آرگومان دریافت کرده و از آن برای اهداف خود استفاده میکند. یک پارامتر مقداری مانند تعریف یک متغیر است: فرمول استفاده شده توسط فراخواننده مقداری را تولید میکند؛ این مقدار به یک متغیر اختصاص داده میشود و سپس تابع اجرا میگردد. تابع زیر را در نظر بگیرید:
از آنجا که هر دو پارامتر از نوع VAL هستند، کد به شکل زیر تبدیل میشود:
از طرف دیگر، پارامترهای expression توسط فراخواننده ارزیابی نمیشوند. این پارامترها به عنوان فرمول ارسال میشوند و هر بار که تابع از آنها استفاده میکند، ارزیابی میگردند. یک پارامتر expression بسته به evaluation contextای که هنگام ارزیابی پارامتر فعال است، میتواند مقادیر متفاوتی در بخشهای مختلف بدنه تابع داشته باشد. کد زیر همان تابع SumTwoNumbers است که قبلا استفاده کردیم، اما این بار با پارامترهای عبارت (EXPR):
از آنجا که هر دو پارامتر از نوع EXPR هستند، کد معادل ساختار زیر است:
در مثالی که تا اینجا نشان دادیم، چه از پارامترهای VAL استفاده شود و چه EXPR، هیچ تفاوتی در نتیجه تابع وجود ندارد. با این حال، در بیشتر موارد، تعیین حالت ارسال صحیح به منطق تابع (semantics) بستگی دارد که تاثیر عمیقی بر رفتار آن میگذارد. استفاده از پارامترهای VAL حالت پیشفرض است و هر زمان که تابع انتظار کار با یک مقدار مشخص را دارد، باید از VAL (به صورت ضمنی یا صریح، که پیشنهاد ما همیشه تعیین صریح است) استفاده کنید. پارامترهای EXPR زمانی مورد نیاز هستند که تابع یک عبارت را در contextای که بالقوه متفاوت است ارزیابی کند و بر اساس منطق تابع نتایج متفاوتی تولید نماید. بیایید این موضوع را از طریق یک مثال بررسی کنیم. هدف تابع زیر محاسبه یک عبارت با محدود کردن محاسبه به محصولات قرمز رنگ است. این تابع آنطور که در نظر گرفته شده کار نمیکند زیرا حالت ارسال پارامتر به صورت پیشفرض VAL است، اما تابع برای عملکرد صحیح به یک پارامتر EXPR نیاز دارد:
فرض کنید تابع را با ارسال measureِ Sales Amount به عنوان آرگومان amount فراخوانی کنیم. در این حالت، کد با یک متغیر دگرگون میشود و اثر تغییرِ filter context که توسط CALCULATE اعمال شده است، از بین میرود:
بدنه تابع ComputeForRed پارامتر را در داخل CALCULATE ارزیابی میکند، جایی که filter context تغییر داده میشود تا رنگ محصول اجبارا به Red فیلتر شود. با این حال، با استفاده از پارامتر VAL، ارزیابی آرگومان قبل از اجرای تابع انجام میشود. در قطعه کد قبلی، ما از متغیر amount برای شبیهسازی این رفتار استفاده کردیم. از آنجا که این پارامتر ثابت است، بدون توجه به filter context بدون تغییر باقی میماند. با وجود اینکه بدنه تابع پارامتر را در filter contextای ارزیابی میکند که تنها محصولات قرمز را فیلتر کرده، نتیجه به دست آمده همان Sales Amount برای تمام رنگهای محصولِ قابل مشاهده است. تغییر حالت ارسال پارامتر به EXPR باعث میشود تابع به درستی کار کند:
بیایید با یک مثال واقعیتر، این موضوع را بیشتر باز کنیم. ما یک table function ایجاد میکنیم که بهترین مشتریان را بر اساس یک متریک (metric) برمیگرداند. یک مشتری در صورتی بهترین در نظر گرفته میشود که مقدار متریکِ خود آن مشتری از میانگین مقدار آن متریک برای تمام مشتریان بزرگتر باشد. ما میخواهیم این متریک یکی از پارامترهای تابع باشد. در اینجا کوئریِ تعریف و اجرای تابع BestCustomers آورده شده است:
در این کد جزئیات متعددی وجود دارد که ارزش ذکر کردن دارند:
- metricExpr یک پارامتر EXPR است؛ این پارامتر هر بار که فراخوانی میشود، ارزیابی میگردد. در بدنه تابع، دو iterator (تکرارکننده) متفاوت به metricExpr ارجاع میدهند؛ بنابراین، metricExpr برای هر سطر در هر iterator ارزیابی خواهد شد.
- پارامتر metricExpr در داخل AVERAGEX (یک بار به ازای هر مشتری) در تعریف AverageMetric ارزیابی میشود. توجه داشته باشید که این ارزیابی در طول یک iteration (تکرار) روی جدول Customer رخ میدهد، جایی که AVERAGEX یک row context ایجاد میکند. در نتیجه، metricExpr در یک row context ارزیابی میشود.
- پارامتر metricExpr مجددا در داخل حلقه تکرار FILTER هنگام محاسبه BestCustomers ارزیابی میشود. در این سناریو، یک row context متفاوت وجود دارد که این بار توسط FILTER ایجاد شده است. بنابراین، metricExpr یک بار به ازای هر مشتری در row context ایجاد شده توسط FILTER ارزیابی میگردد.
پارامتر metricExpr چندین بار و تحت evaluation contextهای متفاوتی ارزیابی میشود. در نتیجه، هر بار که ارزیابی میشود نتایج متفاوتی تولید میکند. این تصادفی نیست. ما دقیقا نیاز داریم که metricExpr برای کارکرد صحیح تابع، نتایج متفاوتی تولید کند. در واقع، metricExpr در دو row context متفاوت در طول دو iteration مجزا فراخوانی میشود، و هر بار نیاز دارد که فروش مشتریِ در حال پیمایش را از طریق context transition ارزیابی کند. تابع نتیجه صحیحی تولید میکند؛ پس از اجرا، خروجی شامل ۱,۸۰۷ مشتری است که برخی از آنها در تصویر زیر نشان داده شدهاند.
تغییر نوع پارامتر به VAL یک نتیجه خالی تولید میکند. کوئری زیر یک جدول خالی برمیگرداند و تنها تفاوت بین نسخه قبلی و این نسخه، حالت ارسال پارامتر است.
دلیل خالی بودن نتیجه این است که یک value parameter یک بار در هنگام فراخوانی تابع ارزیابی شده و پس از آن دیگر ارزیابی نمیشود. هر بار که تابع به پارامتر دسترسی پیدا میکند، نتیجه یکسانی را به دست میآورد، صرف نظر از اینکه در چه evaluation contextای از آن استفاده میشود. در مثال VAL، پیش از فراخوانی تابع BestCustomers، مژر Sales Amount ارزیابی میشود. بنابراین، پارامتر metricVal شامل ارزش کل Sales Amount برای تمام مشتریان است. سپس، وقتی تابع اجرا میشود، AverageMetric میانگین تمام مقادیر یکسان را تولید میکند (زیرا metricVal دیگر ارزیابی نمیشود، بلکه فقط خوانده میشود). در نهایت، FILTER هیچ نتیجهای برنمیگرداند زیرا شرط بررسی میکند که آیا یک مقدار قطعا از خودش بزرگتر است یا خیر، که منجر به نتیجه FALSE برای تمام سطرها میشود. در مثال EXPR قبلی، هر بار که ارجاعی به metricExpr صورت میگیرد، مژر Sales Amount ارزیابی میشود، گویی که metricExpr با رفرنس measureِ Sales Amount جایگزین شده است. این کار نتیجه مورد انتظار را تولید میکند. استفاده از پارامترهای VAL رفتار پیشفرض است، زیرا بصریترین راه برای استفاده از پارامترهاست. با این حال، در بسیاری از توابع، همانطور که در دو مثال اخیر نشان داده شد، تزریق بخشی از کد از طریق پارامترهای EXPR بسیار مفید است. هنگام استفاده از expression parameters، باید به نحوه استفاده تابع از پارامتر توجه زیادی داشته باشیم. برای مثال، اجازه دهید کد کوئری را تغییر دهیم. این بار، هنوز از یک پارامتر expression استفاده میکنیم، اما هنگام فراخوانی تابع به جای measureِ Sales Amount از SUMX استفاده مینماییم:
کاملا در کمال تعجب، این کوئری یک جدول خالی برمیگرداند. این بار، یافتن مشکل به کمی تلاش بیشتر نیاز دارد زیرا پارامتر روی نوع صحیح تنظیم شده است؛ بنابراین، مشکل جای دیگری است. در واقع، مشکل در این مرحله عدم وجود یک context transition در درون iterationها است. در مثال EXPR قبلی، آرگومان تابع BestCustomers یک رفرنس measure یعنی Sales Amount بود. در آخرین مثال EXPR، از یک فرمول ساده شامل SUMX استفاده کردیم که مطابق با تعریف measureِ Sales Amount است. زمانی که از یک measure reference به عنوان آرگومان metricExpr استفاده کردیم، هر بار که به پارامتر metricExpr در تابع BestCustomers ارجاع داده میشد، یک context transition به صورت ضمنی رخ میداد و کد همانطور که انتظار میرفت کار میکرد. با استفاده از عبارت SUMX، انتقال بستر (context transition) اتفاق نمیافتد زیرا CALCULATE ضمنیِ پیرامون measure reference وجود ندارد. پارامترها شبیه به measureها نیستند: هیچ context transition خودکاری اتفاق نمیافتد. از آنجایی که context transition وجود ندارد، ارزیابی این عبارت (expression) تحت تاثیر row context ایجاد شده توسط FILTER و AVERAGEX قرار نمیگیرد. تفاوت بین measureها و expressionها در آرگومانهای EXPR بسیار حیاتی است، زیرا کنترل و انعطافپذیری بیشتری به توابع میدهد و در صورت استفاده موثر، پتانسیل بهبود عملکرد را دارد. با این وجود، برای جلوگیری از نتایج غیرمنتظره باید به دقت مدیریت شود. برای بازیابی رفتار صحیح کد، ضروری است که به صورت صریح از CALCULATE استفاده شود:
افزودن CALCULATE خارجی، یک context transition را تحمیل میکند و باعث میشود کوئری ۱,۸۰۷ مشتری را برگرداند. با این حال، هنگام نوشتن توابع، باید به این جزئیات توجه ویژهای داشته باشیم. به جای تغییر در نحوه فراخوانی تابع، بهتر است کد تابع را طوری تغییر دهیم که در مواقع نیاز، context transition را فعال کند. در مثال ما، کافیست هر بار که metricExpr در یک row context ارزیابی میشود CALCULATE را اضافه کنیم تا مشخص شود که به context transition نیاز داریم:
این نسخه از تابع، مستقل از پارامتری که دریافت میکند کار خواهد کرد؛ چه یک فرمول و چه یک measure کاملا بدون مشکل عمل میکنند. زمانی که به context transition نیاز باشد، این کار توسط کد تابع از طریق یک CALCULATE صریح تحمیل میشود. این موضوع ممکن است یک جزئیات کوچک به نظر برسد، اما دقیقا تفاوت بین تابعی که توسط یک مبتدی نوشته شده و تابعی که توسط یک متخصص DAX نوشته شده است را نشان میدهد. یک متخصص اطمینان حاصل میکند که تابع حتی در صورتی که به روشی غیرمنتظره استفاده شود نیز کار خواهد کرد. در نهایت، هنگام نوشتن توابع، همیشه ایده خوبی است که با نوشتن کد به بهینهترین شکل ممکن، آن را بهینهسازی (optimize) کنید. دلیل آن این است که انتظار میرود توابع توسط چندین measure و توابع دیگر فراخوانی شوند؛ در نتیجه، نوشتن کد بهینه میتواند تاثیر عمیقی بر عملکرد semantic model داشته باشد. این آخرین نسخه از تابع، با تجمیع نتیجه metricExpr برای هر مشتری در یک متغیر، تعداد دفعات اجرای متریک را کاهش میدهد:
نتیجهگیری
توابع تعریفشده توسط کاربر منابع ارزشمندی برای توسعهدهندگان DAX هستند. با ایجاد توابع، توسعهدهندگان میتوانند کدِ مدل را به بخشهای کوچکتر و قابل مدیریت تقسیم کنند که تست و دیباگ مستقل را تسهیل میبخشد. پس از اعتبارسنجی و بهینهسازی کامل، هر تابع تبدیل به یک بلوک سازنده میشود که به استحکام کلی پروژه کمک میکند. هنگام توسعه توابع، در نظر گرفتن هر دو حالتِ parameter-passing modes ضروری است و به ویژه برای پارامترهای expression، باید ارزیابی کرد که آیا کپسولهسازی با استفاده از CALCULATE برای پشتیبانی از context transitions ضروری است یا خیر. ما نمیتوانیم آیندهای را تصور کنیم که در آن یک semantic model پیشرفته دارای measureهای متعدد باشد اما فاقد توابع تعریفشده توسط کاربر باشد. به طور سنتی، measureها و calculation groupها پیچیدگیهای محاسباتی را مدیریت میکردند، اما این امر اغلب منجر به کاهش عملکرد و کدهای با خوانایی کمتر میشود. توابع تعریفشده توسط کاربر یک روش موثر برای انتزاعی کردن محاسبات پیچیده بدون کاهش عملکرد ارائه میدهند.
این محتوا چطور بود؟
نظر شما به بهبود محتوا کمک میکند

امیر دایی
500k دنبالکنندهاز بچگی عاشق برنامه نویسی بودم