将您的 Parse Android 应用迁移到 Firebase

如果您是一位 Parse 用户,正在寻找后端即服务解决方案,用以替代以前的解决方案,Firebase 可能是您的 Android 应用的理想选择。

本指南介绍如何将特定服务集成到您的应用中。如需了解基本 Firebase 设置的说明,请参阅 Android 设置指南。

Google Analytics for Firebase

Google Analytics for Firebase 是一款免费的应用衡量解决方案,可提供关于应用使用情况和用户互动度的分析数据。Analytics 与各种 Firebase 功能进行了集成,可以无限制地向您提供多达 500 种不同事件的报告,您可以利用 Firebase SDK 定义这些事件。

如需了解详情,请参阅 Google Analytics for Firebase 文档

建议的迁移策略

使用多个分析服务提供方是非常普遍的现象,Google Analytics for Firebase 能够轻松适应这种情况。只需将 Analytics 添加至您的应用,它即会自动收集事件和用户属性(如首次打开、应用更新、设备型号、年龄等),让您从中受益。

对于自定义的事件和用户属性,您可采取双写策略,同时使用 Parse Analytics 和 Google Analytics for Firebase 记录事件和属性,进而逐步过渡到新解决方案。

代码比较

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 Analytics for Firebase

// 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 实时数据库

Firebase 实时数据库是一种 NoSQL 云端托管数据库。数据以 JSON 格式存储并实时同步到所连接的每个客户端。

请参阅 Firebase 实时数据库文档了解详情。

与 Parse 数据的差异

对象

在 Parse 中,您存储的是一个 ParseObject 或其子类,其中包含有 JSON 兼容数据的键值对。这种数据没有架构,这意味着您无需指定在每个 ParseObject 上存在什么键。

所有 Firebase 实时数据库数据均被存储为 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 实时数据库中,使用将数据拆分至不同路径的平展型数据结构更好地表示了各种关系,因此数据可通过不同调用有效地下载。

以下示例说明如何组织博客应用中博文与其作者之间的关系:

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 中,您通过将异步侦听器附加到数据库引用的方法来检索数据。该侦听器会针对数据的初始状态触发一次,以后只要数据有更改就会再次触发,因此您无需添加任何代码即可判断数据是否发生变化。

以下示例说明如何根据“对象”部分介绍的示例来检索某个游戏玩家的分数:

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 实时数据库经过优化,可针对所有连接的客户端以毫秒级速度同步数据,其最终数据结构与 Parse 核心数据有很大不同。这意味着,迁移的第一步是考虑您的数据需要进行哪些更改,其中包括:

  • 您的 Parse 对象应如何映射到 Firebase 数据
  • 如果您的数据有父子关系,如何将数据拆分到不同路径,以便通过不同调用高效地下载。

迁移数据

在决定如何在 Firebase 中组织数据之后,您需要计划如何应对您的应用需要向两种数据库写入数据的过渡期。您可以选择以下选项:

后台同步

在此方案中,您需要部署两个应用版本:一个是使用 Parse 的旧版本,另一个是使用 Firebase 的新版本。然后,通过 Parse Cloud Code 处理这两种数据库之间的同步(Parse 到 Firebase),您的代码会侦听 Firebase 上的变化,并将这些变化与 Parse 同步。 在开始使用新版本之前,您必须:

  • 将现有的 Parse 数据转换为新的 Firebase 结构,并将其写入 Firebase 实时数据库。
  • 编写 Parse Cloud Code 函数,以便使用 Firebase REST API 将旧版客户端在 Parse 数据中产生的变化写入 Firebase 实时数据库。
  • 编写和部署代码,以便侦听 Firebase 上的变化并将其同步到 Parse 数据库。

此方案能确保完全隔离新旧代码,避免让客户端变得复杂。此方案的两大挑战是:处理初始导出的庞大数据集以及保证双向同步不会引发无限递归。

双写

在此方案中,您需要编写一个同时使用 Firebase 和 Parse 的新版应用,使用 Parse Cloud Code 将旧版客户端产生的变化从 Parse 数据同步到 Firebase 实时数据库。当有足够多的用户从 Parse 专用版应用迁移后,您就可以从双写版本中移除 Parse 代码。

此方案不需要任何服务器端代码,但是它的缺点是,未访问的数据不会迁移,并且您的应用大小会因使用两种 SDK 而增大。

Firebase 身份验证

Firebase 身份验证可使用密码和深受欢迎的联合用户身份提供方(如 Google、Facebook 和 Twitter)对用户进行身份验证。它还提供了界面代码库,让您在跨所有平台实现和维护应用的全面身份验证体验方面节省相当可观的必要投资。

请参阅 Firebase 身份验证文档了解详情。

与 Parse 身份验证的差异

Parse 提供一个称为 ParseUser 的专用用户类,可自动处理用户帐号管理所需的功能。ParseUserParseObject 的一个子类,这意味着,用户数据包含在 Parse 数据中,并可像任何其他 ParseObject 一样进行通过额外的字段进行扩展。

FirebaseUser 拥有一组固定的基本属性(唯一身份 ID、主电子邮件地址、名称和照片网址),这些属性存储在一个单独的项目的用户数据库中,用户可以更新这些属性。您无法向 FirebaseUser 对象直接添加其他属性,但可以在您的 Firebase 实时数据库中存储额外的属性。

以下示例说明如何注册一个用户并额外添加一个电话号码字段。

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 控制台或您的自托管数据库中导出您的用户数据库。例如,从 Parse 控制台导出的某个 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 实时数据库。如果您按照帐号迁移部分所述的流程迁移帐号,那么您的 Firebase 帐号会使用与 Parse 帐号相同的 ID,方便您轻松迁移和复制由用户 ID 键控的任何关系。

Firebase 云消息传递

Firebase 云消息传递 (FCM) 是一种跨平台消息传递解决方案,让您可以免费且可靠地传递消息和通知。通知编辑器是一项在 Firebase 云消息传递的基础上构建的免费服务,可帮助移动应用开发者发送有针对性的用户通知。

如需了解详情,请参阅 Firebase 云消息传递文档

与 Parse 推送通知的区别

在设备上安装并注册获取通知的每个 Parse 应用都有一个关联的 Installation 对象,您可在其中存储定位通知所需要的所有数据。 InstallationParseUser 的一个子类,这意味着您可以按需要将任何额外数据添加到您的 Installation 实例。

通知编辑器提供基于应用、应用版本、设备语言等信息的预定义的用户细分。您可以使用 Google Analytics for Firebase 事件和属性构建更复杂的用户细分,用以创建受众群体。如需了解详情,请参阅受众群体帮助指南。这些定位信息不会出现在 Firebase 实时数据库中。

建议的迁移策略

迁移设备令牌

在本文成文之时,Parse Android SDK 使用的是较早版本的 FCM 注册令牌,与通知编辑器提供的功能不兼容。

您可以通过向自己的应用添加 FCM SDK 来获取新令牌,但是,这可能使 Parse SDK 用于接收通知的令牌失效。如果您想避免令牌失效的情况,可以设置 Parse SDK 同时使用 Parse 的发送者 ID 和您的发送者 ID。这样,您就不会使 Parse SDK 使用的令牌失效,但请注意,如果 Parse 关闭项目,则这种解决方法也会随之失效。

将渠道迁移至 FCM 主题

如果您在使用 Parse 渠道发送通知,则可迁移至 FCM 主题,后者可以提供同样的“发布者-订阅者”模式。要处理从 Parse 向 FCM 的转变,您可编写一个新版本的应用,让新应用使用 Parse SDK 退订 Parse 渠道并使用 FCM SDK 订阅对应的 FCM 主题。在此版本的应用中,您应停用在 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 远程配置

Firebase 远程配置是一种云服务,让您可以更改应用的行为和外观,而无需用户下载应用更新。使用远程配置时,您可以创建应用内默认值,从而实现对应用行为和外观的控制。之后,您便可以使用 Firebase 控制台为所有用户或细分用户群重写应用内默认值。

如果您要在迁移过程中测试不同的解决方案,并希望能够将更多客户端动态转移到不同的服务提供方,Firebase 远程配置就可以派上大用场。例如,如果您的某个应用版本同时使用了 Firebase 和 Parse 来处理数据,则您可以使用随机百分位规则来确定哪些客户端从 Firebase 读取数据,并逐渐扩大相应比例。

如需详细了解 Firebase 远程配置功能,请参阅远程配置简介

与 Parse 配置的差异

通过 Parse 配置,您可以在 Parse 配置信息中心向您的应用添加键值对,然后在客户端提取 ParseConfig。您得到的每个 ParseConfig 实例始终是不可变的。您将来从网络检索到新 ParseConfig 时,它不会修改任何现有 ParseConfig 实例,但会创建一个新实例并通过 getCurrentConfig() 使其可用。

而使用 Firebase 远程配置时,您会为键值对创建应用内默认值,并可从 Firebase 控制台重写这些默认值,这样一来,您可使用规则和条件向您用户群的不同细分提供不同的应用用户体验。Firebase 远程配置通过实现一个单例类,在您的应用中提供相应键值对。最开始,该单例会返回您定义的应用内默认值。之后,您可以随时在适当的时候为您的应用从服务器提取一组新值,成功提取该组新值后,您就可以选择在何时激活并向应用提供这些新值。

建议的迁移策略

向 Firebase 远程配置迁移的方法是,先将您的 Parse 配置的键值对复制到 Firebase 控制台,然后部署一个使用 Firebase 远程配置的新版本的应用。

如果您想对 Parse 配置和 Firebase 远程配置进行实验,可部署一个使用两种 SDK 的新版本的应用,直至有足够多的用户从 Parse 专用版本迁移为止。

代码比较

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");

发送以下问题的反馈:

此网页
需要帮助?请访问我们的支持页面