共用方式為


TripPin 第 7 部分 - 具有 M 類型的進階架構

注意

此內容目前會參考 Visual Studio 中單元測試舊版實作的內容。 內容將於近期更新,以涵蓋新的 Power Query SDK測試架構

此多部分教學課程涵蓋如何建立Power Query的新數據源延伸模組。 本教學課程旨在循序完成,每個課程都是以先前課程中建立的連接器為基礎,以累加方式將新功能新增至您的連接器。

在本課程中,您將會:

在上一課,您使用簡單的「架構數據表」系統來定義數據表架構。 此架構數據表方法適用於許多 REST API/資料 連線 器,但傳回完整或深度巢狀數據集的服務,可能會受益於本教學課程中運用 M 類型系統的方法。

本課程將引導您完成下列步驟:

  1. 新增單元測試。
  2. 定義自定義 M 類型。
  3. 使用型別強制執行架構。
  4. 將一般程式代碼重構為個別的檔案。

新增單元測試

開始使用進階架構邏輯之前,您會在連接器中新增一組單元測試,以減少意外中斷項目的機會。 單元測試的運作方式如下:

  1. 將一般程式代碼從 UnitTest 範例 複製到您的 TripPin.query.pq 檔案。
  2. 將區段宣告新增至檔案 TripPin.query.pq 頂端。
  3. 建立 共享 記錄(稱為 TripPin.UnitTest)。
  4. Fact為每個測試定義 。
  5. 呼叫 Facts.Summarize() 以執行所有測試。
  6. 參考上一個呼叫做為共用值,以確保在Visual Studio中執行專案時會進行評估。
section TripPinUnitTests;

shared TripPin.UnitTest =
[
    // Put any common variables here if you only want them to be evaluated once
    RootTable = TripPin.Contents(),
    Airlines = RootTable{[Name="Airlines"]}[Data],
    Airports = RootTable{[Name="Airports"]}[Data],
    People = RootTable{[Name="People"]}[Data],

    // Fact(<Name of the Test>, <Expected Value>, <Actual Value>)
    // <Expected Value> and <Actual Value> can be a literal or let statement
    facts =
    {
        Fact("Check that we have three entries in our nav table", 3, Table.RowCount(RootTable)),
        Fact("We have Airline data?", true, not Table.IsEmpty(Airlines)),
        Fact("We have People data?", true, not Table.IsEmpty(People)),
        Fact("We have Airport data?", true, not Table.IsEmpty(Airports)),
        Fact("Airlines only has 2 columns", 2, List.Count(Table.ColumnNames(Airlines))),        
        Fact("Airline table has the right fields",
            {"AirlineCode","Name"},
            Record.FieldNames(Type.RecordFields(Type.TableRow(Value.Type(Airlines))))
        )
    },

    report = Facts.Summarize(facts)
][report];

在專案上選取 [執行] 會評估所有 [事實],並提供如下所示的報表輸出:

初始單元測試。

使用測試驅動開發的一些原則,您現在會新增目前失敗的測試,但很快就會重新實作和修正(在本教學課程結束時)。 具體而言,您將新增測試,以檢查您在 人員 實體中返回的其中一個巢狀記錄(電子郵件)。

Fact("Emails is properly typed", type text, Type.ListItem(Value.Type(People{0}[Emails])))

如果您再次執行程式代碼,現在應該會看到您有失敗的測試。

失敗的單元測試。

現在,您只需要實作功能,才能讓這項工作。

定義自定義 M 類型

上一課架構強制執行方法使用了定義為名稱/類型組的「架構數據表」。 使用扁平化/關係型數據時,它運作良好,但不支援在巢狀記錄/數據表/清單上設定類型,或可讓您跨數據表/實體重複使用類型定義。

在 TripPin 案例中,人員 和 Airports 實體中的數據包含結構化數據行,甚至共用類型 (Location) 來代表地址資訊。 您將使用自定義 M 類型宣告來定義每個實體,而不是在架構數據表中定義名稱/類型組。

以下是語言規格中 M 語言類型的快速重新整理:

「類型值」是「分類」其他值的值。 由類型分類的值稱為「符合」該類型。 M 類型系統由下列類型種類組成:

  • 基本型別,分類基本值 (binary、 、 datetimedatedatetimezone、 、 typelistnumbernullrecordtextlogicaldurationtime和 ) 也包含一些抽象型別 (functiontableany、 和 )none
  • 記錄類型,其根據欄位名稱和值類型來分類記錄值
  • 清單類型,其使用單一項目基底類型來分類清單
  • 函式類型,其根據函式的參數和傳回值來分類函式值
  • 資料表類型,其根據資料行名稱、資料行類型和索引鍵來分類資料表值
  • 可為 Null 的型別,其分類 Null 值和所有由基底類型分類的值
  • 類型類型,其分類本身是類型的值

使用您取得的原始 JSON 輸出(以及/或查閱服務$metadata中的定義),您可以定義下列記錄類型來代表 OData 複雜類型:

LocationType = type [
    Address = text,
    City = CityType,
    Loc = LocType
];

CityType = type [
    CountryRegion = text,
    Name = text,
    Region = text
];

LocType = type [
    #"type" = text,
    coordinates = {number},
    crs = CrsType
];

CrsType = type [
    #"type" = text,
    properties = record
];

請注意 參考 LocationType CityTypeLocType 表示其結構化數據行的方式。

針對最上層實體(您想要以數據表表示),您可以定義 資料表類型

AirlinesType = type table [
    AirlineCode = text,
    Name = text
];

AirportsType = type table [
    Name = text,
    IataCode = text,
    Location = LocationType
];

PeopleType = type table [
    UserName = text,
    FirstName = text,
    LastName = text,
    Emails = {text},
    AddressInfo = {nullable LocationType},
    Gender = nullable text,
    Concurrency = Int64.Type
];

接著,您會更新 SchemaTable 變數(以做為實體對類型對應的「查閱表格」,以使用這些新的類型定義:

SchemaTable = #table({"Entity", "Type"}, {
    {"Airlines", AirlinesType },    
    {"Airports", AirportsType },
    {"People", PeopleType}    
});

使用類型強制執行架構

您將依賴一般函式 (Table.ChangeType) 來對您的資料強制執行架構,就像您在上一課中使用的SchemaTransformTable一樣。 不同於 SchemaTransformTableTable.ChangeType會採用實際的 M 數據表類型做為自變數,並將所有巢狀類型以遞歸方式套用您的架構。 其簽章看起來像這樣:

Table.ChangeType = (table, tableType as type) as nullable table => ...

您可以在 Table.ChangeType.pqm 檔案中找到函式的完整程式代碼清單Table.ChangeType

注意

為了彈性,函式可用於數據表,以及記錄清單(也就是數據表在 JSON 檔中的表示方式)。

接著,您必須更新連接器程式代碼,將 參數從 table 變更schematype,並在 中新增 對 Table.ChangeTypeGetEntity呼叫。

GetEntity = (url as text, entity as text) as table => 
    let
        fullUrl = Uri.Combine(url, entity),
        schema = GetSchemaForEntity(entity),
        result = TripPin.Feed(fullUrl, schema),
        appliedSchema = Table.ChangeType(result, schema)
    in
        appliedSchema;

GetPage 會更新為使用架構中的欄位清單(若要知道取得結果時要展開的內容名稱),但會將實際的架構強制執行保留為 GetEntity

GetPage = (url as text, optional schema as type) as table =>
    let
        response = Web.Contents(url, [ Headers = DefaultRequestHeaders ]),        
        body = Json.Document(response),
        nextLink = GetNextLink(body),
        
        // If we have no schema, use Table.FromRecords() instead
        // (and hope that our results all have the same fields).
        // If we have a schema, expand the record using its field names
        data =
            if (schema <> null) then
                Table.FromRecords(body[value])
            else
                let
                    // convert the list of records into a table (single column of records)
                    asTable = Table.FromList(body[value], Splitter.SplitByNothing(), {"Column1"}),
                    fields = Record.FieldNames(Type.RecordFields(Type.TableRow(schema))),
                    expanded = Table.ExpandRecordColumn(asTable, fields)
                in
                    expanded
    in
        data meta [NextLink = nextLink];

確認正在設定巢狀類型

PeopleType 現在的定義會將 Emails 欄位設定為文字清單({text})。 如果您要正確套用類型,則單元測試中對 Type.ListItem 的呼叫現在應該會傳type text回 ,而不是 type any

再次執行單元測試會顯示它們現在全都通過。

成功進行單元測試。

將一般程式代碼重構成個別的檔案

注意

M 引擎將改善未來參考外部模組/一般程式代碼的支援,但此方法應該會帶您到那時為止。

此時,您的延伸模塊幾乎具有與 TripPin 連接器程式代碼一樣多的「通用」程式代碼。 未來,這些 通用函 式會是內建標準函式連結庫的一部分,或者您可以從另一個延伸模塊參考它們。 現在,您會以下列方式重構程序代碼:

  1. 將可重複使用的函式移至個別的檔案 (.pqm)。
  2. 檔案上的 [建置動作] 屬性設定為 [ 編譯 ],以確保它在建置期間會包含在延伸模塊檔案中。
  3. 定義函式以使用 Expression.Evaluate 載入程式代碼。
  4. 載入您想要使用的每個通用函式。

執行此動作的程式代碼包含在下列代碼段中:

Extension.LoadFunction = (fileName as text) =>
  let
      binary = Extension.Contents(fileName),
      asText = Text.FromBinary(binary)
  in
      try
        Expression.Evaluate(asText, #shared)
      catch (e) =>
        error [
            Reason = "Extension.LoadFunction Failure",
            Message.Format = "Loading '#{0}' failed - '#{1}': '#{2}'",
            Message.Parameters = {fileName, e[Reason], e[Message]},
            Detail = [File = fileName, Error = e]
        ];

Table.ChangeType = Extension.LoadFunction("Table.ChangeType.pqm");
Table.GenerateByPage = Extension.LoadFunction("Table.GenerateByPage.pqm");
Table.ToNavigationTable = Extension.LoadFunction("Table.ToNavigationTable.pqm");

結論

本教學課程對從 REST API 取得的數據強制執行架構的方式進行了多項改善。 連接器目前正硬式編碼其架構資訊,其在運行時間具有效能優勢,但無法適應服務元數據加班的變更。 未來的教學課程會移至純動態方法,以從服務的$metadata檔推斷架構。

除了架構變更之外,本教學課程還新增了程式碼的單元測試,並將常見的協助程式函式重構為個別檔案,以改善整體可讀性。

下一步

TripPin 第 8 部分 - 新增診斷