GAEでTwitterみたいなフォロー関係を実現する - fanout problem
http://dl.google.com/io/2009/pres/W_0415_Building_Scalable_Complex_App_Engines.pdf
古い資料だけど。何か驚くべき手法が書かれているのか?と思って読んだらそういうわけでもなく。大勢のフォロワーに向けてパブリッシングするメッセージエンティティに、フォロワーのキーをリストプロパティでもっておけということだった(下記コードはイメージです)。
public class Message implements Serializable { private Key key; private Key senderKey; private String body; private List<Key> followerKeys; }
あとからフォローしたりアンフォローした場合は、タスクでせっせと過去メッセージのfollowerKeysを書き換えるんでしょう。
エンティティあたりインデックス数の制限により、この設計ではフォロワー数の上限が10,000(インデックス数上限20,000として)になってしまうけれど、どうせそんな規模にはそうそうならないでしょう。そこだけ許容してしまえば、とてもシンプルに作れそう。
もうひとつ問題があって、リストプロパティのデシリアライズコストが馬鹿にならないらしい。この資料が作成された当時はどうにもならなかったようだけど、今のGAEにはProjectionクエリがあるので、followerKeysは取得せず欲しいプロパティだけを取得することができる。これはこれでまた制約があって、たとえばProjectionクエリで取得するプロパティはインデックス作成対象になっていなければならないので、Text型などは使えなかったりするのだけど。
oauth/access_token にアクセストークンを要求した結果と、SignedRequest内に格納されているアクセストークンで、expiresの数値の意味が違うメモ
パラメータとしてcodeを渡してoauth/access_tokenにアクセストークンを要求した場合、expiresの値として返ってくるのは、例えば↓くらいの数値で、これはトークン生成時からの有効期限(単位:秒)を表している。この例の秒数は日にち換算するとちょうど60日くらい(facebookのドキュメントで long-lived user access_token と呼ばれている期限長めのタイプ)。
access_token=xxx&expires=5183998
一方、ページタブ組み込みのアプリで使うSignedRequestではどういう値なのかというと、例えば↓のような具合で、expiresには期限切れ日時自体がセットされている(これは1970年1月1日からの秒数)。
{ expires=1361682000, issued_at=1361675975, oauth_token=xxx, ... }
めんどい。
Java + facebook Graph API メモ
Graph API は restfb
Graph API の呼び出しはrestfb。きれいに設計されていてとても使いやすい。オブジェクトをGETしたいときは、オブジェクトの種類を問わずfetchObject
またはfetchConnection
を使う。これはすべてのオブジェクトを一意なIDで管理していて型(?)におおらかなfacebook側の設計と親和性が高い。もちろん、生JSONとかMAPが返ってきてもイヤなので、型は引数で指定。
User user = facebook.fetchObject("me", User.class, Parameter.with("fields", "id,name,location"));
使いたいフィールドが、restfbによって提供されているモデルオブジェクトに含まれていなければ、自分で拡張すればよい。たとえば、User
クラスにはpictureフィールドがない(プロフィール画像が取れない!)ので、そんな場合はこうやって…
ExtendedUser user = facebook.fetchObject("me", ExtendedUser.class, Parameter.with("fields", "id,name,location,picture"));
ExtendedUser
を自作する。facebookが返すpictureフィールドは値がオブジェクトなので、対応するクラスが必要。返ってくるデータの形式さえつかんでいれば、あとは機械的作業。
public class ExtendedUser extends User { private static final long serialVersionUID = 1L; @Facebook private Picture picture; public Picture getPicture() { return picture; } public void setPicture(UserPicture picture) { this.picture = picture; } } public static class Picture implements Serializable { private static final long serialVersionUID = 1L; @Facebook private Data data; public Data getData() { return data; } public void setData(Data data) { this.data = data; } public static class Data implements Serializable { private static final long serialVersionUID = 1L; @Facebook("is_silhouette") private boolean isSilhouette; @Facebook private String url; public boolean isSilhouette() { return isSilhouette; } public void setSilhouette(boolean isSilhouette) { this.isSilhouette = isSilhouette; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } } }
ログイン処理は自分で書いてしまう
restfbはログイン処理してくれないので、ここは自分で書いてしまう。こんな感じで使えるFacebookLogin
クラスを作った。あとで公開しとく。かも。
public class IndexController extends BaseController { @Override protected Navigation run() throws Exception { AccessToken accessToken = sessionScope("access_token"); if (accessToken != null && !accessToken.isExpired()) { return forward("index.jsp"); } FacebookLogin login = FacebookLogin.Builder .with(FacebookService.APP_ID, FacebookService.APP_SECRET) .setCallbackUrl("https://apps.facebook.com/xxx/") .setPermissions(Permission.values()) .begin(); String code = asString("code"); if (StringUtil.isEmpty(code)) { AccessToken token = login.getAccessToken(code); sessionScope("access_token", token); return forward("index.jsp"); } return html(login.getLoginDialogRedirectScript()); } }
jQueryの謎: new jQuery.fn.init と jQuery.prototype
jQueryのソースを読んでいて、よく理解できない点が2つ。軽く調べて、一応の結論を得たのでメモっておく。
まずは2つの問題をまとめておく。
new jQuery.fn.init 問題
問題の箇所を2.0.0b1から抜粋すると、以下のとおりなのだけど
line:51~
jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' return new jQuery.fn.init( selector, context, rootjQuery ); },
端的に言えば、ここで new をしないで
jQuery = function( selector, context ) { return this.init( selector, context, rootjQuery ); },
こう書いたら何か問題あるのか、という話。こうすれば、jQuery.fn.init などではなく、きれいにjQuery(をコンストラクタとする)オブジェクトが返ってくるのに。
jQuery.prototype 問題
おなじく問題の箇所を2.0.0b1から抜粋。
line:85~
jQuery.fn = jQuery.prototype = {
このように、jQuery.prototype として jQuery.fn が設定されているわけだけど、new jQuery.fn.init 問題のところでみたとおり、関数jQueryは内部で new した別のオブジェクトを返すので、仮にこれをコンストラクタとして new jQuery() みたいなコードを書いても、jQuery.prototype を継承するオブジェクトは作れない。
じゃあ jQuery.prototype を設定する意味なくない?という話。
以下この2つの問題について考えていく。
昔のjQueryオブジェクトはホントにjQueryオブジェクトだった
いわゆるjQueryオブジェクトというのはjQuery.fn.initをコンストラクタとして作られたオブジェクトなわけだけど、実はこれはjQuery1.2.2以降の話で、jQuery1.2.1までのjQueryオブジェクトは、文字通り関数jQueryをコンストラクタとして作られたオブジェクトだった。1.2.1の関数jQueryを見てみると次のとおり。
var jQuery = window.jQuery = function(selector, context) { // If the context is a namespace object, return a new object return this instanceof jQuery ? this.init(selector, context) : new jQuery(selector, context); };
thisがjQueryのインスタンスかどうか判定して分岐しているけれど、これは要するに、jQuery("selector") と書いても new jQuery("selector") と書いても同様に、関数jQueryをコンストラクタとするオブジェクトが得られるよ、ということ。new を使わない前者の書き方だと this が window になるので、this instanceof jQuery は false となり、結局 new jQuery 呼び出しになるわけだ。
これが今のようなコードに変わったのが1.2.2です(下記)。jQuery.fn.init じゃなくて jQuery.prototype.init だったりはするけれど、この両者は同じオブジェクトだし、そこは気にしない方向で。今問題にしたいのは、このときから正確な意味でのjQueryオブジェクトが返されなくなったということ。
var jQuery = window.jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' return new jQuery.prototype.init( selector, context ); };
もっと昔はinitメソッドなんてなかった
さらに歴史をさかのぼると1.1.2まではjQueryオブジェクト初期化の役割をもつインスタンスメソッドとしてのinitメソッドは存在しなかった。かわりに、関数jQueryに初期化コードがベタっと書かれている。
この部分の設計が大きく変わったのは1.1.3。そこで、関数jQuery自体はシンプルになり、かわりにjQuery.fn(ひいてはjQuery.prototype)に init メソッドが定義された。
var jQuery = function(a,c) { // If the context is global, return a new object if ( window == this || !this.init ) return new jQuery(a,c); return this.init(a,c); }; jQuery.fn = jQuery.prototype = { init: function(a,c) { // ... 略
これは単に、JavaScriptでクラス型オブジェクト指向的なコードを書くプログラミングスタイルに移行したということだろうか。インスタンスメソッドと同列に init とか initialize みたいな名前のメソッドを定義して、それをコンストラクタから呼ぶスタイルに変わった。
今問題にしている視点だけから言えば、この時代のコードは直感的でわかりやすい。jQueryオブジェクトは実際に関数jQueryをコンストラクタとして作られたオブジェクトであり、jQuery.prototype にそのメソッドが定義されている。jQuery.prototype には jQuery.fn という別名がつけられており、拡張したいときはjQuery.fnに関数を追加する。という感じです。
jQuery.prototype問題の答え
1.2.1までは関数jQueryがそのままjQueryオブジェクトのコンストラクタだったので、jQuery.prototype を設定することには意味があった。今ではそれが不要だとしても、逆に設定しないことに必要性があるわけでもないし一応残しておくか、という判断なのかな、というのがとりあえずの結論です。いわゆる「歴史的な理由」。
new jQuery.fn.init の理由は高速化・・・らしいけど
new jQuery.fn.init というコードが追加された1.2.2とはどういうリリースだったのか、もうちょっと詳しく見てみよう。
jQueryブログによると、バグフィックスなどのほかに $(DOMElement) 呼び出しの高速化が行われている。これは怪しい。
しかし、1.2.2の init メソッドには
// Handle $(DOMElement) if ( selector.nodeType ) { this[0] = selector; this.length = 1; return this;
というコードが追加されているので、$(DOMElement) の高速化ってのはこのコードのことを指しているのだろう・・・などと思いつつ、しかし new jQuery.fn.init の理由は結局よくわからないので、githubの履歴を漁って、該当のコミットを探してみた。
https://github.com/jquery/jquery/commit/f97f77c034dc62001a687c728bdfdc71a23bf6b8
John Resig自身による修正。理由は、$(DOMElement) の高速化・・・あ、いや、all uses of $(...) と言っているので、DOMElementに限らないのかもしれない。実際この修正で $(DOMElement) の実行速度だけが改善されるというのはよくわからない。$(DOMElement) の改善はやっぱりこっち(下記)がメインだと思うんだよね・・・。
https://github.com/jquery/jquery/commit/1a2fdafd386a8f7be8b633634a684969921f8b8f
$(...) 全般の速度改善だとしても、なんでこれで改善するのかよくわからないが。
ともかく何故速くなるのかはよくわからないけど、理由は高速化だったということで、忙しいのでこのへんでノ
ブログとか縦長のサイトで下にスクロールしまくってもサイドバーのコンテンツを追跡表示しつづけるあれを修正
position: fixed に設定すると、設定した瞬間のleft値で固定されてしまうので、横にスクロールした場合、追跡表示要素がx軸的にも同じ位置に表示されてしまう。じゃますぎ。
というわけで、
http://xfan.hateblo.jp/entry/2013/01/20/101604
に
var offsetLeft = self.offset().left; // これと $(window).scroll(function() { self.css($(this).scrollTop() > offsetTop ? { left : offsetLeft - $(this).scrollLeft(), // これ // ...略 } : initialCss); });
などとコードを追加。元記事を修正しておいた。
GAEの開発環境という言葉がまぎらわしい
デプロイしたからといってそこが本番環境とは限らないのだよね。GAE上に開発用アプリケーションをつくることはよくあるわけで。
特にホスト名の管理が必要なアプリの場合。たとえば、あるURLを特定のホスト名でしか見れなくする、あるいはリダイレクトする、あるいはcanonical urlを設定したいときも。個人的にはよくある。
そんなときに、ローカルの開発環境ではこのホスト名、開発用アプリケーションにデプロイしたときはこのホスト名、本番環境ではこのホスト名、というのをヘタにコードにすると、
if (AppEngineUtil.isProduction() && env.isDevelopment()) { // ... }
みたいな、どっちなんだよ!状態になったりして。
最近は↓のようなユーティリティをつくるようになりました。
public class HogeUtil { public static boolean isRunningOnServer() { return SystemProperty.environment.value() == SystemProperty.Environment.Value.Production; } public static boolean isRunningOnLocalhost() { return SystemProperty.environment.value() == SystemProperty.Environment.Value.Development; } }
ブログとか縦長のサイトで下にスクロールしまくってもサイドバーのコンテンツを追跡表示しつづけるあれ
jQueryプラグインにした。あとでプロジェクトにまとめてgithubにUPしよう。
(function($) { $.fn.track = function(offset) { offset = offset || 0; this.each(function() { var self = $(this); var initialCss = { position: self.css("position"), top: self.css("top"), width: self.css("width") }; var width = self.css("box-sizing") === "border-box" ? self.outerWidth() : self.width(); var offsetTop = self.offset().top - offset - parseInt(self.css("margin-top")); var offsetLeft = self.offset().left; $(window).scroll(function() { self.css($(this).scrollTop() > offsetTop ? { position: "fixed", top: offset, left: offsetLeft - $(this).scrollLeft(), width: width } : initialCss); }); }); }; })(jQuery);
特定の位置までスクロールされると該当要素のpositionをfixedに変更するのが基本なんだけど、fixedにしたときにtopプロパティがautoのままではまずいのと、widthがautoまたは%指定のように親要素に依存する設定ではまずいということで、その辺りをごにょっと。