Esercitazione: eseguire più istruzioni EVALUATE con PowerShell

In questa esercitazione, si usa PowerShell per inviare una singola richiesta API REST Execute DAX Queries che contiene più istruzioni EVALUATE, e quindi analizzare la risposta Apache Arrow con più set di risultati. Questo modello consente di recuperare diversi set di risultati correlati in un round trip da uno script di automazione di PowerShell.

Diagramma che mostra una richiesta HTTP POST contenente tre istruzioni EVALUATE nel corpo della query e la risposta IPC Arrow contenente tre set di risultati nello stesso ordine.

Perché inviare più istruzioni EVALUATE in una sola richiesta

L'API Execute DAX Queries accetta un'unica stringa query che può contenere più istruzioni EVALUATE. Ogni istruzione restituisce il proprio insieme di risultati, e il corpo della risposta è la concatenazione di un flusso Arrow IPC per ogni istruzione EVALUATE in ordine di dichiarazione. L'invio di query correlate consente di evitare il sovraccarico per richiesta di chiamate HTTP separate, inclusa la convalida aggiuntiva dei token Microsoft Entra e l'inizializzazione del motore DAX. L'invio di più EVALUATE istruzioni in un'unica richiesta può anche contribuire ad attenuare l'impatto della limitazione del numero di richieste. Power BI limita i chiamanti a 120 richieste di query al minuto per utente per le operazioni di query del modello semantico.

Cosa costruisci

In uno script di PowerShell è possibile:

  1. Acquisire un token di accesso Microsoft Entra.
  2. Creare il corpo di una richiesta il cui query contiene tre istruzioni EVALUATE.
  3. Inviare la richiesta e acquisire il flusso di risposta grezzo di Arrow IPC.
  4. Analizzare la risposta in un set di risultati per ogni istruzione EVALUATE.
  5. Visualizzare ogni set di risultati come oggetti di PowerShell.

Prerequisiti

  • PowerShell 7.4 o versione successiva. Windows PowerShell 5.1 non è supportato perché il Apache.Arrow pacchetto usato in questa esercitazione è in conflitto con l'System.Memoryassembly incluso in PowerShell 5.1.
  • Un'area di lavoro Power BI in capacità Premium o Fabric con almeno un modello semantico.
  • Autorizzazioni di compilazione e lettura per il modello semantico.
  • Modulo MicrosoftPowerBIMgmt per l'autenticazione. I cmdlet usano l'app client Power BI proprietaria di Microsoft, quindi non è necessario registrare una propria app in Microsoft Entra.
  • Le librerie Apache.Arrow e Apache.Arrow.Compression .NET per deserializzare la risposta. L'API REST Execute DAX Queries comprime i buffer Arrow con la compressione frame LZ4, quindi sono necessari Apache.Arrow.Compression e le relative dipendenze (K4os.Compression.LZ4, K4os.Compression.LZ4.Streams, K4os.Hash.xxHash, ZstdSharp.Port). Il passaggio successivo illustra come scaricarli.
  • Le impostazioni del tenant seguenti sono abilitate nel portale di amministrazione di Power BI:
    • API REST per l'esecuzione di query del set di dati (in Impostazioni per sviluppatori).
    • Consenti endpoint XMLA e Analizza in Excel con modelli semantici locali (nelle Impostazioni di integrazione).

Installare PowerShell 7.4 o versione successiva usando winget:

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

Dopo l'installazione, avviare la nuova shell con pwsh. Esegui i restanti comandi in questa esercitazione in quella sessione.

Installare il modulo MicrosoftPowerBIMgmt. Il flag -Force accetta la richiesta di conferma del repository non attendibile di PowerShell Gallery.

Install-Module -Name MicrosoftPowerBIMgmt -Scope CurrentUser -Force

Scarica i pacchetti NuGet necessari ed estrai gli assembly in C:\Tools\Apache.Arrow\. Un .nupkg file è un archivio ZIP, quindi Expand-Archive funziona direttamente su di esso. Il ciclo seleziona la cartella di destinazione netX.0 di livello più alto in ogni pacchetto, in modo che gli assembly rimangano compatibili man mano che i pacchetti pubblicano target più recenti.

$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 - Autenticare

Accedere al servizio Power BI in modo interattivo, quindi estrarre un token di accesso. Il Connect-PowerBIServiceAccount cmdlet non richiede di registrare la propria app in Microsoft Entra.

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

2 - Crea una richiesta con più istruzioni EVALUATE

Definire le destinazioni dell'area di lavoro e del modello semantico. Quindi, creare il corpo della richiesta. La query proprietà è una singola stringa che contiene tre EVALUATE istruzioni separate da righe vuote.

$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 - Inviare la richiesta e acquisire il flusso di risposta non elaborato

Inviare la richiesta POST e leggere il corpo della risposta come flusso binario. Usare HttpWebRequest anziché Invoke-RestMethod, Invoke-PowerBIRestMethodo Invoke-WebRequest. La risposta è un flusso IPC Arrow binario. I cmdlet di PowerShell di livello superiore interpretano i corpi di risposta come testo, che danneggia il contenuto binario. HttpWebRequest restituisce il flusso originale senza modificarlo.

$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 - Analizzare la risposta con più set di risultati

Il corpo della risposta è la concatenazione di un flusso IPC di Apache Arrow per ogni istruzione EVALUATE. PowerShell non viene fornito con un parser Arrow, quindi questo passaggio carica la Apache.Arrow libreria .NET tramite un helper C# inline piccolo aggiunto con Add-Type. Mantenere in C# la logica di iterazione del flusso mantiene breve il punto di chiamata e restituisce un elenco di set di risultati che lo script PowerShell può scorrere. La funzione di supporto apre un nuovo ArrowStreamReader dopo ogni marcatore di fine flusso, quindi lo stesso ciclo gestisce qualsiasi numero di insiemi di risultati nella risposta.

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 - Lavorare con ogni set di risultati

Convertire ogni set di risultati in PSCustomObject righe. Ora puoi passare tramite pipe le righe a Where-Object, Group-Object, Export-Csv o qualsiasi altro cmdlet di 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

Ogni variabile contiene le righe dell'istruzione corrispondente EVALUATE , nell'ordine in cui le istruzioni vengono visualizzate nella richiesta.

Troubleshooting

  • 401 Non autorizzato : il token memorizzato nella cache è scaduto. Eseguire Connect-PowerBIServiceAccount di nuovo per aggiornarlo, quindi ripetere la lettura $accessToken da Get-PowerBIAccessToken.
  • Avvisi MSAL durante Connect-PowerBIServiceAccountMicrosoftPowerBIMgmt raggruppa un MSAL.NET meno recente che genera messaggi di traccia interni (ad esempio, SetAuthorityUri, TryNormalizeRealm, MsaDeviceOperationProvider is not available) con gravità di avviso. Si possono ignorare tranquillamente, purché il cmdlet stampi il blocco Environment / TenantId / UserName. Per sopprimerli, specifica -WarningAction SilentlyContinue.
  • HTTP 200 con un set di risultati di errore : la richiesta HTTP è riuscita, ma il flusso Arrow genera un errore. Esaminare i metadati dello schema per IsError=truee leggere FaultCode e FaultString. Per ulteriori informazioni, vedi Procedure consigliate per Execute DAX Queries REST API.
  • Invoke-RestMethod restituisce testo non crittografato : non usare Invoke-RestMethod, Invoke-PowerBIRestMethodo Invoke-WebRequest con questa API. La risposta è binaria; usare HttpWebRequest come illustrato nel passaggio 3.
  • Add-Type non riesce a essere caricato Apache.Arrow.dll : in Windows PowerShell 5.1 il Apache.Arrow pacchetto è in conflitto con l'assembly in-boxSystem.Memory. Usare PowerShell 7.4 o versione successiva.
  • Nessun set di risultati restituito o un numero di set di risultati inferiore rispetto alle EVALUATE istruzioni — Verificare che ogni EVALUATE istruzione sia sintatticamente valida di per sé. Un singolo EVALUATE non valido fa restituire all'API un errore anziché una risposta parziale con più set di risultati.