บทช่วยสอน: เรียกใช้คําสั่ง EVALUATE หลายคําสั่งด้วย PowerShell

ในบทช่วยสอนนี้ คุณใช้ PowerShell เพื่อส่งคําขอ REST API แบบสอบถาม DAX เดียวที่มี EVALUATE หลายคําสั่ง จากนั้นแยกวิเคราะห์การตอบสนองลูกศร Apache แบบหลายชุดผลลัพธ์ รูปแบบนี้ช่วยให้คุณสามารถดึงชุดผลลัพธ์ที่เกี่ยวข้องหลายชุดในการเดินทางไปกลับครั้งเดียวจากสคริปต์ระบบอัตโนมัติของ PowerShell

ไดอะแกรมแสดงการร้องขอ HTTP POST หนึ่งรายการที่มีคําสั่ง EVALUATE สามคําสั่งในเนื้อหาแบบสอบถาม และการตอบสนอง Arrow IPC ที่มีชุดผลลัพธ์สามชุดในลําดับเดียวกัน

เหตุใดจึงต้องส่งใบแจ้งยอด EVALUATE หลายรายการในคําขอเดียว

API ดําเนินการคิวรี DAX ยอมรับสตริงเดียว query ที่สามารถมีคําสั่งได้หลาย EVALUATE คําสั่ง แต่ละคําสั่งจะส่งกลับชุดผลลัพธ์ของตัวเอง และเนื้อหาการตอบกลับคือการต่อกันของสตรีม Arrow IPC หนึ่งรายการต่อ EVALUATE คําสั่งในลําดับการประกาศ การส่งคิวรีที่เกี่ยวข้องร่วมกันจะหลีกเลี่ยงค่าใช้จ่ายต่อคําขอของการเรียก HTTP แยกต่างหาก รวมถึงการตรวจสอบโทเค็น Microsoft Entra เพิ่มเติมและการเริ่มต้นกลไกจัดการ DAX การส่งใบแจ้งยอดหลาย EVALUATE ใบในคําขอเดียวยังสามารถช่วยลดผลกระทบของการควบคุมปริมาณคําขอได้อีกด้วย Power BI จํากัดผู้เรียกไว้ที่ 120 คําขอคิวรีต่อนาทีต่อผู้ใช้สําหรับการดําเนินการคิวรีแบบจําลองความหมาย

สิ่งที่คุณสร้าง

ในสคริปต์ PowerShell หนึ่งสคริปต์ คุณ:

  1. รับโทเค็นการเข้าถึง Microsoft Entra
  2. สร้างเนื้อหาคําขอที่มี query คําสั่งสาม EVALUATE คําสั่ง
  3. ส่งคําขอและบันทึกสตรีมการตอบสนอง Arrow IPC ดิบ
  4. แยกวิเคราะห์การตอบกลับเป็นชุดผลลัพธ์หนึ่งชุดต่อ EVALUATE คําสั่ง
  5. แสดงแต่ละชุดผลลัพธ์เป็นวัตถุ PowerShell

ข้อกำหนดเบื้องต้น

  • PowerShell 7.4 หรือใหม่กว่า Windows PowerShell 5.1 ไม่ได้รับการสนับสนุนApache.Arrowเนื่องจากแพคเกจที่ใช้ในบทช่วยสอนนี้ขัดแย้งกับSystem.Memoryแอสเซมบลีที่รวมอยู่ใน PowerShell 5.1
  • พื้นที่ทํางาน Power BI บนความจุ Premium หรือ Fabric ที่มีแบบจําลองความหมายอย่างน้อยหนึ่งแบบ
  • สิทธิ์ในการสร้างและอ่านบนแบบจําลองความหมาย
  • โมดูล MicrosoftPowerBIMgmt สําหรับการรับรองความถูกต้อง cmdlet ใช้แอปไคลเอ็นต์ Power BI บุคคลที่หนึ่งของ Microsoft ดังนั้นคุณไม่จําเป็นต้องลงทะเบียนแอปของคุณเองใน Microsoft Entra
  • ไลบรารี Apache.Arrow และ Apache.Arrow.Compression .NET เพื่อ deserialize การตอบสนอง ดําเนินการคิวรี DAX REST API บีบอัดบัฟเฟอร์ลูกศรด้วยการบีบอัดเฟรม LZ4 ดังนั้นจึงApache.Arrow.Compressionจําเป็นต้องมีการขึ้นต่อกัน (K4os.Compression.LZ4, K4os.Compression.LZ4.Streams, , K4os.Hash.xxHash) ZstdSharp.Port ขั้นตอนต่อไปจะแสดงวิธีการดาวน์โหลด
  • การตั้งค่าผู้เช่าต่อไปนี้ที่เปิดใช้งานในพอร์ทัลผู้ดูแลระบบ Power BI:
    • ชุดข้อมูลดําเนินการคิวรี REST API (ภายใต้ การตั้งค่านักพัฒนา)
    • อนุญาตตําแหน่งข้อมูล XMLA และวิเคราะห์ใน Excel ด้วยโมเดลความหมายภายในองค์กร (ภายใต้ การตั้งค่าการรวม)

ติดตั้ง PowerShell 7.4 หรือใหม่กว่าโดยใช้ winget:

winget install --id Microsoft.PowerShell --source winget

หลังการติดตั้ง ให้เปิดเชลล์ใหม่ด้วยpwsh เรียกใช้คําสั่งที่เหลือในบทช่วยสอนนี้จากเซสชันนั้น

ติดตั้งโมดูล MicrosoftPowerBIMgmt -Forceค่าสถานะยอมรับพร้อมท์ PowerShell Gallery untrusted-repository

Install-Module -Name MicrosoftPowerBIMgmt -Scope CurrentUser -Force

ดาวน์โหลดแพ็คเกจ NuGet ที่จําเป็นและแยกแอสเซมบลีไปที่C:\Tools\Apache.Arrow\ .nupkgไฟล์เป็นไฟล์เก็บถาวร ZIP ดังนั้นจึงExpand-Archiveใช้งานได้โดยตรง ลูปจะเลือกโฟลเดอร์เป้าหมายสูงสุด netX.0 ในแต่ละแพ็คเกจ เพื่อให้แอสเซมบลีเข้ากันได้เมื่อแพ็คเกจเผยแพร่เป้าหมายที่ใหม่กว่า

$dest = "C:\Tools\Apache.Arrow"
New-Item -ItemType Directory -Force -Path $dest | Out-Null

$packages = @(
    "Apache.Arrow",
    "Apache.Arrow.Compression",
    "K4os.Compression.LZ4",
    "K4os.Compression.LZ4.Streams",
    "K4os.Hash.xxHash",
    "ZstdSharp.Port"
)

foreach ($pkg in $packages) {
    $nupkg  = Join-Path $env:TEMP "$pkg.nupkg"
    $expand = Join-Path $env:TEMP $pkg
    if (Test-Path $expand) { Remove-Item $expand -Recurse -Force }

    Invoke-WebRequest -Uri "https://www.nuget.org/api/v2/package/$pkg" -OutFile $nupkg
    Expand-Archive -Path $nupkg -DestinationPath $expand -Force

    $libDirs = Get-ChildItem (Join-Path $expand "lib") -Directory
    $best = $libDirs | Where-Object { $_.Name -match "^net\d" } |
            Sort-Object Name -Descending | Select-Object -First 1
    if (-not $best) {
        $best = $libDirs | Sort-Object Name -Descending | Select-Object -First 1
    }

    Get-ChildItem (Join-Path $best.FullName "*.dll") |
        Copy-Item -Destination $dest -Force
}

1 - ตรวจสอบสิทธิ์

ลงชื่อเข้าใช้บริการของ บริการของ Power BI แบบโต้ตอบ จากนั้นแยกโทเค็นการเข้าถึง Connect-PowerBIServiceAccount cmdlet ไม่ต้องการให้คุณลงทะเบียนแอปของคุณเองใน Microsoft Entra

Connect-PowerBIServiceAccount -WarningAction SilentlyContinue
$accessToken = (Get-PowerBIAccessToken).Authorization -replace '^Bearer\s+',''

2 - สร้างคําขอด้วยคําสั่ง EVALUATE หลายคําสั่ง

กําหนดเป้าหมายพื้นที่ทํางานและแบบจําลองความหมาย จากนั้นสร้างเนื้อหาคําขอ คุณสมบัติเป็น query สตริงเดียวที่มีคําสั่งสาม EVALUATE คําสั่งคั่นด้วยบรรทัดว่าง

$groupId   = "YOUR_WORKSPACE_ID"
$datasetId = "YOUR_DATASET_ID"

$query = @"
EVALUATE
ROW("RowCount", COUNTROWS('Sales'))

EVALUATE
TOPN(10, 'Sales', 'Sales'[Amount], DESC)

EVALUATE
SUMMARIZECOLUMNS(
    'Date'[Year],
    "TotalSales", SUM('Sales'[Amount]))
"@

$body = @{
    query                  = $query
    resultsetRowcountLimit = 500000
} | ConvertTo-Json

3 - ส่งคําขอและบันทึกสตรีมการตอบกลับดิบ

ส่งคําขอ POST และอ่านเนื้อหาการตอบกลับเป็นสตรีมไบนารี ใช้ HttpWebRequest แทน Invoke-RestMethod, Invoke-PowerBIRestMethod, หรือ Invoke-WebRequest. การตอบสนองคือสตรีม IPC ลูกศรไบนารี cmdlet PowerShell ระดับสูงจะตีความเนื้อหาการตอบสนองเป็นข้อความ ซึ่งทําให้เนื้อหาไบนารีเสียหาย HttpWebRequest ส่งคืนสตรีมดิบที่ไม่ได้แก้ไข

$url = "https://api.powerbi.com/v1.0/myorg/groups/$groupId" +
       "/datasets/$datasetId/executeDaxQueries"

$request = [System.Net.HttpWebRequest]::Create($url)
$request.Method      = "POST"
$request.ContentType = "application/json"
$request.Accept      = "application/vnd.apache.arrow.stream"
$request.Timeout     = 180000   # milliseconds
$request.Headers.Add("Authorization", "Bearer $accessToken")

$bodyBytes     = [System.Text.Encoding]::UTF8.GetBytes($body)
$requestStream = $request.GetRequestStream()
$requestStream.Write($bodyBytes, 0, $bodyBytes.Length)
$requestStream.Close()

$response       = $request.GetResponse()
$responseStream = $response.GetResponseStream()

# Buffer the response into memory so the parser can iterate over multiple Arrow IPC streams.
$memoryStream = New-Object System.IO.MemoryStream
$responseStream.CopyTo($memoryStream)
$responseStream.Close()
$response.Close()
$memoryStream.Position = 0

4 - แยกวิเคราะห์การตอบสนองหลายชุดผลลัพธ์

เนื้อหาการตอบสนองคือการต่อสตรีม Apache Arrow IPC หนึ่ง EVALUATE สตรีมต่อคําสั่ง PowerShell ไม่ได้มาพร้อมกับตัวแยกวิเคราะห์ลูกศร ดังนั้นขั้นตอนนี้จะโหลดApache.Arrowไลบรารี .NET ผ่านตัวช่วย C# แบบอินไลน์ขนาดเล็กที่เพิ่มด้วยAdd-Type การเก็บตรรกะสตรีมลูปไว้ใน C# จะทําให้ไซต์การโทรสั้นและส่งคืนรายการชุดผลลัพธ์ที่สคริปต์ PowerShell ของคุณสามารถทําซ้ําได้ ตัวช่วยจะเปิด ArrowStreamReader เครื่องหมายใหม่หลังจากจุดสิ้นสุดสตรีมแต่ละรายการ ดังนั้นลูปเดียวกันจะจัดการชุดผลลัพธ์จํานวนเท่าใดก็ได้ในการตอบกลับ

Add-Type -Path "C:\Tools\Apache.Arrow\Apache.Arrow.dll"
Add-Type -Path "C:\Tools\Apache.Arrow\Apache.Arrow.Compression.dll"

# Reference the full .NET reference set that ships with PowerShell 7 so the
# inline C# below can resolve BCL types such as List<T> and Dictionary<,>.
$refs  = Get-ChildItem "$PSHOME\ref\*.dll" | ForEach-Object FullName
$refs += Get-ChildItem "C:\Tools\Apache.Arrow\*.dll" | ForEach-Object FullName

Add-Type -ReferencedAssemblies $refs -IgnoreWarnings -WarningAction SilentlyContinue -TypeDefinition @"
using System;
using System.Collections.Generic;
using System.IO;
using Apache.Arrow;
using Apache.Arrow.Compression;
using Apache.Arrow.Ipc;

public class DaxResultSet
{
    public List<string> ColumnNames = new List<string>();
    public List<Dictionary<string, object>> Rows =
        new List<Dictionary<string, object>>();
}

public static class DaxMultiResultReader
{
    public static List<DaxResultSet> ReadAll(Stream stream)
    {
        var results = new List<DaxResultSet>();
        var codecFactory = new CompressionCodecFactory();
        while (stream.Position < stream.Length)
        {
            var rs = new DaxResultSet();
            bool gotSchema = false;
            using (var reader = new ArrowStreamReader(stream, codecFactory, leaveOpen: true))
            {
                RecordBatch batch;
                while ((batch = reader.ReadNextRecordBatch()) != null)
                {
                    using (batch)
                    {
                        if (!gotSchema)
                        {
                            foreach (var f in batch.Schema.FieldsList)
                                rs.ColumnNames.Add(f.Name);
                            gotSchema = true;
                        }
                        for (int r = 0; r < batch.Length; r++)
                        {
                            var row = new Dictionary<string, object>();
                            for (int c = 0; c < batch.ColumnCount; c++)
                                row[rs.ColumnNames[c]] = GetValue(batch.Column(c), r);
                            rs.Rows.Add(row);
                        }
                    }
                }
            }
            if (gotSchema) results.Add(rs);
        }
        return results;
    }

    private static object GetValue(IArrowArray a, int i)
    {
        if (a == null) return null;
        if (a is DictionaryArray da)
        {
            // Resolve the dictionary index, then look up the value in the dictionary.
            int dictIndex;
            switch (da.Indices)
            {
                case Int32Array idx32: if (idx32.IsNull(i)) return null; dictIndex = idx32.GetValue(i).Value;       break;
                case Int16Array idx16: if (idx16.IsNull(i)) return null; dictIndex = idx16.GetValue(i).Value;       break;
                case Int8Array  idx8:  if (idx8.IsNull(i))  return null; dictIndex = idx8.GetValue(i).Value;        break;
                case Int64Array idx64: if (idx64.IsNull(i)) return null; dictIndex = (int)idx64.GetValue(i).Value;  break;
                default: return da.Indices.ToString();
            }
            return GetValue(da.Dictionary, dictIndex);
        }
        if (a is StringArray sa)      return sa.GetString(i);
        if (a is BooleanArray ba)     return ba.IsNull(i) ? (object)null : ba.GetValue(i);
        if (a is Int64Array i64)      return i64.IsNull(i) ? (object)null : i64.GetValue(i);
        if (a is Int32Array i32)      return i32.IsNull(i) ? (object)null : i32.GetValue(i);
        if (a is DoubleArray d)       return d.IsNull(i)   ? (object)null : d.GetValue(i);
        if (a is Decimal128Array dec) return dec.GetValue(i);
        if (a is Date32Array d32)     return d32.GetDateTime(i);
        if (a is Date64Array d64)     return d64.GetDateTime(i);
        if (a is TimestampArray ts)   return ts.GetTimestamp(i);
        return a.ToString();
    }
}
"@

$results = [DaxMultiResultReader]::ReadAll($memoryStream)
Write-Host "Received $($results.Count) result sets."

5 - ทํางานกับแต่ละชุดผลลัพธ์

แปลงชุดผลลัพธ์แต่ละชุดเป็น PSCustomObject แถว ตอนนี้คุณสามารถไปป์แถวผ่าน Where-Object, Group-Object, หรือ Export-Csvcmdlet PowerShell อื่นๆ ได้

function ConvertTo-PSObjectRows {
    param([Parameter(Mandatory)] $ResultSet)
    foreach ($row in $ResultSet.Rows) {
        $obj = [ordered]@{}
        foreach ($col in $ResultSet.ColumnNames) { $obj[$col] = $row[$col] }
        [PSCustomObject]$obj
    }
}

$rowCount    = ConvertTo-PSObjectRows -ResultSet $results[0]
$topProducts = ConvertTo-PSObjectRows -ResultSet $results[1]
$yearTotals  = ConvertTo-PSObjectRows -ResultSet $results[2]

$rowCount    | Format-Table
$topProducts | Format-Table
$yearTotals  | Format-Table

ตัวแปรแต่ละตัวจะเก็บแถวจากคําสั่งที่ EVALUATE สอดคล้องกันตามลําดับที่คําสั่งปรากฏในคําขอ

การแก้ไขปัญหา

  • 401 ไม่ได้รับอนุญาต — โทเค็นที่แคชไว้หมดอายุ เรียกใช้Connect-PowerBIServiceAccountอีกครั้งเพื่อรีเฟรช แล้วอ่าน$accessTokenใหม่จากGet-PowerBIAccessToken
  • คําเตือน MSAL ระหว่างConnect-PowerBIServiceAccountMicrosoftPowerBIMgmt รวม MSAL.NET รุ่นเก่าที่ส่งข้อความติดตามภายใน (เช่น SetAuthorityUri, , TryNormalizeRealm) MsaDeviceOperationProvider is not availableที่ระดับความรุนแรงของคําเตือน พวกเขาปลอดภัยที่จะเพิกเฉยตราบใดที่ cmdlet พิมพ์EnvironmentUserName / TenantId / บล็อก หากต้องการระงับ ให้ผ่าน-WarningAction SilentlyContinue
  • HTTP 200 ที่มีชุดผลลัพธ์ข้อผิดพลาด — คําขอ HTTP สําเร็จ แต่สตรีมลูกศรมีข้อผิดพลาด ตรวจสอบข้อมูลเมตาของ Schema สําหรับ IsError=trueและอ่าน FaultCode และFaultString สําหรับรายละเอียด โปรดดู แนวทางปฏิบัติที่ดีที่สุดสําหรับ REST API ของการดําเนินการคิวรี DAX
  • Invoke-RestMethod ส่งคืนข้อความที่อ่านไม่ออก — อย่าใช้ Invoke-RestMethod, Invoke-PowerBIRestMethodหรือ Invoke-WebRequest กับ API นี้ คําตอบเป็นไบนารี ใช้ HttpWebRequest ตามที่แสดงในขั้นตอนที่ 3
  • Add-TypeโหลดApache.Arrow.dllไม่สําเร็จ — บน Windows PowerShell 5.1 แพคเกจขัดแย้งApache.Arrowกับแอสเซมบลีในกล่องSystem.Memory ใช้ PowerShell 7.4 หรือใหม่กว่า
  • ไม่มีหรือน้อยกว่าชุดผลลัพธ์ที่ส่งคืนมากกว่า EVALUATE คําสั่ง — ยืนยันว่าแต่ละ EVALUATE คําสั่งถูกต้องตามหลักไวยากรณ์ด้วยตัวเอง การไม่ถูกต้อง EVALUATE เพียงครั้งเดียวทําให้ API แสดงข้อผิดพลาดแทนที่จะเป็นการตอบสนองหลายชุดผลลัพธ์บางส่วน