ChangeQuery をもう少し実践的に書いてみる

2020/04/18

経緯

前回「SharePoint Online でアイテムの更新状況を取得する(ChangeQuery)」の続き

ChangeQuery を使うことで、SharePoint で行われた変更を取得できることが判ったので、もう少し実践的にコーディングしてみる

ストーリーは、あるサイトのリストとデータベースを同期するという感じで考えてみる

環境

クライアント環境

  • Windows 10 Pro
  • PowerShell 5.1.17134.165

SharePoint Online 環境 (同期元)

  • Site URL: https://<tenant>.sharepoint.com/sites/example

  • リスト名: SyncBase

    列名 内部列名 列のタイプ
    タイトル Title 1行テキスト
    コメント Comment 1行テキスト
  • 初期データとして、200 件登録済み


    こんな感じで、タイトル1 ~ タイトル200 まで

コード

[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client")
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime")

# エラー時に処理を停止する
$ErrorActionPreference = "Stop"

# SharePoint Online の URL
$siteUrl = 'https://<tenant>.sharepoint.com/sites/example'
# ユーザー名
$user = '[email protected]';
# パスワード
$secure = Read-Host -Prompt "Enter the password for ${user}(Office365)" -AsSecureString;

try {
    # SharePoint Online 認証情報
    $credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($user, $secure);
    # SharePoint Client Context インスタンスを生成
    $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteURL)
    $ctx.Credentials = $credentials
} catch {
    # SharePoint に接続できない時、エラーを出して処理を終了する
    Write-Error ($_.Exception.ToString())
    $ctx.Dispose()
    exit
}

ここまではいつものログイン処理


# 処理開始時間
$procstart = Get-Date

# 前回実行時間ファイル
[string]$ProcDatetimeFile = (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'SPSync.datetime')

# 前回の処理開始時間
$ProcessedDatetime = (Get-Date).AddDays(-1) # デフォルト 1 日前
if (Test-Path -Path $ProcDatetimeFile) {
    $ProcessedDatetime = [datetime](Get-Content -Path $ProcDatetimeFile).Trim()
}

処理開始時間は、本処理が成功した時に前回実行時間として保存するために取得しています

前回実行時間が保存されているファイルは、スクリプトファイルと同じディレクトリに SPSync.datetime として保存されているとし、なければ 1 日前と決め打ちしています


Write-Host ("前回実行時間: {0}" -f $ProcessedDatetime)

# 対象リスト情報を読み込む
$list = $ctx.Web.Lists.GetByTitle('SyncBase')
$ctx.Load($list)
$ctx.ExecuteQuery()

後ほどリストの ID を使うので、先に SyncBase リストの情報を取得しています


# ChangeQueryの定義
$cq = [Microsoft.SharePoint.Client.ChangeQuery]::new($false, $false)
$cq.Item = $true          # アイテムの更新が対象
$cq.Add = $true           # 追加したアイテム情報が対象
$cq.Update = $true        # 修正したアイテム情報が対象
$cq.DeleteObject = $true  # 削除したアイテム情報が対象
$cq.FetchLimit = 100      # 100 件づつ取得

前回と同じで、アイテムの更新(追加・修正・削除)を対象とします

FetchLimit を設定して、100 件づつ取得するようにしています
ちなみに FetchLimit は 2000 が上限です


# ChangeToken オブジェクトの生成
$cq.ChangeTokenStart = [Microsoft.Sharepoint.Client.ChangeToken]::new()

# ChangeTokenStart に前回実行時間をセット
$cq.ChangeTokenStart.StringValue = ("1;3;{0};{1};-1" -f $list.Id.ToString(), ($ProcessedDatetime.ToUniversalTime().Ticks.ToString()))

範囲を指定して変更ログを読み取るためには、ChangeToken オブジェクトを使うため生成しています

ChangeTokenStart に前回実行時間をセットする、ChangeToken.StringValue を書式に沿って生成しています

ChangeToken.StringValue の書式は次の通り

例: 1;3;xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;636678394680000000;-1

  1. バージョン番号
  2. スコープ(0: Content Database、1: Site Collection、2: Site、3: List)
  3. スコープで指定したリソースの GUID
  4. 時間
  5. ChangeToken の変更アイテム値(-1 で初期化)

try {

    # 永久ループ、リミッターを付けるために for 文でも良い
    while(1)
    {
        # 対象リストから更新のあったアイテムを取得
        $changeItems = $list.GetChanges($cq)
        $ctx.Load($changeItems)
        $ctx.ExecuteQuery()

        Write-Host ("Change Item Count: {0}" -f $changeItems.Count.ToString())

        # 前回実行時から変更されたアイテムがない
        if ($changeItems.Count -eq 0) {
            break
        }

        foreach($item in $changeItems){
            switch ($item.ChangeType) {
                "Add" {
                    Write-Host ("ChangeType Add.  ItemId : {0}" -f $item.ItemId)
                }
                "Update" {
                    Write-Host ("ChangeType Update. ItemId : {0}" -f $item.ItemId)
                }
                "DeleteObject" {
                    Write-Host ("ChangeType Delete. ItemId : {0}" -f $item.ItemId)
                }
            }
            # 最後のトークンを取得する
            $lastToken = ([Microsoft.SharePoint.Client.Change]$item).ChangeToken.StringValue
        }
        # 取得した最後のアイテムの ChangeToken.StringValue をセット
        $cq.ChangeTokenStart.StringValue = $lastToken
    }

    # 処理が正常終了したら処理開始時間をファイルに書き込む
    $procstart.ToString('yyyy/MM/dd HH:mm:ss') | Out-File -FilePath $ProcDatetimeFile

} catch {
    # 例外が出たらエラーを出す
    Write-Error ($_.Exception.ToString())

} finally {
    # Context を破棄
    $ctx.Dispose()

}

ポイントは、 「最後のトークンを取得する」 の次の行で、処理したアイテムの ChangeToken を取得して、再度 $list.GetChanges($cq) する前に、ChangeTokenStart.StringValue$lastToken をセットしていること

こうすることで、1 度に 2000 件(FetchLimit の上限)までしか取得できない ChangeQuery を繰り返して取得することができる

実行結果

リスト SyncBase にアイテムを 200 件追加して実行

前回実行時間: 2018/07/21 21:53:19
Change Item Count: 100
ChangeType Add.  ItemId : 1
ChangeType Add.  ItemId : 2
 :
(省略)
 :
ChangeType Add.  ItemId : 99
ChangeType Add.  ItemId : 100
Change Item Count: 100
ChangeType Add.  ItemId : 101
ChangeType Add.  ItemId : 102
 :
(省略)
 :
ChangeType Add.  ItemId : 199
ChangeType Add.  ItemId : 200
Change Item Count: 0

100 件づつのページングを経て、200 件の変更ログが取得できている

リスト SyncBase の 1 件目 と 200 件目のみ更新して実行

前回実行時間: 2018/07/22 21:53:19
Change Item Count: 2
ChangeType Update. ItemId : 1
ChangeType Update. ItemId : 200
Change Item Count: 0

これにより、アイテム 100 件づつページングが必要ではなく、更新ログ 100 件づつでページングが必要ということが判る

考察

少しまともに書こうと思ったら長くなってしまった… _(:3」∠)_

サンプルコード一式は Github に置いてます