Parse Android アプリの Firebase への移行

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

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

Google アナリティクス

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

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

おすすめの移行方式

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

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

コードの比較

Parse Analytics

// Start collecting data
ParseAnalytics.trackAppOpenedInBackground(getIntent());

Map<String, String> dimensions = new HashMap<String, String>();
// Define ranges to bucket data points into meaningful segments
dimensions.put("priceRange", "1000-1500");
// Did the user filter the query?
dimensions.put("source", "craigslist");
// Do searches happen more often on weekdays or weekends?
dimensions.put("dayType", "weekday");

// Send the dimensions to Parse along with the 'search' event
ParseAnalytics.trackEvent("search", dimensions);

Google アナリティクス

// Obtain the FirebaseAnalytics instance and start collecting data
mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);

Bundle params = new Bundle();
// Define ranges to bucket data points into meaningful segments
params.putString("priceRange", "1000-1500");
// Did the user filter the query?
params.putString("source", "craigslist");
// Do searches happen more often on weekdays or weekends?
params.putString("dayType", "weekday");

// Send the event
mFirebaseAnalytics.logEvent("search", params);

Firebase Realtime Database

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

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

Parse データとの違い

オブジェクト

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

すべての Firebase Realtime Database のデータは JSON オブジェクトとして保存されます。ParseObject とは異なり、ユーザーは使用可能な JSON タイプに対応するタイプの JSON ツリー値に書き込むだけです。ユーザーは Java オブジェクトを使用して、データベースからの読み取りと書き込みをシンプルにすることができます。

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

Parse
@ParseClassName("GameScore")
public class GameScore {
        public GameScore() {}
        public GameScore(Long score, String playerName, Boolean cheatMode) {
            setScore(score);
            setPlayerName(playerName);
            setCheatMode(cheatMode);
        }

        public void setScore(Long score) {
            set("score", score);
        }

        public Long getScore() {
            return getLong("score");
        }

        public void setPlayerName(String playerName) {
            set("playerName", playerName);
        }

        public String getPlayerName() {
            return getString("playerName");
        }

        public void setCheatMode(Boolean cheatMode) {
            return set("cheatMode", cheatMode);
        }

        public Boolean getCheatMode() {
            return getBoolean("cheatMode");
        }
}

// Must call Parse.registerSubclass(GameScore.class) in Application.onCreate
GameScore gameScore = new GameScore(1337, "Sean Plott", false);
gameScore.saveInBackground();
Firebase
// Assuming we defined the GameScore class as:
public class GameScore {
        private Long score;
        private String playerName;
        private Boolean cheatMode;

        public GameScore() {}
        public GameScore(Long score, String playerName, Boolean cheatMode) {
            this.score = score;
            this.playerName = playerName;
            this.cheatMode = cheatMode;
        }

        public Long getScore() {
            return score;
        }

        public String getPlayerName() {
            return playerName;
        }

        public Boolean getCheatMode() {
            return cheatMode;
        }
}

// We would save it to our list of high scores as follows:
DatabaseReference mFirebaseRef = FirebaseDatabase.getInstance().getReference();
GameScore score = new GameScore(1337, "Sean Plott", false);
mFirebaseRef.child("scores").push().setValue(score);
詳細については、Android でのデータの読み取りと書き込みガイドをご覧ください。

データ間の関係

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

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

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

Parse
// Create the author
ParseObject myAuthor = new ParseObject("Author");
myAuthor.put("name", "Grace Hopper");
myAuthor.put("birthDate", "December 9, 1906");
myAuthor.put("nickname", "Amazing Grace");

// Create the post
ParseObject myPost = new ParseObject("Post");
myPost.put("title", "Announcing COBOL, a New Programming Language");

// Add a relation between the Post and the Author
myPost.put("parent", myAuthor);

// This will save both myAuthor and myPost
myPost.saveInBackground();
Firebase
DatabaseReference firebaseRef = FirebaseDatabase.getInstance().getReference();
// Create the author
Map<String, String> myAuthor = new HashMap<String, String>();
myAuthor.put("name", "Grace Hopper");
myAuthor.put("birthDate", "December 9, 1906");
myAuthor.put("nickname", "Amazing Grace");

// Save the author
String myAuthorKey = "ghopper";
firebaseRef.child('authors').child(myAuthorKey).setValue(myAuthor);

// Create the post
Map<String, String> post = new HashMap<String, String>();
post.put("author", myAuthorKey);
post.put("title", "Announcing COBOL, a New Programming Language");
firebaseRef.child('posts').push().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 を使用するか、ParseQuery を使用してクエリを実行することによってデータを読み取ります。

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

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

Parse
ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereEqualTo("playerName", "Dan Stemkoski");
query.findInBackground(new FindCallback<ParseObject>() {
    public void done(List<ParseObject> scoreList, ParseException e) {
        if (e == null) {
            for (ParseObject score: scoreList) {
                Log.d("score", "Retrieved: " + Long.toString(score.getLong("score")));
            }
        } else {
            Log.d("score", "Error: " + e.getMessage());
        }
    }
});
Firebase
DatabaseReference mFirebaseRef = FirebaseDatabase.getInstance().getReference();
Query mQueryRef = mFirebaseRef.child("scores").orderByChild("playerName").equalTo("Dan Stemkoski");

// This type of listener is not one time, and you need to cancel it to stop
// receiving updates.
mQueryRef.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot snapshot, String previousChild) {
        // This will fire for each matching child node.
        GameScore score = snapshot.getValue(GameScore.class);
        Log.d("score", "Retrieved: " + Long.toString(score.getScore());
    }
});
使用可能なイベント リスナーのタイプや、データの並べ替え方法とフィルタリング方法について詳しくは、Android でのデータの読み取りと書き込みガイドをご覧ください。

おすすめの移行方式

データの見直し

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

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

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

Parse
ParseUser user = new ParseUser();
user.setUsername("my name");
user.setPassword("my pass");
user.setEmail("email@example.com");

// other fields can be set just like with ParseObject
user.put("phone", "650-253-0000");

user.signUpInBackground(new SignUpCallback() {
    public void done(ParseException e) {
        if (e == null) {
            // Hooray! Let them use the app now.
        } else {
            // Sign up didn't succeed. Look at the ParseException
            // to figure out what went wrong
        }
    }
});
Firebase
FirebaseAuth mAuth = FirebaseAuth.getInstance();

mAuth.createUserWithEmailAndPassword("email@example.com", "my pass")
    .continueWithTask(new Continuation<AuthResult, Task<Void>> {
        @Override
        public Task<Void> then(Task<AuthResult> task) {
            if (task.isSuccessful()) {
                FirebaseUser user = task.getResult().getUser();
                DatabaseReference firebaseRef = FirebaseDatabase.getInstance().getReference();
                return firebaseRef.child("users").child(user.getUid()).child("phone").setValue("650-253-0000");
            } else {
                // User creation didn't succeed. Look at the task exception
                // to figure out what went wrong
                Log.w(TAG, "signInWithEmail", task.getException());
            }
        }
    });

おすすめの移行方式

アカウントの移行

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 オブジェクトが関連付けられています。ターゲット通知に必要なすべてのデータは、このオブジェクトに格納されます。InstallationParseUser のサブクラスであるため、必要なすべての追加データを Installation インスタンスに追加できます。

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

おすすめの移行方式

デバイス トークンの移行

このドキュメントを記述している時点で、Parse Android SDK は Notifications Composer が提供する機能と互換性のない、古いバージョンの FCM 登録トークンを使用しています。

新しいトークンを入手するには、アプリに FCM SDK を追加します。ただし、この操作を行うと、通知を受信するために Parse SDK で使用されているトークンが無効になる可能性があります。無効にならないようにするには、Parse の送信者 ID とご使用の送信者 ID を両方使用するように Parse SDK を設定できます。この方法の場合、Parse SDK で使用されているトークンは無効になりませんが、Parse のプロジェクトがシャットダウンされると回避策は有効でなくなることにご注意ください。

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

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

<service android:name="com.parse.PushService" />
<receiver android:name="com.parse.ParsePushBroadcastReceiver"
  android:exported="false">
<intent-filter>
<action android:name="com.parse.push.intent.RECEIVE" />
<action android:name="com.parse.push.intent.DELETE" />
<action android:name="com.parse.push.intent.OPEN" />
</intent-filter>
</receiver>
<receiver android:name="com.parse.GcmBroadcastReceiver"
  android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />

<!--
IMPORTANT: Change "com.parse.starter" to match your app's package name.
-->
<category android:name="com.parse.starter" />
</intent-filter>
</receiver>

<!--
IMPORTANT: Change "YOUR_SENDER_ID" to your GCM Sender Id.
-->
<meta-data android:name="com.parse.push.gcm_sender_id"
  android:value="id:YOUR_SENDER_ID" />;

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

ParsePush.unsubscribeInBackground("Giants", new SaveCallback() {
    @Override
    public void done(ParseException e) {
        if (e == null) {
            FirebaseMessaging.getInstance().subscribeToTopic("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 ペアを追加し、クライアントで ParseConfig をフェッチできます。取得されるすべての ParseConfig インスタンスは常に不変です。後でネットワークから新しい ParseConfig を取得しても、既存の ParseConfig インスタンスは変更されません。代わりに、新しいインスタンスが作成されて getCurrentConfig() で使用できるようになります。

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

ParseConfig.getInBackground(new ConfigCallback() {
    @Override
    public void done(ParseConfig config, ParseException e) {
        if (e == null) {
            Log.d("TAG", "Yay! Config was fetched from the server.");
        } else {
            Log.e("TAG", "Failed to fetch. Using Cached Config.");
            config = ParseConfig.getCurrentConfig();
        }

        // Get the message from config or fallback to default value
        String welcomeMessage = config.getString("welcomeMessage", "Welcome!");
    }
});

Firebase

mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance();
// Set defaults from an XML resource file stored in res/xml
mFirebaseRemoteConfig.setDefaults(R.xml.remote_config_defaults);

mFirebaseRemoteConfig.fetch()
    .addOnSuccessListener(new OnSuccessListener<Void>() {
        @Override
        public void onSuccess(Void aVoid) {
            Log.d("TAG", "Yay! Config was fetched from the server.");
            // Once the config is successfully fetched it must be activated before newly fetched
            // values are returned.
            mFirebaseRemoteConfig.activateFetched();
        }
    })
    .addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception exception) {
            Log.e("TAG", "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.
String welcomeMessage = mFirebaseRemoteConfig.getString("welcomeMessage");