Samouczek: uruchamianie wielu instrukcji EVALUATE przy użyciu programu PowerShell

W tym samouczku użyjesz PowerShell do przesłania pojedynczego żądania do interfejsu API REST Execute DAX Queries, zawierającego wiele poleceń EVALUATE, a następnie przeanalizujesz odpowiedź Apache Arrow z wieloma zestawami wyników. Ten wzorzec umożliwia pobranie kilku powiązanych zestawów wyników w jednej rundzie ze skryptu automatyzacji programu PowerShell.

Diagram przedstawiający jedno żądanie HTTP POST zawierające trzy instrukcje EVALUATE w treści zapytania oraz odpowiedź IPC strzałki zawierająca trzy zestawy wyników w tej samej kolejności.

Dlaczego prześlij wiele instrukcji EVALUATE w jednym żądaniu

Interfejs API wykonywania zapytań języka DAX akceptuje jeden query ciąg, który może zawierać wiele EVALUATE instrukcji. Każda instrukcja zwraca własny zestaw wyników, a treść odpowiedzi jest konkatenacją jednego strumienia Arrow IPC dla każdej instrukcji EVALUATE, zgodnie z kolejnością deklaracji. Przesyłanie powiązanych zapytań razem pozwala uniknąć narzutu związanego z każdym żądaniem przy oddzielnych wywołaniach HTTP, w tym dodatkowej walidacji tokenu Microsoft Entra i inicjalizacji aparatu DAX. Wysyłanie wielu EVALUATE poleceń w jednym żądaniu może również pomóc ograniczyć skutki ograniczania liczby żądań. Power BI ogranicza liczbę wywołań do 120 żądań zapytań na minutę na użytkownika na potrzeby operacji zapytań modelu semantycznego.

Co tworzysz

W jednym skrypcie programu PowerShell:

  1. Uzyskaj token dostępu Microsoft Entra.
  2. Utwórz treść żądania, której query zawiera trzy instrukcje EVALUATE.
  3. Wyślij żądanie i przechwyć surowy strumień odpowiedzi Arrow IPC.
  4. Przetwórz odpowiedź na jeden zestaw wyników dla każdej instrukcji EVALUATE.
  5. Wyświetl każdy zestaw wyników jako obiekty programu PowerShell.

Wymagania wstępne

  • Program PowerShell w wersji 7.4 lub nowszej. Program Windows PowerShell 5.1 nie jest obsługiwany, ponieważ pakiet Apache.Arrow używany w tym samouczku koliduje z zestawem System.Memory dołączonym do programu PowerShell 5.1.
  • Obszar roboczy Power BI w pojemności Premium lub Fabric z co najmniej jednym modelem semantycznym.
  • Uprawnienia kompilacji i odczytu w modelu semantycznym.
  • Moduł MicrosoftPowerBIMgmt na potrzeby uwierzytelniania. Polecenia cmdlet używają własnej aplikacji klienckiej Power BI firmy Microsoft, więc nie trzeba rejestrować własnej aplikacji w usłudze Microsoft Entra.
  • Biblioteki .NET Apache.Arrow i Apache.Arrow.Compression służące do deserializacji odpowiedzi. Interfejs API REST Execute DAX Queries kompresuje bufory Arrow przy użyciu kompresji ramek LZ4, więc Apache.Arrow.Compression i jego zależności (K4os.Compression.LZ4, K4os.Compression.LZ4.Streams, K4os.Hash.xxHash, ZstdSharp.Port) są wymagane. W następnym kroku pokazano, jak je pobrać.
  • Następujące ustawienia dzierżawy włączone w portalu administracyjnym Power BI:
    • Interfejs API REST wykonywania zapytań zestawu danych (w obszarze Ustawienia dewelopera).
    • ** Zezwól na punkty końcowe XMLA i analizowanie w Excelu za pomocą lokalnych modeli semantycznych (w obszarze Ustawienia integracji).

Zainstaluj program PowerShell w wersji 7.4 lub nowszej przy użyciu zestawu narzędzi winget:

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

Po instalacji uruchom nową powłokę poleceniem pwsh. Uruchom pozostałe polecenia w tym samouczku z tej sesji.

Zainstaluj moduł MicrosoftPowerBIMgmt. Przełącznik -Force akceptuje monit dotyczący niezaufanego repozytorium w usłudze Galeria programu PowerShell.

Install-Module -Name MicrosoftPowerBIMgmt -Scope CurrentUser -Force

Pobierz wymagane pakiety NuGet i wyodrębnij ich biblioteki do C:\Tools\Apache.Arrow\. Plik .nupkg to archiwum ZIP, więc Expand-Archive działa na nim bezpośrednio. Pętla wybiera najwyższy netX.0 folder docelowy w każdym pakiecie, aby zestawy były zgodne, ponieważ pakiety publikują nowsze elementy docelowe.

$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 — Uwierzytelnianie

Zaloguj się do usługa Power BI interaktywnie, a następnie wyodrębnij token dostępu. Polecenie cmdlet Connect-PowerBIServiceAccount nie wymaga rejestrowania własnej aplikacji w Microsoft Entra.

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

2 — Tworzenie żądania z wieloma instrukcjami EVALUATE

Zdefiniuj obszary robocze i elementy docelowe modelu semantycznego. Następnie skompiluj treść żądania. Właściwość query jest pojedynczym ciągiem, który zawiera trzy EVALUATE instrukcje oddzielone pustymi wierszami.

$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 — Wysyłanie żądania i przechwytywanie nieprzetworzonego strumienia odpowiedzi

Wyślij żądanie POST i odczytaj treść odpowiedzi jako strumień binarny. Użyj HttpWebRequest zamiast Invoke-RestMethod, Invoke-PowerBIRestMethodlub Invoke-WebRequest. Odpowiedź jest binarnym strumieniem IPC Apache Arrow. Polecenia cmdlet programu PowerShell wyższego poziomu interpretują treść odpowiedzi jako tekst, który uszkadza zawartość binarną. HttpWebRequest zwraca surowy strumień bez modyfikacji.

$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 - Przeanalizuj odpowiedź zawierającą wiele zestawów wyników

Treść odpowiedzi stanowi konkatenacja jednego strumienia IPC Apache Arrow dla każdej instrukcji EVALUATE. PowerShell nie zawiera parsera Arrow, więc w tym kroku ładowana jest biblioteka .NET Apache.Arrow przy użyciu niewielkiej, wbudowanej klasy pomocniczej C# dodanej za pomocą Add-Type. Pozostawienie logiki pętli strumienia danych w języku C# sprawia, że miejsce wywołania jest krótkie i zwraca listę zbiorów wyników, po której skrypt PowerShell może iterować. Pomocnik otwiera nowy ArrowStreamReader po każdym znaczniku końca strumienia, więc ta sama pętla obsługuje dowolną liczbę zestawów wyników w odpowiedzi.

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 — Praca z każdym zestawem wyników

Przekonwertuj każdy zestaw wyników na PSCustomObject wiersze. Teraz możesz przepuścić wiersze przez Where-Object, Group-Object, Export-Csv lub dowolne inne polecenie cmdlet programu 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

Każda zmienna przechowuje wiersze z odpowiedniej EVALUATE instrukcji w kolejności, w której instrukcje są wyświetlane w żądaniu.

Troubleshooting

  • 401 Brak autoryzacji — token buforowany wygasł. Uruchom ponownie Connect-PowerBIServiceAccount, aby je odświeżyć, a następnie ponownie odczytaj $accessToken z Get-PowerBIAccessToken.
  • Ostrzeżenia biblioteki MSAL w trakcie Connect-PowerBIServiceAccountMicrosoftPowerBIMgmt zawiera starszą wersję biblioteki MSAL.NET, która emituje wewnętrzne komunikaty śledzenia (na przykład SetAuthorityUri, TryNormalizeRealm, MsaDeviceOperationProvider is not available) na poziomie ostrzeżenia. Można je bezpiecznie zignorować, o ile cmdlet wyświetli blok Environment / TenantId / UserName. Aby je wyłączyć, przekaż -WarningAction SilentlyContinue.
  • HTTP 200 z błędnym zestawem wyników — Żądanie HTTP zakończyło się powodzeniem, ale strumień Arrow zawiera błąd. Sprawdź metadane schematu elementu IsError=true i odczytaj FaultCode oraz FaultString. Aby uzyskać szczegółowe informacje, zobacz Najlepsze rozwiązania dotyczące interfejsu API REST wykonywania zapytań języka DAX.
  • Invoke-RestMethod zwraca zniekształcony tekst — nie używaj Invoke-RestMethod, Invoke-PowerBIRestMethod ani Invoke-WebRequest z tym interfejsem API. Odpowiedź jest binarna; użyj polecenia HttpWebRequest , jak pokazano w kroku 3.
  • Add-Type nie może załadować Apache.Arrow.dll — W programie Windows PowerShell 5.1 pakiet Apache.Arrow powoduje konflikt z wbudowanym zestawem System.Memory. Użyj programu PowerShell w wersji 7.4 lub nowszej.
  • Nie zwrócono żadnych zestawów wyników lub zwrócono ich mniej niż EVALUATE instrukcji — Upewnij się, że każda EVALUATE instrukcja jest składniowo poprawna sama w sobie. Pojedynczy nieprawidłowy element EVALUATE powoduje, że interfejs API zwraca błąd zamiast częściowej odpowiedzi zawierającej wiele zestawów wyników.