超お父さんの日記

日記とか雑談とか、スノーボードとか、サーフィンとか、たまに技術系とか。

【Amplify iOS】DynamoDBのGlobal Secondary Indexを使いたい

AmplifyのDataStoreはとてもすごい。
どうすごいかは公式のブログに書いてある。

aws.amazon.com

そんな便利ですごいDataStoreだけど、データのソートってどうやるの?
例えばゲームスコアのランキング。

DataStoreでデータ持ってきてから、こちらでソートすることもできるけど、
全部のデータ持ってきてソートするとか、ちょっと。
予めソートされたデータを持ってきたい。

DataStoreのデータはDynamoDBと同期されているので、
DynamoDBのGlobal Secondary Index(以下、GSI)を使ってデータ持って来れればイケる気がする。

調べてみると、schema.graphqlでtypeに@keyをつけるとGSIを設定できるみたい。
こちらの記事、とても参考になりました。ありがとうございました。
qiita.com

実際に以下のようなスキーマを定義してみると、、、

type GameScore @model
@key(name: "rankingIndex", fields: ["gameId", "score"], queryField: "rankingQuery")
{
  id: ID!
  userId: String!
  gameId: String!
  score: Int!
}

amplify pushして、DynamoDBを見るとrankingIndexというGSIが作られている!
AppSyncの管理画面でSchemaを見ると rankingQuery という名前のQueryがあり、そのリゾルバーを見るとrankingIndexを使っているっぽい!

と、ここまで順調だったのだけど、DataStoreでGSIのクエリをどう呼び出せばよいのか・・・
ググってもズバリな情報は見つけられず。
こうなったらと、Amplify iOSgithubに質問を投げてみた。

How can I fetch the sorted data using the DynamoDB Global Secondary Index? · Issue #565 · aws-amplify/amplify-ios · GitHub

DataStoreでソートする機能はいつか実装されるようです。
DataStoreに拘らず、他に方法はないのかという質問は返ってこず、
機能リクエストのタグが付けられて終わってしまいました。。
英語が何かダメだったのかな。。?

DataStoreはもう無理そうなので諦めて、APIの機能で rankingQuery を呼び出せないかということで、
Amplify.API.query周りのコードを読み進めてみた。
けどAmplify.APIのquery処理に任意のクエリrankingQueryを呼び出せるような仕組みは無さそうだった。 *1
ただ、少し弄ればできそうだったので、自作することにした。

GraphQLRequestを生成するとこで、いろんなDecoratorクラスが生成され、
生成されたDecoratorクラスたちがGraphQLを呼び出す際のクエリ文字列などを生成していた。

なので、rankingQuery呼び出し用のDecoratorクラスを作り、GraphQLRequestを拡張してrankingメソッドを追加した。
こんな感じ。

public struct RankingIndexDecorator: ModelBasedGraphQLDocumentDecorator {
    
    enum SortDirection: String {
        case asc = "ASC"
        case desc = "DESC"
    }
    
    let gameId: String
    let queryField = "rankingQuery"
    var greaterThan: Double = 0
    var sortDirection: SortDirection = .asc
    
    public func decorate(_ document: SingleDirectiveGraphQLDocument, modelType: Model.Type) -> SingleDirectiveGraphQLDocument {
        
        var inputs = document.inputs
        inputs["gameId"] = GraphQLDocumentInput(type: "String", value: .scalar(gameId))
        inputs["score"] = GraphQLDocumentInput(type: "ModelFloatKeyConditionInput", value: .object(["gt" : greaterThan]))
        inputs["sortDirection"] = GraphQLDocumentInput(type: "ModelSortDirection", value: .scalar(sortDirection.rawValue))
        
        return document.copy(name: queryField, inputs: inputs)
    }
}
extension GraphQLRequest {
    
    public static func ranking<M: Model>(_ modelType: M.Type,
                                         gameId: String,
                                         scoreGreaterThan: Double = 0,
                                         where predicate: QueryPredicate? = nil) -> GraphQLRequest<[M]> {
        var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelType: modelType, operationType: .query)
        documentBuilder.add(decorator: RankingIndexDecorator(gameId: gameId, greaterThan: scoreGreaterThan))
        
        if let predicate = predicate {
            documentBuilder.add(decorator: FilterDecorator(filter: predicate.graphQLFilter))
        }
        
        documentBuilder.add(decorator: PaginationDecorator())
        let document = documentBuilder.build()
        
        return GraphQLRequest<[M]>(document: document.stringValue,
                                   variables: document.variables,
                                   responseType: [M].self,
                                   decodePath: document.name + ".items")
    }
}

で、呼び出すのは

Amplify.API.query(request: .ranking(GameScore.self, gameId: "HogeGame"))

で動かしてみると、、、ソートされてデータが取得できた!!
めでたし。めでたし。

他にちゃんとした方法があるようでしたら、どなたか教えて欲しいです><

*1:podのAmplifyのバージョンは1.0.3