Facebookアプリを作ってみましたシリーズ4 (FQLがすごいぞ編)

今回は、JavaScript SDKとFQLを使いました。

アプリはこちら

Facebookのイベント参加者の男女比を表示してくれる、とても便利なアプリです。
楽しいFacebookライフにぜひご活用ください。
Facebookイベントの男女比チェッカー


今回の感想は、

FQLがすごい

FQLを使うと、FacebookのデータにSQLっぽくアクセスする事ができます。
くわしくはこちら、Facebook Query Language (FQL)


FacebookのデータへのアクセスにはGraph APIを使うのが一般的でしょうか。GraphAPIは非常にわかりやすく直感的にFacebookのデータを取得したり更新したりできます。
GraphAPIではなくFQLを使った場合は、なんとSQLを使う感覚でFacebookにアクセスができてしまいます。
たとえば、ログインしているユーザーの情報を取得したい場合はこんな感じ

SELECT name FROM user WHERE uid = me()

データ取得の内容次第では、FQLを使う事でGraph APIよりもはるかに簡単で高速にデータアクセスができます。
たとえば今回のような分析系アプリを作る場合は必須機能ですね。


さらに、GraphAPIでは取得できないデータでもFQLを使うと取得できてしまったりします。*1
今回の例だと、ユーザーが参加していない(招待されている状態)のイベントはGraphAPIでは取得できませんが、FQLを使うと簡単に取得する事ができます。

//JavaScriptを使った例
//ユーザーが招待されていて、まだ始まっていないイベントを取得する場合
var query_my_events = FB.Data.query('SELECT eid, rsvp_status, start_time FROM event_member WHERE uid = me() AND start_time > now()');
//こんな感じでサブクエリが使えます。
var query_events = FB.Data.query("SELECT eid, name FROM event WHERE eid IN (SELECT eid FROM {0})", query_my_events);


イベントに参加しているユーザの情報を取得するのも簡単です。

var query_event_users = FB.Data.query('SELECT uid, rsvp_status FROM event_member WHERE eid={0}', eid);
var query_users = FB.Data.query("SELECT uid, name, sex, pic_square FROM user WHERE uid IN (SELECT uid FROM {0})", query_event_users);


FacebookのシステムにFQLというものが存在している事についてはそんなに驚く事でもないかもですが、
これを一般公開していて誰でも使えるようにしているところがすごいと思いました。さすがFacebookです。


今回もソースをそのまま貼っときますので、必要であれば勝手に見てください。

こまった事

JavaScriptSDKの場合だとFQLにエラーがあった場合のエラーハンドリングの仕組みが無いような気がしますが、どうなんでしょうか?
(もしあるなら教えてください)


たとえば、FQLで、存在しないカラムを指定してもエラーにはならず、結果も返ってこない。
結果が返らないというのは空の結果という意味ではなく、コールバックが実行されないという意味です。
一応、FQLの構文的なエラーに関してはFB.Date.queryの時点で例外になるので良いですが、カラム名などはスペルミスに気をつける必要があります。

ソース

一応Ruby on Railsで作ってますが、Ruby側の処理は何もやってないのでerbだけ貼っときます。


layouts/application.html.erb

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja" xmlns:og="http://ogp.me/ns#" xmlns:fb="http://www.facebook.com/2008/fbml">
<head>
  <title>イベントの男女比チェッカー</title>
  <%= stylesheet_link_tag :all %>
  <%= javascript_include_tag :all %>
  <%= csrf_meta_tag %>
  <script language="javascript" src="http://www.google.com/jsapi"></script>
	<meta property="og:type" content="website" />
	<meta property="og:title" content="イベントの男女比チェッカー" />
	<meta property="og:url" content="http://apps.facebook.com/genderrate/" />
	<meta property="og:image" content="http://kissrobber.com/genderratio/images/logo.png" />
	<meta property="fb:app_id" content="<%= Facebooker2.app_id %>" />
	<meta property="og:description" content="このアプリを使うとFacebookのイベント参加者の男女比を簡単に確認する事ができます。" />
	<meta name="author" content="http://iq148.com" />
	<link rel="canonical" href="http://apps.facebook.com/genderrate/" />
	<link rel="shortcut icon" href="http://kissrobber.com/genderratio/favicon.ico" />
</head>
<body>

<%= yield %>

<div id="fb-root"></div>
<script src="http://connect.facebook.net/ja_JP/all.js"></script>
<script>
  window.fbAsyncInit = function() {
    FB.init({appId: '<%= Facebooker2.app_id %>', status: true, cookie: true,
             xfbml: true});
			 
  	if (window.fbAsyncInited) {
		fbAsyncInited();
	}
  };
  (function() {
		var e = document.createElement('script');
		e.async = true;
		e.src = document.location.protocol + '//connect.facebook.net/ja_JP/all.js';
		document.getElementById('fb-root').appendChild(e);
  }());

  function feed(){
   FB.ui(
	   {
	     method: 'feed',
	     name: 'イベントの男女比チェッカー',
	     link: 'http://apps.facebook.com/genderrate/',
	     picture: 'http://kissrobber.com/genderratio/images/logo.png',
	     description: 'このアプリを使うとFacebookのイベント参加者の男女比を簡単に確認する事ができます。',
	     actions: {'name':'powered by iq148.com', 'link':'http://iq148.com'}
	   });
  }

 google.load("visualization", "1", {packages: ["imagechart"]});
</script>
</body>
</html>


index.html.erb

<h1><%= image_tag('logo2.png') %>イベントの男女比チェッカー</h1>
<div>
	<fb:like layout="button_count" href="http://apps.facebook.com/genderrate/">
    </fb:like>
    <a href="http://twitter.com/share" class="twitter-share-button" data-url="http://apps.facebook.com/genderrate/" data-text="Facebookイベント参加者の男女比チェッカー" data-count="horizontal" data-via="kissrobber" data-lang="ja">Tweet</a>
    <script type="text/javascript" src="http://platform.twitter.com/widgets.js">
    </script>
    <a href="http://b.hatena.ne.jp/entry/http://apps.facebook.com/genderrate/" class="hatena-bookmark-button" data-hatena-bookmark-title="Facebookイベント参加者の男女比チェッカー" data-hatena-bookmark-layout="standard" title="このエントリーをはてなブックマークに追加"><img src="http://b.st-hatena.com/images/entry-button/button-only.gif" alt="このエントリーをはてなブックマークに追加" width="20" height="20" style="border: none;"/></a>
    <script type="text/javascript" src="http://b.st-hatena.com/js/bookmark_button.js" charset="utf-8" async="async">
    </script>
    <a href="javascript:void(0);" onclick="feed();"><%= image_tag('btn_share.png', :style => "border-style:none;")%></a>
	<fb:send href="http://apps.facebook.com/genderrate/"></fb:send>
</div>
<div id="index" style="display:none;">
	<h3>これは何?</h3>
	このFacebookアプリを使うと、あなたが招待されたイベント参加者の男女比を簡単に確認できます。<br/>
	参加予定のイベントや参加を迷っているイベントの男女比を確認してみませんか?<br/>
	<br/>
	ご利用イメージ
	<br/>
	<%= image_tag('image.png') %>
	<br/>
	<br/>
	※このアプリはあなたが招待されたイベント情報を参照します。データの収集等は一切しておりません。<br/>
	<a href="javascript:void(0);" id="btn_login" ><%= image_tag('btn_login.png', :style => "border-style:none;")%></a>
	<br/>
</div>
<div id="main" style="width:100%">
    <div id="events">
    </div>
    <br/>
    <div id="show" style="display:none;">
        <table>
            <tr>
                <td width="150px" valign="top">
                    <p>
                        参加する男
                    </p>
                    <div id="male_attending_faces">
                    </div>
                    <br/>
                    <p>
                        参加するかも男
                    </p>
                    <div id="male_unsure_faces">
                    </div>
                </td>
                <td width="350px" valign="top">
                    <div id="chart_attending">
                    </div>
                    <div id="chart_unsure">
                    </div>
                </td>
                <td width="150px" valign="top">
                    <p>
                        参加する女
                    </p>
                    <div id="female_attending_faces">
                    </div>
                    <br/>
                    <p>
                        参加するかも女
                    </p>
                    <div id="female_unsure_faces">
                    </div>
                </td>
            </tr>
        </table>
    </div>
</div>
<div style="top:150px;left:300px;position:absolute;" id="loading_img">
    <%= image_tag('ajax-loader.gif') %>
</div>
<div style="top:180px;left:50px;font-size:xx-large;position:absolute;display:none;background-color:#FFFFFF;" id="confirm_dialog">
    それでもあなたは参加しますか?
</div>

<hr/>
Powered by <a href="http://twitter.com/#!/kissrobber" target="_blank">@kissrobber</a> ( <a href="http://iq148.com" target="_blank">iq148.com</a> ) 

<script>
    function fbAsyncInited(){
        FB.getLoginStatus(function(response){
            if (response.session) {
                $("#main").show();
                $("#index").hide();
                get_events();
                $("#loading_img").hide();
            }
            else {
                $("#index").show();
                $("#main").hide();
                $("#loading_img").hide();
            }
        });
    }
    
    $("#btn_login").click(function(){
        FB.login(function(response){
            if (response.session) {
                $("#main").show();
                $("#index").hide();
                get_events();
                $("#loading_img").hide();
            }
            else {
                $("#main").hide();
                $("#index").show();
                $("#loading_img").hide();
            }
        }, {
            perms: 'user_events'
        });
    });
    
    var RSVP_STATUS_LABEL = {
        'attending': "参加",
        'unsure': "未定",
        'declined': "不参加",
        'not_replied': "未回答"
    };
    
    function get_events(){
        $('#events').empty();
        
        var query_my_events = FB.Data.query('SELECT eid, rsvp_status, start_time FROM event_member WHERE uid = me() AND start_time > now()');
        var query_events = FB.Data.query("SELECT eid, name FROM event WHERE eid IN (SELECT eid FROM {0})", query_my_events);
        
        FB.Data.waitOn([query_my_events, query_events], function(){
            var events = {};
            var has_events = false;
            FB.Array.forEach(query_my_events.value, function(row){
                events[row.eid] = row;
                has_events = true;
            });
            FB.Array.forEach(query_events.value, function(row){
                row.rsvp_status_ja = RSVP_STATUS_LABEL[events[row.eid].rsvp_status];
                $.extend(events[row.eid], row);
            });
            
            if (has_events) {
                var events_div = $('#events');
                events_div.html("<table width='500px' border='1px'><tbody id='event_tbody'></tbody></table>");
                events_tbody = $('#event_tbody', events_div);
                $.each(events, function(key, value){
                    var html = $.tmpl("<td><a href='javascript:void(0);'>${name}</a></td><td>${rsvp_status_ja}</td>", value);
                    $(html).click(function(){
                        show(value.eid);
                    });
                    events_tbody.append($('<tr/>').html(html));
                });
            }
            else {
                $('#events').html("現在イベントの予定はありません。イベントに招待された時に是非ご利用ください。");
            }
            
            
        });
        
    };
    
    function show(eid){
    
        $("#loading_img").show();
        $("#show").show();
        
        $('#users').empty();
        $('#chart_attending').empty();
        $('#chart_unsure').empty();
        $("#male_attending_faces").empty();
        $("#female_attending_faces").empty();
        $("#male_unsure_faces").empty();
        $("#female_unsure_faces").empty();
        
        var query_event_users = FB.Data.query('SELECT uid, rsvp_status FROM event_member WHERE eid={0}', eid);
        var query_users = FB.Data.query("SELECT uid, name, sex, pic_square FROM user WHERE uid IN (SELECT uid FROM {0})", query_event_users);
        
        FB.Data.waitOn([query_event_users, query_users], function(){
            var users = {};
            var uids = [];
            var info = {
                'female_attending': 0,
                'male_attending': 0,
                'female_unsure': 0,
                'male_unsure': 0
            };
            FB.Array.forEach(query_event_users.value, function(row){
                users[row.uid] = row;
                uids.push(row.uid);
            });
            FB.Array.forEach(query_users.value, function(row){
                row.rsvp_status_ja = RSVP_STATUS_LABEL[users[row.uid].rsvp_status];
                $.extend(users[row.uid], row);
            });
            
            var to = function(){
                var f = Array.prototype.shift.apply(arguments);
                args = arguments;
                return setTimeout(function(){
                    f.apply(null, args)
                }, 10);
            };
            
            var i = 0;
            to(function f(fin){
                if (!(i < uids.length)) 
                    return fin();
                
                value = users[uids[i]];
                var html = $.tmpl("<img src='${pic_square}' />", value);
                if (value.sex == 'male') {
                    if (value.rsvp_status == 'attending') {
                        info['male_attending']++;
                        $("#male_attending_faces").prepend(html);
                    }
                    else 
                        if (value.rsvp_status == 'unsure' || value.rsvp_status == 'not_replied') {
                            info['male_unsure']++;
                            $("#male_unsure_faces").prepend(html);
                        }
                }
                else {
                    if (value.rsvp_status == 'attending') {
                        info['female_attending']++;
                        $("#female_attending_faces").prepend(html);
                    }
                    else 
                        if (value.rsvp_status == 'unsure' || value.rsvp_status == 'not_replied') {
                            info['female_unsure']++;
                            $("#female_unsure_faces").prepend(html);
                        }
                }
                
                i++;
                to(f, fin);
            }, function(){
            
                var html = $.tmpl("${female_attending}, ${male_attending}, ${female_unsure}, ${male_unsure},", info);
                
                if (info['male_attending'] + info['female_attending'] > 0) {
                    var dataTable = new google.visualization.DataTable();
                    dataTable.addRows(2);
                    dataTable.addColumn('number');
                    dataTable.setValue(1, 0, info['male_attending']);
                    dataTable.setValue(0, 0, info['female_attending']);
                    draw_chart('chart_attending', dataTable, '参加者の男女比(' + Math.round((info['male_attending'] / (info['male_attending'] + info['female_attending'])) * 100) + ':' +
                    Math.round((info['female_attending'] / (info['male_attending'] + info['female_attending'])) * 100) +
                    ')');
                }
                if (info['male_unsure'] + info['female_unsure'] > 0) {
                    var dataTable = new google.visualization.DataTable();
                    dataTable.addRows(2);
                    dataTable.addColumn('number');
                    dataTable.setValue(1, 0, info['male_unsure']);
                    dataTable.setValue(0, 0, info['female_unsure']);
                    draw_chart('chart_unsure', dataTable, '参加するかもしれない人たちの男女比(' + Math.round((info['male_unsure'] / (info['male_unsure'] + info['female_unsure'])) * 100) + ':' +
                    Math.round((info['female_unsure'] / (info['male_unsure'] + info['female_unsure'])) * 100) +
                    ')');
                }
                
                $("#loading_img").hide();
                
                $("#confirm_dialog").fadeIn(600, function(){
                    $("#confirm_dialog").fadeOut(5000);
                });
            });
        });
    }
    
    function draw_chart(id, dataTable, title){
        var vis = new google.visualization.ImageChart(document.getElementById(id));
        var options = {
            chf: 'a,s,000000|bg,lg,50,EFEFEF,0,BBBBBB,1',
            chs: '350x230',
            cht: 'p3',
            chco: '990066|3072F3',
            chd: 's:Uf',
            chl: '女|男',
            chma: '20,20',
            chp: (Math.PI / 2) * 3,
            chtt: title
        };
        vis.draw(dataTable, options);
    }
</script>

*1:当たり前ですがFQLを使っても、クライアントに権限が無い情報を取得する事はできません