Zelfstudie: Meerdere EVALUATE-instructies uitvoeren met PowerShell

In deze zelfstudie gebruikt u PowerShell om één REST API-aanvraag voor DAX-query's uitvoeren met meerdere EVALUATE instructies te verzenden en vervolgens het Apache Arrow-antwoord met meerdere resultaten te parseren. Met dit patroon kunt u meerdere gerelateerde resultsets in één round trip ophalen met behulp van een PowerShell-automatiseringsscript.

Diagram dat één HTTP POST-verzoek toont dat drie EVALUATE-instructies bevat in de hoofdtekst van de query, en het Arrow IPC-antwoord dat drie resultaatsets in dezelfde volgorde bevat.

Waarom meerdere EVALUATE-instructies in één aanvraag indienen

De API VOOR DAX-query's uitvoeren accepteert één query tekenreeks die meerdere EVALUATE instructies kan bevatten. Elke instructie retourneert zijn eigen resultatenset en de antwoordbody is de concatenatie van één Arrow IPC-stream per EVALUATE instructie, in volgorde van declaratie. Het indienen van gerelateerde query's voorkomt de overhead per aanvraag van afzonderlijke HTTP-aanroepen, waaronder extra Microsoft Entra tokenvalidatie en initialisatie van DAX-engine. Het verzenden van meerdere EVALUATE statements in één verzoek kan ook helpen om de gevolgen van snelheidsbeperking van verzoeken te beperken. Power BI beperkt aanroepers per gebruiker tot 120 queryaanroepen per minuut voor querybewerkingen voor semantische modellen.

Wat u bouwt

In één PowerShell-script gaat u het volgende doen:

  1. Een Microsoft Entra toegangstoken verkrijgen.
  2. Bouw een aanvraagtekst met daarin query die drie EVALUATE statements bevat.
  3. Verzend het verzoek en registreer de onbewerkte Arrow IPC-responsstroom.
  4. Parseer het antwoord in één resultaatset per EVALUATE-instructie.
  5. Geef elke resultatenset weer als PowerShell-objecten.

Prerequisites

  • PowerShell 7.4 of hoger. Windows PowerShell 5.1 wordt niet ondersteund omdat het Apache.Arrow pakket dat in deze zelfstudie wordt gebruikt, conflicteert met de System.Memory assembly die is opgenomen in PowerShell 5.1.
  • Een Power BI werkruimte in Premium- of Fabric-capaciteit met ten minste één semantisch model.
  • Bouw en leesrechten voor het semantische model.
  • De MicrosoftPowerBIMgmt-module voor verificatie. De cmdlets maken gebruik van Microsoft eigen Power BI client-app, zodat u uw eigen app niet hoeft te registreren in Microsoft Entra.
  • De Apache.Arrow- en Apache.Arrow.Compression-.NET-bibliotheken gebruiken om het antwoord te deserialiseren. De REST API 'DAX-query's uitvoeren' comprimeert Arrow-buffers met LZ4-framecompressie, dus Apache.Arrow.Compression en de afhankelijkheden (K4os.Compression.LZ4, K4os.Compression.LZ4.Streams, K4os.Hash.xxHash, ZstdSharp.Port) zijn vereist. In de volgende stap ziet u hoe u ze downloadt.
  • De volgende tenantinstellingen zijn ingeschakeld in de Power BI-beheerportal:
    • REST API voor query’s uitvoeren op gegevenssets (onder Ontwikkelaarsinstellingen).
    • Sta XMLA-eindpunten toe en Analyseren in Excel met on-premises semantische modellen (onder Integratie-instellingen).

Installeer PowerShell 7.4 of hoger met behulp van winget:

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

Start na de installatie de nieuwe shell met pwsh. Voer de resterende opdrachten uit in deze zelfstudie vanuit die sessie.

Installeer de MicrosoftPowerBIMgmt-module. De vlag -Force bevestigt de prompt voor de niet-vertrouwde PowerShell Gallery-opslagplaats.

Install-Module -Name MicrosoftPowerBIMgmt -Scope CurrentUser -Force

Download de vereiste NuGet-pakketten en pak hun assembly's uit naar C:\Tools\Apache.Arrow\. Een .nupkg bestand is een ZIP-archief, dus Expand-Archive werkt er rechtstreeks op. De lus selecteert in elk pakket de hoogst beschikbare netX.0-doelmap, zodat de assemblies compatibel blijven wanneer de pakketten nieuwere doelen uitbrengen.

$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 - Verifiëren

Meld u interactief aan bij de Power BI-service en extraheer vervolgens een toegangstoken. Voor de Connect-PowerBIServiceAccount cmdlet hoeft u uw eigen app niet te registreren in Microsoft Entra.

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

2 - Een verzoek maken met meerdere EVALUATE-instructies

Definieer de werkruimte- en semantische modeldoelen. Stel vervolgens de hoofdtekst van de aanvraag op. De query eigenschap is één tekenreeks die drie EVALUATE instructies bevat, gescheiden door lege regels.

$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 - De aanvraag verzenden en de onbewerkte antwoordstroom vastleggen

Verzend de POST-aanvraag en lees de hoofdtekst van het antwoord als een binaire stroom. Gebruik HttpWebRequest in plaats van Invoke-RestMethod, Invoke-PowerBIRestMethodof Invoke-WebRequest. Het antwoord is een binaire Arrow IPC-gegevensstroom. De PowerShell-cmdlets op een hoger niveau interpreteren antwoordteksten als tekst, waardoor binaire inhoud wordt beschadigd. HttpWebRequest geeft de ruwe stream ongewijzigd terug.

$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 - Het antwoord met meerdere resultaatsets interpreteren

De antwoordtekst is de aaneenschakeling van één Apache Arrow IPC-stroom per EVALUATE statement. PowerShell wordt niet geleverd met een pijlparser, dus met deze stap wordt de Apache.Arrow .NET-bibliotheek geladen via een kleine inline C#-helper die is toegevoegd aan Add-Type. Door de stream-looplogica in C# te houden, blijft de oproepsite kort en wordt een lijst met resultatensets geretourneerd die door uw PowerShell-script kunnen worden herhaald. De helper opent een nieuwe ArrowStreamReader na elke einde-van-stroommarkering, zodat dezelfde lus een willekeurig aantal resultatensets in het antwoord afhandelt.

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 - Werken met elke resultatenset

Converteer elke resultatenset naar PSCustomObject rijen. U kunt de rijen nu doorgeven aan Where-Object, Group-Object, Export-Csv of een andere PowerShell-cmdlet.

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

Elke variabele bevat de rijen uit de bijbehorende EVALUATE instructie, in de volgorde waarin de instructies worden weergegeven in de aanvraag.

Troubleshooting

  • 401 Niet geautoriseerd : het token in de cache is verlopen. Voer Connect-PowerBIServiceAccount opnieuw uit om deze te vernieuwen en lees daarna $accessToken opnieuw uit Get-PowerBIAccessToken.
  • MSAL-waarschuwingen tijdens Connect-PowerBIServiceAccountMicrosoftPowerBIMgmt bundelt een oudere MSAL.NET die interne traceringsberichten verzendt (bijvoorbeeld SetAuthorityUriTryNormalizeRealm, ) MsaDeviceOperationProvider is not availablebij de ernst van de waarschuwing. Ze zijn veilig te negeren zolang de cmdlet het Environment / TenantId / UserName blok afdrukt. Als u deze wilt onderdrukken, geeft u -WarningAction SilentlyContinue op.
  • HTTP 200 met een foutresultatenset : de HTTP-aanvraag is geslaagd, maar de pijlstroom bevat een fout. Inspecteer de metagegevens van het schema voor IsError=trueen lees FaultCode - en FaultString. Zie Best practices voor de REST API voor DAX-query's uitvoeren voor meer informatie.
  • Invoke-RestMethod retourneert vervormde tekst : niet gebruiken Invoke-RestMethod, Invoke-PowerBIRestMethodof Invoke-WebRequest met deze API. Het antwoord is binair; gebruiken HttpWebRequest zoals wordt weergegeven in stap 3.
  • Add-Type kan niet worden geladen Apache.Arrow.dll : bij Windows PowerShell 5.1 conflicteert het Apache.Arrow pakket met de in-box System.Memory assembly. Gebruik PowerShell 7.4 of hoger.
  • Er worden geen of minder resultaatsets geretourneerd dan EVALUATE-instructies — Controleer of elke EVALUATE-instructie syntactisch geldig is. Eén ongeldige EVALUATE zorgt ervoor dat de API een fout teruggeeft in plaats van een gedeeltelijke response met meerdere resultatensets.