読者です 読者をやめる 読者になる 読者になる

Murayama blog.

プログラミングと、その次の話

Parse - Cloud Code - Cloud Functions

Cloud Functions

Cloud Codeの役に立つもう少し複雑な例をみてみましょう。ちょっとした演算結果を取得するのに、膨大なオブジェクトをデバイス上にダウンロードしなければならないというようなケースは、クラウド上で演算すべきでしょう。たとえば、映画(movie)のレビュー(review)を扱うアプリケーションを考えてみましょう。Reviewオブジェクトは次のように表現されるでしょう。

{
  "movie": "The Matrix",
  "stars": 5,
  "comment": "Too bad they never made any sequels."
}

仮に"The Matrix"のstarsのアベレージが欲しいなら、デバイス上ですべてのReviewを問い合わせて平均値を演算する必要があるでしょう。多くのデータを取得したにも関わらず必要なのは平均値だけなのです。このような場合、Cloud Codeを使えば、映画の名前を渡すだけでstarのアベレージだけを返すことができます。

Cloudファンクションは、requestオブジェクトにJSONパラメータを含むことができます。これを使えば映画の名前を渡すことができます。Parse JavaScript SDKクラウド環境でも利用できるので、Reviewオブジェクトを取得するために使用することができます。averageStarsファンクションの実装は次のようになります。

Parse.Cloud.define("averageStars", function(request, response) {
  var query = new Parse.Query("Review");
  query.equalTo("movie", request.params.movie);
  query.find({
    success: function(results) {
      var sum = 0;
      for (var i = 0; i < results.length; ++i) {
        sum += results[i].get("stars");
      }
      response.success(sum / results.length);
    },
    error: function() {
      response.error("movie lookup failed");
    }
  });
});

averageStarsファンクションと前回のhelloファンクションの違いは、Cloudファンクションを呼び出すときにパラメータを渡しているところです。リクエストパラメータはrequest.params.movieのようにアクセスできます。次にCloudファンクションがどのように呼び出されるのかを見ていきましょう。

Calling a Cloud Function

Cloudファンクションは、REST APIのようなクライアントSDKから呼び出されます。たとえば、映画の名前をパラメータに指定してaverageStarsファンクションを呼び出す場合は次のようになります。

 curl -X POST \
  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"movie":"The Matrix"}' \
  https://api.parse.com/1/functions/averageStars

通常、Cloudファンクションには2つの引数が渡されます。

  1. request - リクエスト情報が格納されたオブジェクトです。次のフィールドが設定されています。
    1. params - クライアントから送信されたパラメータ
    2. user - リクエストを発行したParse.User。ログインしていない場合は設定されない。
  2. response - レスポンス情報は次の2つのファンクションを含みます。
    1. success - このファンクションは、クライアントに返す任意のパラメータを引数に取ります。このオブジェクトはJSONオブジェクト/配列、あるいはParse.Objectを指定できます。
    2. error - エラーが発生した場合に呼び出します。クライアントに返すための任意のエラーメッセージを渡します。


ファンクションが正常に完了したら、次のようなレスポンスがクライアントに返ります。

{
  "result": 4.8
}

エラーが発生した場合のレスポンスは次のようになります。

{
  "code": 141,
  "error": "movie lookup failed"
}

Running Code On Save

特定のデータフォーマットに強制するようなケースもクラウド上でコードを実行すべきでしょう。たとえば、AndroidiOS両方のアプリケーションがある場合、両方のデバイスでデータを検証することになるでしょう。個別の環境でコードを書くよりも、Cloud Codeで実装すれば1つの実装で済みます。

映画レビューの例を見てみましょう。starsの有効範囲を1, 2, 3, 4, 5に限定するものとします。-6や1337starsはReviewに指定できないようにします。有効範囲外のデータをリジェクトする場合、beforeSaveメソッドを使うことができます。

Parse.Cloud.beforeSave("Review", function(request, response) {
  if (request.object.get("stars") < 1) {
    response.error("you cannot give less than one star");
  } else if (request.object.get("stars") > 5) {
    response.error("you cannot give more than five stars");
  } else {
    response.success();
  }
});

response.errorが呼び出されると、Reviewオブジェクトは保存せず、クライアントにエラーが返ります。response.successが呼び出されると、オブジェクトの保存は完了します。この2つのコードのいずれかを呼び出すように実装します。

モバイルアプリケーションの場合は異なるバージョンが存在しがちですが、Cloud Codeはすべてのユーザーに同じバージョンを提供できるという利点もあります。したがって、入力チェックを実装していないバージョンのアプリケーションを起動したとしても、Cloud Code上のbeforeSaveメソッドによって問題を防ぐことができるでしょう。

Parse JavaScript SDKて定義済みのクラス(Parse.Userなど)に対してbeforeSaveを使う場合は、第1引数に文字列を渡すのではなく、クラス自身を指定するようにしてください。

Modifying Objects On Save

不正なデータであってもスローするのではなく、保存する前に適切に処理したいこともあるでしょう。beforeSaveは、このようなケースにも利用することができます。オブジェクトを適切に処理してからresponse.successを呼び出すようにします。

映画レビューの例の場合、長過ぎるコメントも上手く処理したいというケースがあるでしょう。コメントを1行で表示することが難しいかもしれません。そのようなときは、beforeSaveメソッドを使えば140文字以内に切り詰めることができます。

Parse.Cloud.beforeSave("Review", function(request, response) {
  var comment = request.object.get("comment");
  if (comment.length > 140) {
    // Truncate and add a ...
    request.object.set("comment", comment.substring(0, 137) + "...");
  }
  response.success();  
});

Performing Actions After a Save

オブジェクトの保存が完了したら、PUSHノーティフィケーションを送るなど、後処理を追加したいこともあるでしょう。afterSaveメソッドを使えばこのような要件を達成できます。たとえば、ブログポストのコメント数を記録するような場合は、次のようになるでしょう。

Parse.Cloud.afterSave("Comment", function(request) {
  query = new Parse.Query("Post");
  query.get(request.object.get("post").id, {
    success: function(post) {
      post.increment("comments");
      post.save();
    },
    error: function(error) {
      console.error("Got an error " + error.code + " : " + error.message);
    }
  });
});

クライアントはどのように終了したのかに関係なく、保存リクエストに対しての正常なレスポンスを受信します。例えば、ハンドラが例外をスローしたとしても、クライアントは正常なレスポンスとして受け取ります。ハンドラの実行中に発生した例外はCloud Codeログの中で見つけることができます。

Running Code On Delete

オブジェクトの削除前にカスタムCloud Codeを実行できます。これにはbeforeDeleteメソッドを使います。これにより、ACLでの表現よりもより洗練された形で削除ポリシーを実装することができます。たとえば、フォトアルバムアプリがあるとして、写真(Photo)はアルバム(Album)と関連を持っています。ユーザが写真の残っているアルバムを削除しようとするご操作を防ぐためには、次のように実装することができます。

Parse.Cloud.beforeDelete("Album", function(request, response) {
  query = new Parse.Query("Photo");
  query.equalTo("album", request.object.id);
  query.count({
    success: function(count) {
      if (count > 0) {
        response.error("Can't delete album if it still has photos.");
      } else {
        response.success();
      }
    },
    error: function(error) {
      response.error("Error " + error.code + " : " + error.message + " when getting photo count.");
    }
  });
});

response.errorが呼び出されると、Albumオブジェクトは削除されません。また、クライアントにはエラーが返却されます。response.successが呼び出されると、正常にAlbumの削除が完了します。コードの中でいずれかのメソッドを呼び出すようにしてください。
Parseの事前定義済みのクラス(Parse.Userなど)に対してbeforeDeleteメソッドを使う場合は、第1引数に文字列ではなく、クラス自身を渡すようにしてください。

Performing Actions After a Delete

オブジェクトの削除が完了したら、PUSHノーティフィケーションを送るなど、後処理を追加したいこともあるでしょう。afterDeleteメソッドを使えばこのような要件を達成できます。たとえば、ブログポストの削除時に関連するコメントを削除するような場合は、次のようになるでしょう。

Parse.Cloud.afterDelete("Post", function(request) {
  query = new Parse.Query("Comment");
  query.equalTo("post", request.object.id);
  query.find({
    success: function(comments) {
      Parse.Object.destroyAll(comments, {
        success: function() {},
        error: function(error) {
          console.error("Error deleting related comments " + error.code + ": " + error.message);
        }
      });
    },
    error: function(error) {
      console.error("Error finding related comments " + error.code + ": " + error.message);
    }
  });
});

afterDeleteハンドラはrequest.objectを通じて削除済みのオブジェクトにアクセスできます。このフェッチ済みのオブジェクトは、再度フェッチしたり、保存したりすることはできません。

クライアントはどのように終了したのかに関係なく、保存リクエストに対しての正常なレスポンスを受信します。例えば、ハンドラが例外をスローしたとしても、クライアントは正常なレスポンスとして受け取ります。ハンドラの実行中に発生した例外はCloud Code logの中で見つけることができます。

Resource Limits

Timeouts

Cloudファンクションは、15秒を超えるとタイムアウトとなります。beforeSaveファンクション、afterSaveファンクションは3秒を超えるとタイムアウトとなります。CloudファンクションやbeforeSave/afterSaveファンクションが、他のCloud Codeから呼び出された場合、呼び出し側のファンクションに残された時間に制限されます。例えば、beforeSaveファンクションが、13秒経過済みのCloudファンクションから呼ばれた場合、通常の3秒よりも短い残りの2秒でタイムアウトとなります。

Network requests

successやerrorの後の進行中のネットワークリクエストは、キャンセルされます。通常、successを呼ぶ前に、すべてのネットワークリクエストが完了するように待つべきです。afterSaveファンクションはsuccessやerrorを呼びません。Cloud Codeはすべてのネットワークリクエストが終了するのを待ちます。