[Parse][iOS] PFObjectの保存・更新・削除の基本

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

前回の記事ではToDo リストアプリを作りました。その中でいくつか新キャラが登場していましたが、今回は PFObject について掘り下げようと思います。

PFObjectとは?

Parse.comのドキュメントによると PFObject は以下の特徴を持っているとのこと。

  • JSON互換(キーと値のペア)
  • スキーマレス(事前にキーとか定義しなくてもいいよ)
  • あんたが勝手に作ったPFObjectをバックエンドに保存するよ
  • キーは英数字の文字列で指定してね
  • 値はJSONエンコードできるものなら何でもいいよ
    • 文字列
    • 数値
    • ブール値
    • 配列
    • 辞書
  • データを区別するためにクラス名を持つよ
  • クラス名はアッパーキャメルケース(例:GameScoreとか)、キー名はローワーキャメルケース(playerName)がいいよ

ドキュメントに従い、以下のようなJSONオブジェクトを例に考えてみます。

{
    "score" : 1337,
    "playerName" : "Sean Plott",
    "cheatMode" : false
}

チートモード?なんだそれ?ってのは置いときまして、これをPFObjectで表すと、

PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"];
gameScore[@"score"] = @1337;
gameScore[@"playerName"] = @"Sean Plott";
gameScore[@"cheatMode"] = @NO;

となるらしいです。なんかちょっと制限のあるミュータブルな辞書っぽい感じで書けます。

オブジェクトを保存する

で、上記のように勝手に作ったオブジェクトを保存するには、以下のメソッドを使用します。

- (BOOL)save
同期的に保存処理を行う。
- (BOOL)save:(NSError **)error
同期的に保存処理を行う。エラー付き。
- (void)saveInBackground
非同期で保存処理を行う。
- (void)saveInBackgroundWithBlock:(PFBooleanResultBlock)block
非同期で保存処理を行う。
保存処理が終了したら引数で指定したBlocksが実行される。
- (void)saveInBackgroundWithTarget:(id)target selector:(SEL)selector
非同期で保存処理を行う。
保存処理が終了したらtargetで指定したインスタンスのselectorで指定したメソッドが実行される。
- (void)saveEventually
オフラインで保存処理を行う。(注!
- (void)saveEventually:(PFBooleanResultBlock)callback
オフラインで保存処理を行う。(注!
保存処理が終了したら引数で指定したBlocksが実行される。

たぶん一番多用するのは- (void)saveInBackgroundWithBlock:(PFBooleanResultBlock)blockメソッドな気がします。

PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"];
gameScore[@"score"] = @1337;
gameScore[@"playerName"] = @"Sean Plott";
gameScore[@"cheatMode"] = @NO;

[todo saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    // HUD消したり・・・

    if (!succeeded) {
        // エラー処理
        return;
    }

    // 成功時の処理
}];

- saveEventuallyメソッド

オフラインで保存を行う。となっておりますが、オフライン時にsaveEventually系のメソッドを利用して保存処理を実行した場合、実際にサーバに反映されるのはアプリをリセット(メモリから消す)してから起動したときのようです。

なので、ToDo リストアプリのように一覧画面で都度最新のデータ取得するような作りの場合、オフライン時に保存したデータはすぐに反映されないようです。。。

IOS: saveEventually not saving eventually until after App reset! | Parse

saveAll

複数のオブジェクトを一気に保存することも可能です。その場合はsaveAll系のクラスメソッドを使います。

PFObject *gameScore1 = [PFObject objectWithClassName:@"GameScore"];
gameScore1[@"score"] = @1337;
gameScore1[@"playerName"] = @"Sean Plott";
gameScore1[@"cheatMode"] = @NO;

PFObject *gameScore2 = [PFObject objectWithClassName:@"GameScore"];
gameScore2[@"score"] = @99999;
gameScore2[@"playerName"] = @"Yuki Beckham";
gameScore2[@"cheatMode"] = @YES;

[PFObject saveAllInBackground:@[gameScore1, gameScore2]
                        block:^(BOOL succeeded, NSError *error) {
                            // HUD消したり・・・

                            if (!succeeded) {
                                // エラー処理
                               return;
                            }

                            // 成功時の処理
                        }];

用意されているsaveAll系のクラスメソッドは以下の通り。

+ (BOOL)saveAll:(NSArray *)objects
同期的に保存処理を行う。
+ (BOOL)saveAll:(NSArray *)objects error:(NSError **)error
同期的に保存処理を行う。エラー付き。
+ (void)saveAllInBackground:(NSArray *)objects
非同期で保存処理を行う。
+ (void)saveAllInBackground:(NSArray *)objects block:(PFBooleanResultBlock)block
非同期で保存処理を行う。
保存処理が終了したら引数で指定したBlocksが実行される。
+ (void)saveAllInBackground:(NSArray *)objects target:(id)target selector:(SEL)selector
非同期で保存処理を行う。
保存処理が終了したらtargetで指定したインスタンスのselectorで指定したメソッドが実行される。

(saveAllEventuallyとかは無い!)

データタイプ

保存できる値の基本的なタイプは以下のものがあります。(File、Pointer、Relationについては後で)

タイプ Parseでのタイプ Objective-Cの型 Objective-Cの例
文字列 String NSString @"もじれつ"
数値 Number NSNumber @100
ブール値 Boolean NSNumber @YES
日付 Date NSDate [NSDate date]
バイナリ Bytes NSData [@"foo" dataUsingEncoding:NSUTF8StringEncoding]
位置情報 GeoPoint PFGeoPoint [PFGeoPoint geoPointWithLatitude:35.697239 longitude:139.774719]
配列 Array NSArray @[ @"item1", @"item2", @"item3" ]
オブジェクト Object NSDictionary @{ @"key1" : @"value1", @"key2" : @"value2", @"key3" : @"value3" }

以下のプログラムを実行すると・・・

PFObject *bigObject = [PFObject objectWithClassName:@"BigObject"];
bigObject[@"myNumber"]     = @100;
bigObject[@"myString"]     = @"もじれつ";
bigObject[@"myDate"]       = [NSDate date];
bigObject[@"myData"]       = [@"foo" dataUsingEncoding:NSUTF8StringEncoding];
bigObject[@"myGeoPoint"]   = [PFGeoPoint geoPointWithLatitude:35.697239 longitude:139.774719];
bigObject[@"myArray"]      = @[ @"item1", @"item2", @"item3" ];
bigObject[@"myDictionary"] = @{ @"key1" : @"value1", @"key2" : @"value2", @"key3" : @"value3" };
bigObject[@"myNull"]       = [NSNull null];
[bigObject saveInBackground];

こうなります(myNull無くね?)↓

parse-ios-pfobject-1

ちなみに、PFObjectのサイズ上限は128KBを超えちゃダメなので、画像とか文書を上記のバイナリで保存するのではなくPFFileを使用しろとのことです(PFFileは後で)。

PFObjectはスキーマレスだよ

ToDo リストアプリを見て分かるように、今のところダッシュボードではスキーマの定義とかやってません。このことから分かるようにParseではスキーマレスなデータを扱うようです。一般的なRDBMSのテーブル的なものはParse上ではクラスと呼ばれ、このクラスは初回のオブジェクト保存と同時に作成されます。

また、スキーマレスということなので、例えば以下のようなプログラムを書いても問題なく動作します。

// Todoクラスの初回のオブジェクト保存
PFObject *todo1 = [PFObject objectWithClassName:@"Todo"];
todo1[@"name"] = @"世界一の剣豪になる!";
[todo1 saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    ・・・
}];

// 新たにTodoクラスのオブジェクトを作成し、カラムを追加して保存
PFObject *todo2 = [PFObject objectWithClassName:@"Todo"];
todo2[@"name"] = @"実家に帰る。";
todo2[@"priority"] = @100; // <- New column!
[todo2 saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    ・・・
}];

parse-ios-pfobject-2

でも、こうすると・・・

// Todoクラスの初回のオブジェクト保存
PFObject *todo1 = [PFObject objectWithClassName:@"Todo"];
todo1[@"name"] = @"世界一の剣豪になる!";
[todo1 saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    ・・・
}];

// 新たにTodoクラスのオブジェクトを作成し、カラムを追加して保存
PFObject *todo2 = [PFObject objectWithClassName:@"Todo"];
todo2[@"name"] = @"実家に帰る。";
todo2[@"priority"] = @100;
[todo2 saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    ・・・
}];

// NumberであるはずのpriorityにStringを入れる
PFObject *todo3 = [PFObject objectWithClassName:@"Todo"];
todo3[@"name"] = @"両親にご飯をごちそうする。";
todo3[@"priority"] = @"High"; // <- 違うタイプのやつ
[todo3 saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    エラーになる!!
}];

さすがにエラーになりますw

オブジェクトの更新

オブジェクトの更新にもsave系のメソッドを使います。

PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];

// IDを指定してオブジェクトを取得する
[query getObjectInBackgroundWithId:@"xWMyZ4YEGZ" block:^(PFObject *gameScore, NSError *error) {
    // チートモードとスコアを更新する
    gameScore[@"cheatMode"] = @YES;
    gameScore[@"score"] = @1338;
    [gameScore saveInBackground];
}];

(PFQueryについても後で)

カウンター

Numberタイプのフィールドは- (void)incrementKey:(NSString)keyメソッドでインクリメントできます。また、- (void)incrementKey:(NSString)key byAmount:(NSNumber *)amountメソッドでamountにインクリメントの量を指定できます。

// 1加算(普通のインクリメント)
[gameScore incrementKey:@"score"];
// 10加算
[gameScore1 incrementKey:@"score" byAmount:@10];
// 1減算(普通のデクリメント)
[gameScore1 incrementKey:@"score" byAmount:@-1];

配列

Arrayタイプのフィールドであれば、以下の便利メソッドが使えます。

- (void)addObject:(id)object forKey:(NSString *)key
指定した配列フィールドに要素(1つ)を追加する。
- (void)addObjectsFromArray:(NSArray *)objects forKey:(NSString *)key
指定した配列フィールドに要素(複数)を追加する。
- (void)addUniqueObject:(id)object forKey:(NSString *)key
指定した配列フィールドに要素(1つ)を追加する。その要素が既に存在する場合は追加しない。
- (void)addUniqueObjectsFromArray:(NSArray *)objects forKey:(NSString *)key
指定した配列フィールドに要素(複数)を追加する。その要素が既に存在する場合は追加しない。
- (void)removeObject:(id)object forKey:(NSString *)key
指定した配列フィールドから要素(1つ)を削除する。
- (void)removeObjectsInArray:(NSArray *)objects forKey:(NSString *)key
指定した配列フィールドから要素(複数)を削除する。

オブジェクトの削除

オブジェクトの削除は以下のメソッドで行います。

- (BOOL)delete
同期的に削除処理を行う。
- (BOOL)delete:(NSError **)error
同期的に削除処理を行う。エラー付き。
- (void)deleteInBackground
非同期で削除処理を行う。
- (void)deleteInBackgroundWithBlock:(PFBooleanResultBlock)block
非同期で削除処理を行う。
削除処理が終了したら引数で指定したBlocksが実行される。
- (void)deleteInBackgroundWithTarget:(id)target selector:(SEL)selector
非同期で削除処理を行う。
削除処理が終了したらtargetで指定したインスタンスのselectorで指定したメソッドが実行される。
- (void)deleteEventually
オフラインで削除処理を行う。

save系とほぼ一緒!

PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];

// IDを指定してオブジェクトを取得する
[query getObjectInBackgroundWithId:@"xWMyZ4YEGZ" block:^(PFObject *gameScore, NSError *error) {
    // オブジェクトを削除
    [gameScore deleteInBackground];
}];

deleteAll

これまたsave系と同様、複数のオブジェクトを一気に削除することも可能です。その場合はdeleteAll系のクラスメソッドを使います。

+ (BOOL)deleteAll:(NSArray *)objects
同期的に削除処理を行う。
+ (BOOL)deleteAll:(NSArray *)objects error:(NSError **)error
同期的に削除処理を行う。エラー付き。
+ (void)deleteAllInBackground:(NSArray *)objects
非同期で削除処理を行う。
+ (void)deleteAllInBackground:(NSArray *)objects block:(PFBooleanResultBlock)block
非同期で削除処理を行う。
削除処理が終了したら引数で指定したBlocksが実行される。
+ (void)deleteAllInBackground:(NSArray *)objects target:(id)target selector:(SEL)selector
非同期で削除処理を行う。
削除処理が終了したらtargetで指定したインスタンスのselectorで指定したメソッドが実行される。

(deleteAllEventuallyとかは無い!)

まとめ

以上、PFObjectの保存・更新・削除をざっとまとめてみました。いやーやればやるほど面白いです。オフライン時の機能が充実すれば更に良しってところでしょうか?

次回は認証かPFQueryかPFFileか、はたまたなんかのチュートリアルなのか??(まだ決まってません)乞うご期待!