Priorityを使ったページングと新着監視の手法

Priorityを使ったページングについてご紹介します

# Priorityとは
データに対して設定する優先度のことで、
queryOrderedByPriorityを使用してデータを並べ替えることができます。
また、設定する優先度は数値または文字列のみで、並べ替えは以下のルールで行われます。
> 
[queryOrderedByPriority](https://firebase.google.com/docs/reference/ios/firebasedatabase/api/reference/Classes/FIRDatabaseQuery#/c:objc(cs)FIRDatabaseQuery(im)queryOrderedByPriority)  

* 優先度を持たない子（デフォルト）が最初に来ます。
* 優先度として数値を持つ子が次に来ます。該当する子は優先度の数値で昇順に並べ替えられます。
* 優先度として文字列を持つ子が最後に来ます。該当する子は優先度で辞書順に並べ替えられます。
* 2 つの子が同じ優先度を持つ場合（ともに優先度を持たない場合も含む）、キーで並べ替えられます。数値キーが最初に来て（数値で昇順に並べ替えられます）、残りのキーが続きます（辞書順で昇順に並べ替えられます）。

> 優先度の値に使用できるのは、数値たは文字列だけです。数値の優先度は IEEE 754 倍精度浮動小数点数として保存され、並べ替えられます。キーは常に文字列として保存され、数値として扱われるのは 32 ビット整数として解析できるときだけです。

## 

ページングUnixTimeを使うので、数値のみ利用します。

# デフォルトのデータの並び順

Firebaseコンソール上でデータを１から順に追加すると以下のようになります。
<img width="307" alt="スクリーンショット 2016-12-20 15.56.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/1901/d6b42d85-d50a-399b-3298-4e5fc3942b48.png">

# UnixTimeを元に出した値をpriorityに設定する

データを書き込む際には、TimestampMaxから時間を引いた値を使用します。

Priorityは値が小さいほど優先度が高い仕様のため、TimestampMax - unixtimeをすることで、値は小さくなります。


```swift
struct FIRPriority {
    static let maxPriority:Int64 = 0
    static let timestampMax:Int64 = 9999999999999 // UnixTimeより大きい値
    func getPriority() -> Double {
        return Double(FIRPriority.timestampMax - Date.currentTimeMillis())
    }
}
```

# 新着取得方法

Priority順に並び替え先頭からLimit分だけ取得するようにします。
最後のoffsetは次のページングする際に利用するので変数で保持しておきます。

```swift
let limit:UInt = 20
var offset = NSNumber(value: 0)
FIRDatabase.database().reference(withPath: "path")
    .queryOrderedByPriority() // priority順に並び替え
    .queryStarting(atValue: offset)
    .queryLimited(toFirst: limit)
    .observeSingleEvent(of: .value, with: { snapshot in
        guard let snapshot = snapshot.children.allObjects as? [FIRDataSnapshot] else {
            return
        }
        offset = snapshot.last?.priority as! NSNumber
    })
```

# ページング

１つ前で説明した新着取得方法とほぼ同じですが、offsetの部分が違います。

```swift
let limit:UInt = 20
var offset = NSNumber(value: 0)
FIRDatabase.database().reference(withPath: "path")
    .queryOrderedByPriority() // priority順に並び替え
    .queryStarting(atValue: NSNumber(value: offset.int64Value + 1)) // 最後に取得したデータの次Priorityを設定
    .queryLimited(toFirst: limit)
    .observeSingleEvent(of: .value, with: { snapshot in
        guard let snapshot = snapshot.children.allObjects as? [FIRDataSnapshot] else {
            return
        }
        offset = snapshot.last?.priority as! NSNumber
    })
```

# 新着監視方法

新着のみをイベントで受け取りたい場合、
これから書き込まれた時に設定される未来のpriority値を範囲として監視すれば良いので
以下のようになります。

```
FIRDatabase.database().reference(withPath: "path")
    .queryOrderedByPriority() // priority順に並び替え
    .queryStarting(atValue: FIRPriority.maxPriority) // 0から
    .queryEnding(atValue: NSNumber(value: offset.int６４Value + 1)) // 最新データのpriorityに+1したもの
    .observe(.childAdded, with: { snapshot in
        var addedCount = 0
        if let data = snapshot.value as? NSDictionary {
            // MEMO:Dict to Object
            addedCount += 1
        }
        print(addedCount)
    })
```

# まとめ

共通化などする余地はありますが、基本的な取得方法はこのようになります。


## 注意

* こちらにもあります通り、REST APIは結果の並び替えに対応していないです。
    * https://firebase.google.com/docs/database/rest/retrieve-data#section-rest-ordered-data
* offset+1しているところで、書き込み頻度がミリ秒単位の場合、priorityが被ることによりデータが抜けたりする可能性があります。

[Original](https://tamazawa.net/2016/12/21/priorityを使ったページングと新着監視の手法/)