Parse iOS アプリの Firebase への移行

Parse を使っていてサービス ソリューションとなる代替バックエンドをお探しの場合、iOS アプリに最適なのが Firebase です。

このガイドでは、アプリに特定のサービスを統合する方法について説明します。Firebase の基本的な設定手順については、iOS+ の設定ガイドをご覧ください。

Google アナリティクス

Google アナリティクスは、アプリの使用状況とユーザー エンゲージメントについて分析することができる、無料のアプリ測定ソリューションです。アナリティクスは Firebase の機能と統合されていて、最大 500 の一意のイベントに対応可能な無制限のレポート作成機能を備えています。これは、Firebase SDK を使用して定義できます。

詳しくは、Google アナリティクスのドキュメントをご覧ください。

おすすめの移行方式

Google アナリティクスに簡単に適用できる一般的なシナリオは、複数のアナリティクス プロバイダを使用するシナリオです。アナリティクス プロバイダをアプリに追加するだけで、初回起動、アプリの更新、デバイスモデル、経過時間など、アナリティクスによって自動収集されるイベントやユーザー プロパティを利用できます。

カスタム イベントとユーザー プロパティに二重書き込み方式(Parse Analytics と Google アナリティクスを両方使用してイベントとプロパティを記録する方式)を採用すると、新しいソリューションを段階的に展開できます。

コードの比較

Parse Analytics

// Start collecting data
[PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions];

NSDictionary *dimensions = @{
  // Define ranges to bucket data points into meaningful segments
  @"priceRange": @"1000-1500",
  // Did the user filter the query?
  @"source": @"craigslist",
  // Do searches happen more often on weekdays or weekends?
  @"dayType": @"weekday"
};
// Send the dimensions to Parse along with the 'search' event
[PFAnalytics trackEvent:@"search" dimensions:dimensions];

Google アナリティクス

// Obtain the AppMeasurement instance and start collecting data
[FIRApp configure];

// Send the event with your params
[FIRAnalytics logEventWithName:@"search" parameters:@{
  // Define ranges to bucket data points into meaningful segments
  @"priceRange": @"1000-1500",
  // Did the user filter the query?
  @"source": @"craigslist",
  // Do searches happen more often on weekdays or weekends?
  @"dayType": @"weekday"
}];

Firebase Realtime Database

Firebase Realtime Database はクラウドにホストされる NoSQL データベースです。データは JSON として保存され、接続されたすべてのクライアントとリアルタイムに同期します。

詳しくは、Firebase Realtime Database のドキュメントをご覧ください。

Parse データとの違い

オブジェクト

Parse では、JSON と互換性のあるデータで構成された Key-Value ペアを含む PFObject、またはそのサブクラスが格納されます。データはスキーマレスであるため、各 PFObject 上に存在するキーを指定する必要はありません。

すべての Firebase Realtime Database のデータは JSON オブジェクトとして保存されます。PFObject とは異なり、ユーザーは使用可能な JSON タイプに対応するタイプの JSON ツリー値に書き込むだけです。

次に、ゲームのハイスコアを保存する例を示します。

Parse
PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"];
gameScore[@"score"] = @1337;
gameScore[@"playerName"] = @"Sean Plott";
gameScore[@"cheatMode"] = @NO;
[gameScore saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
  if (succeeded) {
    // The object has been saved.
  } else {
    // There was a problem, check error.description
  }
}];
Firebase
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
NSString *key = [[ref child:@"scores"] childByAutoId].key;
NSDictionary *score = @{@"score": @1337,
                        @"playerName": @"Sean Plott",
                        @"cheatMode": @NO};
[key setValue:score withCompletionBlock:^(NSError *error,  FIRDatabaseReference *ref) {
  if (error) {
    // The object has been saved.
  } else {
    // There was a problem, check error.description
  }
}];
詳しくは、Apple プラットフォームでのデータの読み取りと書き込みのガイドをご覧ください。

データ間の関係

PFObject には別の PFObject との関係を設定できます。すべてのオブジェクトは他のオブジェクトを値として使用できます。

Firebase Realtime Database で関係を詳細に表現するには、データを個別のパスに分割するフラットデータ構造を使用します。こうすることで、データを個々の呼び出し内で効率的にダウンロードできるようになります。

次に、ブログアプリの投稿とその作成者の関係を構築する例を示します。

Parse
// Create the author
PFObject *myAuthor = [PFObject objectWithClassName:@"Author"];
myAuthor[@"name"] = @"Grace Hopper";
myAuthor[@"birthDate"] = @"December 9, 1906";
myAuthor[@"nickname"] = @"Amazing Grace";

// Create the post
PFObject *myPost = [PFObject objectWithClassName:@"Post"];
myPost[@"title"] = @"Announcing COBOL, a New Programming Language";

// Add a relation between the Post and the Author
myPost[@"parent"] = myAuthor;

// This will save both myAuthor and myPost
[myPost saveInBackground];
Firebase
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];

// Create the author
NSString *myAuthorKey = @"ghopper";
NSDictionary *author = @{@"name": @"Grace Hopper",
                         @"birthDate": @"December 9, 1906",
                         @"nickname": @"Amazing Grace"};
// Save the author
[[ref child:myAuthorKey] setValue:author]

// Create and save the post
NSString *key = [[ref child:@"posts"] childByAutoId].key;
NSDictionary *post = @{@"author": myAuthorKey,
                       @"title": @"Announcing COBOL, a New Programming Language"};
[key setValue:post]

次のデータ レイアウトが作成されます。

{
  // Info about the authors
  "authors": {
    "ghopper": {
      "name": "Grace Hopper",
      "date_of_birth": "December 9, 1906",
      "nickname": "Amazing Grace"
    },
    ...
  },
  // Info about the posts: the "author" fields contains the key for the author
  "posts": {
    "-JRHTHaIs-jNPLXOQivY": {
      "author": "ghopper",
      "title": "Announcing COBOL, a New Programming Language"
    }
    ...
  }
}
詳しくは、データベースの構成ガイドをご覧ください。

データの読み取り

Parse では特定の Parse オブジェクトの ID を使用するか、PFQuery を使用してクエリを実行することによってデータを読み取ります。

Firebase では、データベース参照に非同期リスナーを接続してデータを取得します。リスナーはデータの初期状態を取得するために 1 回トリガーされ、データが変更されたときに再びトリガーされます。そのため、データが変更されているかどうかを判別するコードを追加する必要はありません。

次に、オブジェクト セクションに表示された例に基づいて特定のプレーヤーのスコアを取得する例を示します。

Parse
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"playerName" equalTo:@"Dan Stemkoski"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  if (!error) {
    for (PFObject *score in objects) {
      NSString *gameScore = score[@"score"];
      NSLog(@"Retrieved: %@", gameScore);
    }
  } else {
    // Log details of the failure
    NSLog(@"Error: %@ %@", error, [error userInfo]);
  }
}];
Firebase
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];

// This type of listener is not one time, and you need to cancel it to stop
// receiving updates.
[[[[ref child:@"scores"] queryOrderedByChild:@"playerName"] queryEqualToValue:@"Dan Stemkoski"]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
  // This will fire for each matching child node.
  NSDictionary *score = snapshot.value;
  NSString gameScore = score[@"score"];
  NSLog(@"Retrieved: %@", gameScore);
}];
使用可能なイベント リスナーのタイプや、データの並べ替え方法とフィルタリング方法について詳しくは、Apple プラットフォームでのデータの読み取りと書き込みのガイドをご覧ください。

おすすめの移行方式

データの見直し

Firebase Realtime Database は接続されたすべてのクライアント間でデータをミリ秒単位で同期できるよう最適化されていて、作成されるデータ構造は Parse コアデータと異なります。つまり、移行するにはまず、以下のような、データに必要な変更内容について検討する必要があります。

  • Parse オブジェクトを Firebase データにマップする方法
  • 親子関係がある場合は、データを複数のパスに分割して、独立した呼び出し内で効率的にダウンロードできるようにする方法

データの移行

データを Firebase で構成する方法を決定したら、アプリが両方のデータベースに書き込む必要がある期間について、その処理方法を計画する必要があります。次の設定を選択できます。

バックグラウンド同期

このシナリオでは、アプリのバージョンが 2 つ存在します(Parse を使用する古いバージョンと、Firebase を使用する新しいバージョン)。2 つのデータベース間の同期は Parse クラウドコードで処理されます(Parse から Firebase へ)。コードは Firebase の変更を待機し、変更があれば Parse と同期します。新しいバージョンの使用を開始する前に、以下の作業を行う必要があります。

  • 既存の Parse データを新しい Firebase 構造に変換して、Firebase Realtime Database に書き込みます。
  • Firebase REST API を使用する Parse クラウドコード関数を記述して、古いクライアントが Parse データに行った変更を Firebase Realtime Database に書き込みます。
  • Firebase に対する変更を待機して、それらを Parse データベースと同期するコードを記述して、デプロイします。

このシナリオでは、古いコードと新しいコードを整然と分離して、クライアントをシンプルに保ちます。このシナリオの課題は、最初のエクスポート時に発生する大規模なデータセットの処理と、双方向同期での無限再帰の回避です。

二重書き込み

このシナリオでは、Firebase と Parse を両方使用するアプリの新しいバージョンを記述し、Parse クラウドコードを使用して、古いクライアントが行った変更を Parse データから Firebase Realtime Database に同期します。Parse 専用バージョンのアプリから移行したユーザーが十分な数に達した場合は、二重書き込みバージョンから Parse コードを削除できます。

このシナリオでは、サーバー側のコードが不要です。このシナリオの欠点は、アクセスされていないデータが移行されないこと、そして両方の SDK を使用することによってアプリのサイズが増大することです。

Firebase Authentication

Firebase Authentication では、パスワードと、Google、Facebook、Twitter などの一般的なフェデレーション ID プロバイダを使用してユーザーを認証できます。また、UI ライブラリが用意されているので、すべてのプラットフォームでアプリの認証機能を実装およびメンテナンスするために必要とされる莫大な投資コストを抑えられます。

詳しくは、Firebase Authentication のドキュメントをご覧ください。

Parse Authentication との違い

Parse には、ユーザー アカウント管理に必要な機能を自動的に処理する、PFUser という名前の専用ユーザークラスがあります。PFUserPFObject のサブクラスであるため、Parse データ内でユーザーデータを使用することができ、他の PFObject のような追加フィールドを使用して拡張できます。

FIRUser では、基本的な固定プロパティ セット(一意の ID、メインのメールアドレス、名前、写真の URL など)が独立したプロジェクトのユーザー データベースに格納されていて、ユーザーはこれらのプロパティを更新できます。FIRUser オブジェクトに他のプロパティを直接追加することはできませんが、代わりに Firebase Realtime Database に追加プロパティを格納できます。

次に、ユーザーを登録して、電話番号フィールドを追加する例を示します。

Parse
PFUser *user = [PFUser user];
user.username = @"my name";
user.password = @"my pass";
user.email = @"email@example.com";

// other fields can be set just like with PFObject
user[@"phone"] = @"415-392-0202";

[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
  if (!error) {
    // Hooray! Let them use the app now.
  } else {
    // Something went wrong
    NSString *errorString = [error userInfo][@"error"];
  }
}];
Firebase
[[FIRAuth auth] createUserWithEmail:@"email@example.com"
                           password:@"my pass"
                         completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
  if (!error) {
    FIRDatabaseReference *ref = [[FIRDatabase database] reference];
    [[[[ref child:@"users"] child:user.uid] child:@"phone"] setValue:@"415-392-0202"
  } else {
    // Something went wrong
    NSString *errorString = [error userInfo][@"error"];
  }
}];

おすすめの移行方式

アカウントの移行

Parse から Firebase にユーザー アカウントを移行するには、ユーザー データベースを JSON ファイルまたは CSV ファイルにエクスポートし、Firebase CLI の auth:import コマンドを使用して Firebase プロジェクトにそのファイルをインポートします。

最初に、Parse Console またはセルフホストのデータベースからユーザー データベースをエクスポートします。たとえば、Parse Console からエクスポートした JSON ファイルは次のようになります。

{ // Username/password user
  "bcryptPassword": "$2a$10$OBp2hxB7TaYZgKyTiY48luawlTuYAU6BqzxJfpHoJMdZmjaF4HFh6",
  "email": "user@example.com",
  "username": "testuser",
  "objectId": "abcde1234",
  ...
},
{ // Facebook user
  "authData": {
    "facebook": {
      "access_token": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
      "expiration_date": "2017-01-02T03:04:05.006Z",
      "id": "1000000000"
    }
  },
  "username": "wXyZ987654321StUv",
  "objectId": "fghij5678",
  ...
}

次に、エクスポートしたファイルを Firebase CLI のフォーマットに変換します。Parse ユーザーの objectId を Firebase ユーザーの localId として使用します。また、Parse からの bcryptPassword 値を base64 でエンコードし、passwordHash フィールドで使用します。次に例を示します。

{
  "users": [
    {
      "localId": "abcde1234",  // Parse objectId
      "email": "user@example.com",
      "displayName": "testuser",
      "passwordHash": "JDJhJDEwJE9CcDJoeEI3VGFZWmdLeVRpWTQ4bHVhd2xUdVlBVTZCcXp4SmZwSG9KTWRabWphRjRIRmg2",
    },
    {
      "localId": "fghij5678",  // Parse objectId
      "displayName": "wXyZ987654321StUv",
      "providerUserInfo": [
        {
          "providerId": "facebook.com",
          "rawId": "1000000000",  // Facebook ID
        }
      ]
    }
  ]
}

最後に、変換後のファイルを Firebase CLI でインポートします。ハッシュ アルゴリズムとして bcrypt を指定します。

firebase auth:import account_file.json --hash-algo=BCRYPT

ユーザーデータの移行

ユーザーの追加データを格納している場合は、データの移行セクションに示された方法に従ってデータを Firebase Realtime Database に移行できます。アカウントの移行セクションに示されたフローに従ってアカウントを移行すると、Firebase アカウントの ID は Parse アカウントと同じになります。そのため、ユーザー ID を使用してキー設定されたすべての関係を簡単に移行して、再現できます。

Firebase Cloud Messaging

Firebase Cloud Messaging(FCM)は、メッセージや通知を無料で確実に配信するためのクロスプラットフォーム メッセージング ソリューションです。Notifications Composer は Firebase Cloud Messaging 上に構築された、モバイルアプリ デベロッパー向けのユーザー通知機能を提供する無料サービスです。

詳しくは、Firebase Cloud Messaging のドキュメントをご覧ください。

Parse Push Notifications との違い

通知用に登録されたデバイスにインストールされているすべての Parse アプリケーションには、Installation オブジェクトが関連付けられています。ターゲット通知に必要なすべてのデータは、このオブジェクトに格納されます。InstallationPFUser のサブクラスであるため、必要なすべての追加データを Installation インスタンスに追加できます。

Notifications Composer には、アプリ、アプリのバージョン、デバイスの言語などの情報に基づく定義済みのユーザー セグメントが用意されています。Google アナリティクスのイベントやプロパティを使用してさらに複雑なユーザー セグメントを作成し、ユーザーリストを作成できます。詳しくは、ユーザーリストのヘルプガイドをご覧ください。これらのターゲット情報は、Firebase Realtime Database には表示されません。

おすすめの移行方式

デバイス トークンの移行

通知のターゲットとなるインストールを指定する際に、Parse では APNs デバイス トークンを使用しますが、FCM では APNs デバイス トークンにマップされた FCM 登録トークンを使用します。Apple アプリに FCM SDK を追加するだけで、FCM トークンが自動的にフェッチされるようになります。

FCM トピックへのチャネルの移行

Parse チャネルを使用して通知を送信している場合は、同じパブリッシャー / サブスクライバー モデルを提供する FCM トピックに移行できます。Parse から FCM への移行を処理するには、Parse チャネルからの登録解除に Parse SDK を、対応する FCM トピックへの登録に FCM SDK を使用する、新しいバージョンのアプリを記述します。

たとえば、ユーザーが「Giants」トピックに配信登録している場合は、次のようなコードを実行します。

PFInstallation *currentInstallation = [PFInstallation currentInstallation];
[currentInstallation removeObject:@"Giants" forKey:@"channels"];
[currentInstallation saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
  if (succedeed) {
    [[FIRMessaging messaging] subscribeToTopic:@"/topics/Giants"];
  } else {
    // Something went wrong unsubscribing
  }
}];

この方法を使用して、Parse チャネルと対応する FCM トピックの両方にメッセージを送信し、古いバージョンと新しいバージョンの両方のユーザーをサポートできます。Parse 専用バージョンのアプリから十分な数のユーザーが移行したら、このバージョンを段階的に廃止して、FCM のみを使用した送信を開始できます。

詳しくは、FCM トピックのドキュメントをご覧ください。

Firebase Remote Config

Firebase Remote Config は、ユーザーがアプリのアップデートをダウンロードしなくても、アプリの動作や外観を変更できるクラウド サービスです。Remote Config を使用すると、アプリの動作や外観を制御するアプリ内デフォルト値を作成できます。その後、Firebase コンソールを使用して、すべてのアプリユーザーまたはユーザーベースの特定セグメントに対して、アプリ内デフォルト値をオーバーライドできます。

さまざまなソリューションをテストして、さらに多くのクライアントを別のプロバイダに動的にシフトできるようにする場合は、移行中に Firebase Remote Config を使用すると非常に便利です。たとえば、データに Firebase と Parse を両方使用するアプリのバージョンがある場合は、ランダム パーセンタイル ルールを使用して Firebase から読み取るクライアントを決定し、徐々にその割合を高められます。

Firebase Remote Config について詳しくは、Remote Config の概要をご覧ください。

Parse Config との違い

Parse Config を使用している場合は、Parse Config Dashboard でアプリに Key-Value ペアを追加し、クライアントで PFConfig をフェッチできます。取得されるすべての PFConfig インスタンスは常に不変です。後でネットワークから新しい PFConfig を取得しても、既存の PFConfig インスタンスは変更されません。代わりに、新しいインスタンスが作成されて currentConfig で使用できるようになります。

Firebase Remote Config を使用すると、Key-Value ペアのアプリ内デフォルト値を作成し、Firebase コンソールからオーバーライドできるようになります。また、ルールや条件を使用して、ユーザーベースのセグメントごとに異なるアプリ使用環境を実現できます。Firebase Remote Config は、アプリで使用可能な Key-Value ペアを作成するシングルトン クラスを実装します。最初、シングルトンは、アプリ内で定義したデフォルト値を返します。ユーザーは、アプリにとって都合のよい時間にいつでも、サーバーから新しい値セットをフェッチできます。新しいセットが正常にフェッチされたら、それをいつアクティブにして、新しい値をアプリが使用できるようにするのかを選択できます。

おすすめの移行方式

Firebase Remote Config に移行するには、Parse Config の Key-Value ペアを Firebase コンソールにコピーしてから、Firebase Remote Config を使用する新しいバージョンのアプリをデプロイします。

Parse Config と Firebase Remote Config を両方とも試す場合は、十分な数のユーザーが Parse 専用バージョンから移行するまで、両方の SDK を使用する新しいバージョンのアプリをデプロイできます。

コードの比較

Parse

[PFConfig getConfigInBackgroundWithBlock:^(PFConfig *config, NSError *error) {
  if (!error) {
    NSLog(@"Yay! Config was fetched from the server.");
  } else {
    NSLog(@"Failed to fetch. Using Cached Config.");
    config = [PFConfig currentConfig];
  }

  NSString *welcomeMessage = config[@"welcomeMessage"];
  if (!welcomeMessage) {
    NSLog(@"Falling back to default message.");
    welcomeMessage = @"Welcome!";
  }
}];

Firebase

FIRRemoteConfig remoteConfig = [FIRRemoteConfig remoteConfig];
// Set defaults from a plist file
[remoteConfig setDefaultsFromPlistFileName:@"RemoteConfigDefaults"];

[remoteConfig fetchWithCompletionHandler:^(FIRRemoteConfigFetchStatus status, NSError *error) {
  if (status == FIRRemoteConfigFetchStatusSuccess) {
    NSLog(@"Yay! Config was fetched from the server.");
    // Once the config is successfully fetched it must be activated before newly fetched
    // values are returned.
    [self.remoteConfig activateFetched];
  } else {
    NSLog(@"Failed to fetch. Using last fetched or default.");
  }
}];

// ...

// When this is called, the value of the latest fetched and activated config is returned;
// if there's none, the default value is returned.
NSString welcomeMessage = remoteConfig[@"welcomeMessage"].stringValue;