Arelで色んなSQLを組み立ててみる

(この記事は Ruby Advent Calendar jp:2010の 15 日目です。前日は tomohiro68 さんでした。)

Arelとは

Arelの概要については@a_matsudaさんのgihyoの記事を参照してください。
http://gihyo.jp/dev/serial/01/ruby/0043

はじめに

扱うRDBはSQlite3です。
例として以下のようなスキーマを持ったテーブルを取り扱い、進めていきます。

class CreateBooks < ActiveRecord::Migration
  def self.up
    create_table :books do |t|
      t.string :name
      t.string :category

      t.timestamps
    end
  end

  def self.down
    drop_table :books
  end
end

基本的な操作

where句
books = Arel::Table.new :books

#=
books.project(Arel.sql('*')).where(books[:id].eq(1)).to_sql
   => SELECT * FROM "books" WHERE "books"."id" = 1

#like
books.project(Arel.sql('*')).where(books[:name].matches('book_name_2_%')).to_sql
   => SELECT * FROM "books" WHERE "books"."name" LIKE 'book_name_2_%'

#>,<,andで条件を追加
books.project(Arel.sql('*')).where(books[:id].gt(5)).where(books[:id].lt(10)).to_sql
   => SELECT * FROM "books" WHERE "books"."id" > 5 AND "books"."id" < 10

#orで条件を追加
books.project(Arel.sql('*')).where(books[:id].gt(5).or(books[:id].lt(10))).to_sql
   => SELECT * FROM "books" WHERE ("books"."id" > 5 OR "books"."id" < 10)
order by句
books = Arel::Table.new :books

#default
books.project(Arel.sql('*')).order(books[:id]).to_sql
   => SELECT * FROM "books" ORDER BY "books"."id"

#昇順
books.project(Arel.sql('*')).order(books[:id].asc).to_sql
   => SELECT * FROM "books" ORDER BY "books"."id" ASC

#降順
books.project(Arel.sql('*')).order(books[:id].desc).to_sql
   => SELECT * FROM "books" ORDER BY "books"."id" DESC
集計関数とgroup by
books = Arel::Table.new :books

#count
books.project(books[:id].count, books[:category]).group(books[:category]).to_sql
   => SELECT COUNT("books"."id"), "books"."category" FROM "books" GROUP BY "books"."category"

#集計カラムに別名をつける
books.project(books[:id].count.as('count_id'), books[:category]).group(books[:category]).to_sql
   => SELECT COUNT("books"."id") AS count_id, "books"."category" FROM "books" GROUP BY "books"."category"

#sum
books.project(books[:id].sum.as('sum_id'), books[:category]).group(books[:category]).to_sql
   => SELECT SUM("books"."id") AS sum_id, "books"."category" FROM "books" GROUP BY "books"."category"

おまけ

SQLで既存のテーブルから値を参照する方法がありますが、
一時テーブル(?)を作ってそこから参照することもできます。

select
  1 as num
=>
num 
----
1 

unionでつなげると

select
  *
from (
  select
    1 as num
  union
  select
    2 as num
) as tmp
=>
num 
----
1   
2 

これをArelで書くと以下の様になります(使い方違っているような気もしますが...

table = Arel::Table.new nil
table.from('(select 1 as num union select 2 as num) as tmp').project(Arel.sql('*')).to_sql
   => SELECT * FROM (select 1 as num union select 2 as num) as tmp

無理矢理ですね。

おまけをjoin句でinner joinしてみる!

さらにおまけで紹介したものを未紹介だったjoin句でつないでみます。

table = Arel::Table.new nil
books = Arel::Table.new :books
table = table.from('(select 1 as num union select 2 as num) as tmp').project(Arel.sql('*'))
table.join(books).on(books[:id].eq(Arel.sql('tmp.num'))).to_sql
   => SELECT * FROM (select 1 as num union select 2 as num) as tmp INNER JOIN "books" ON "books"."id" = tmp.num

もはやfind_by_sqlを使え状態です。
しかも使う機会があるのかさえ微妙です。

わかってないこと

サブクエリや型変換を伴うようなSQLの組み上げについては調査中です。
型変換についてはfind_by_sqlを使わないと無理な気がしています。
サブクエリはSQLを一つにまとめる方法でなければできたのですが。

まとめ

このようにArelを使うことによって、より自然な形でSQLを組み立てることができるようになりました。
またArelになってからSQL厨の皆様にとっても使いやすくなったのではないかと思います。

iPhone等のスマホ向けレイアウトを簡単に作れるjQueryMobileを試してみた

jQueryMobileを触ってみたのでメモ。

jQueryMobileはスマートフォン、タブレットに特化したJavascript Framework。

準備

downloadページからjQueryMobile本体とcssをダウンロードしてくる。
http://jquerymobile.com/download/

ホスティングもしているみたいだけど、今回はjQueryMobileで提供されるデザインテーマで利用される画像パスがアプリ側と合わなかった為zipをダウンロードした。
ダウンロードしたファイルを展開するとjs,css,imagesが入っている。
cssはjQueryMobileが要素の属性を判断してレイアウトを整えるのに使われる。
これらと合わせてjQuery本体も読み込む。
よってロードする順番は以下のようになる。

<link href="/stylesheets/jquery.mobile-1.0a2.min.css" media="screen" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
<script src="/javascripts/jquery.mobile-1.0a2.min.js" type="text/javascript"></script>

以上で準備は完了。

要素の記述

実際にjQueryMobileを活用してスマホ向けviewを構築していく。
まずメインのコンテンツとなる部分を以下の属性をつけて記述する。

<div data-role="page" data-theme="d">
</div>

data-role="page"はメインのコンテンツであることを示す属性。
data-theme="d"は適用するデザインテーマ*1の種類を指定する為の属性。

次にヘッダ要素の記述。

<div data-role="header">
  header
</div>

ここでもヘッダを表すdata-role="header"を指定することでヘッダレイアウトが適用される。

リスト要素の記述。

<ul data-role="listview" data-theme="c" data-inset="true">
  <li>
    <a href="/vocabulary/new">add word</a>
  </li>
  <li>
    <a href="/dashboard/index">dashboard</a>
  </li>
  <li>
    <a href="/mobile/list">list</a>
  </li>
  <li>
    <a href="/dashboard/examination">examination</a>
  </li>
</ul>

ここまでくると属性については大体予想できるでしょうが、
data-role="listview"でリスト構造を取ることを指定する。
data-theme="c"でリストのデザインテーマ*2を適用。
data-inset="true"を指定することによって一括りのデータセットとされ、全体が角丸レイアウトされる模様。(詳細未確認

途中に上記以外の要素が入っているが、以上をレンダリングさせると以下のようなレイアウトになる。

リンクについて

jQueryMobileにおいてリンク(aタグ)は全てAjaxによるリクエストとなる。*3
これによりページ遷移なしにiOSのネイティブアプリの様なページ遷移を行うことができる。
イメージについてはjQueryMobileデモを参照。
リンク先のページについては特にAjaxリクエストであることを意識する必要はない。
従来通りのHTMLで、jQueryMobileのレイアウトを記述したHTMLを返すだけで良い。
例:

<!DOCTYPE html>
<html>
<head>
  <title>title</title>
  <link href="/stylesheets/jquery.mobile-1.0a2.min.css" media="screen" rel="stylesheet" type="text/css" />
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
  <script src="/javascripts/jquery.mobile-1.0a2.min.js" type="text/javascript"></script>
</head>
<body>

<div data-role="page" data-theme="d">
  <div data-role="header">
    <h1>Word List</h1>
  </div>

  <div style="margin:0 3px;">
    <ul data-role="listview" data-theme="c" data-inset="true">
      <li>
        <a href="/vocabulary/show/1">word</a>
      </li>
      <li>
        <a href="/vocabulary/show/2">duplicate</a>
      </li>
      <li>
        <a href="/vocabulary/show/3">disrupt</a>
      </li>
      <li>
        <a href="/vocabulary/show/4">rupt</a>
      </li>
      <li>
        <a href="/vocabulary/show/5">annual</a>
      </li>
      <li>
        <a href="/vocabulary/show/6">fundamental</a>
      </li>
      <li>
        <a href="/vocabulary/show/7">ensure</a>
      </li>
      <li>
        <a href="/vocabulary/show/8">add</a>
      </li>
    </ul>
  </div>
</div>
</body>
</html>

jQueryMobileはこのレスポンスをロードして以下の様にレイアウトを整える。

この様に自動でbackボタンも付けられる。これを利用してページ遷移することなく元のページに戻ることができる。

まとめ

jQueryMobileはスマートフォン向けのページを素早く簡単に構築することができます。
自前で実装すると面倒な無遷移のページ移動が簡単に実装できます。
またデザインもjQueryMobile側で吸収してくれているので大量のcssを書かずに済みます。
これ以外にもたくさん機能が提供されているので非常に有用なフレームワークであると思います。
特にイベント関連の充実は本当に素晴らしいです。

これでデザインテーマも簡単に記述できるようになるともっと良いものになると思いました。

*1:http://jquerymobile.com/demos/1.0a2/#docs/api/themes.html

*2:http://jquerymobile.com/demos/1.0a2/#docs/lists/lists-themes.html

*3:Ajaxとならないように指定も可能

Rails3でAjaxでformのcallbackを指定するには

Rails3からの変更点でハマったのでメモ。

Rails2ではformをAjaxで送信するにはform_remote_tagを利用していた。
これがRails3では以下のように:remote => trueを指定することで実現する。

<%= form_tag(url_for(:action => 'create'), :remote => true, :id => "result_form") do %>
<% end %>

今は主にjQueryを使って開発している為、デフォルトで扱うjavascriptjQueryに変更する。
Gemfileに以下記述

gem 'jquery-rails'

bundle installして関連ファイルをgenerate

$ bundle install
$ rails g jquery:install

jquery.jsとrails.jsを読み込むように設定
jquery.jsはgoogleから読み込み

#app/views/layouts/application.html.erb等に記述
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
  <%= javascript_include_tag 'rails' %>

formからのリクエストを受け取るcallbackをJavascriptファイルに設定

$(function($){
    $('#result_form')
        .bind("ajax:complete", function(){
           //ここに具体的な処理を記述
        });
});

他のステータスについても記述可能
それぞれ引数を受け取ることもできる。

$(function($){
    $('#result_form')
        .bind("ajax:loading", function(xhr){
           //ここに具体的な処理を記述
        })
        .bind("ajax:success", function(data, status, xhr){
           //ここに具体的な処理を記述
        })
        .bind("ajax:complete", function(xhr){
           //ここに具体的な処理を記述
        })
        .bind("ajax:failure", function(data, status, xhr){
           //ここに具体的な処理を記述
        });
});

処理を無名関数に記述できるのでRails2でのようにviewにゴチャっと書く必要がなくなり、より明確になったと思う。

MongoDBのReplica Setsについての概要

MongoDBによるレプリケーションの一種、Replica Setsについての概要

MongoDBでは各種RDBMSで採用されているMaster/Slaveでのレプリケーション方式の他にReplica Setsという仕組みを利用することができる。

これは複数のDBプロセスをクラスタリングすることで冗長性を確保する仕組みで、
従来のレプリケーションと違って自動でのFailOverを実現している。

具体的にはPrimaryであるメンバが各種クエリを受付、Read,Writeを行う。
書き込まれた内容はSecondaryにミラーリングされる。
下記図を参照。

実際にReplica Setsを構築する為の手順を以下に示します。

複数のmongodプロセスを立ち上げる。
その為のデータディレクトリを作成。

$ mkdir -p ./data/repltest1
$ mkdir -p ./data/repltest2
$ mkdir -p ./data/repltest3

mongodプロセスの立ち上げ

$ mongod --replSet repltest --port 27017 --dbpath ./data/repltest1 --rest
$ mongod --replSet repltest --port 27018 --dbpath ./data/repltest2 --rest
$ mongod --replSet repltest --port 27019 --dbpath ./data/repltest3 --rest

--restオプションを付与するとHTTP Admin UIでの詳細表示が可能となる。

Replica Setsの設定

$ mongo localhost:27017

以下mongoコンソールで入力
クラスタを定義
_idにはmongod立ち上げ時の--replSetの値をセットする。

config = {_id: 'repltest', members: [
   {_id: 0, host: 'localhost:27017'},
   {_id: 1, host: 'localhost:27018'},
   {_id: 2, host: 'localhost:27019'}]
}

定義したクラスタをrs.initiate()に流しこむ。

> rs.initiate(config);
{
        "info" : "Config now saved locally.  Should come online in about a minute.",
        "ok" : 1
}

状態を確認

> rs.status();
{
        "set" : "repltest",
        "date" : "Tue Oct 05 2010 11:56:23 GMT+0900 (JST)",
        "myState" : 1,
        "members" : [
                {
                        "_id" : 0,
                        "name" : "HOSTNAME.local:27017",
                        "health" : 1,
                        "state" : 1,
                        "self" : true
                },
                {
                        "_id" : 1,
                        "name" : "localhost:27018",
                        "health" : 1,
                        "state" : 2,
                        "uptime" : 14,
                        "lastHeartbeat" : "Tue Oct 05 2010 11:56:21 GMT+0900 (JST)"
                },
                {
                        "_id" : 2,
                        "name" : "localhost:27019",
                        "health" : 1,
                        "state" : 2,
                        "uptime" : 14,
                        "lastHeartbeat" : "Tue Oct 05 2010 11:56:21 GMT+0900 (JST)"
                }
        ],
        "ok" : 1
}

詳細はHTTP Admin UIで確認できる。
ブラウザでhttp://localhost:28017を開く。
(Admin UIはデフォルトではmongodでのportに1000を加えたものが割り当てられる)

Replica Setsの詳細は下記URLより確認。
http://localhost:28017/_replSet

Secondaryの生存確認は2秒間隔で実施される。


この状態でPrimaryをctrl + c等で停止してみる。
すると下記の状態に遷移する。

従来のSecondaryがPrimaryに昇格しているのがわかる。

再び停止していたプロセスを立ち上げるとSecondaryとしてデータの同期が行われる。


このようにPrimaryが落ちた場合でも自動で他のプロセスが昇格することでauto failoverを実現している。

参考:MongoDB ReplicaSets



* mkdir のオプションが大文字だったのを修正

高専カンファレンスのLTで発表しました

高専カンファレンス2010秋 東京のLTで発表しました。

今回はテーマとして物事を正しく捉えて向き合い、そのために必要なことについて経験を元にお話しました。
そのスライドと動画です。

動画は53分あたりから。

第6回ジオメディアサミットで発表しました

2010/9/20 パシフィコ横浜で開催されたジオメディアサミットで発表しました。

当日のスライドと動画です。
ustして頂いたTechWaveさん、ありがとうございます。

当日はエンターテイメントセッションに割り当てられていたのですが、
空気を読まずに半分技術の話をしました。

何か質問、ご指摘等ありましたらお願いします。

Rails MySQLでintegerカラムに潜む罠

Railsでinteger型のカラムにinsertする際にハマったのでメモ。

以下の様なテーブルがあったとする。

  user_id, integer
  point, integer

これに対してActiveRecordを用いてintegerのmax値を超えたデータをinsertする。

  PointList.create(:user_id => 2, :point => 2247483647
  => #

と一見正常に見えるがデータをロードしてみるとintegerのmax値に丸められてしまっている。

  PointList.all
  [#]

この問題はpointカラムにvalidates_uniqueness_ofを設定している場合でもvalidate時点ではintegerを超えた値でも、
既に登録されているデータと値が異なる場合valid?がtrueとなる。

この為重複したデータがMySQL内に存在することになってしまう。

Ruby on Rails 2.3.8とMySQL5.1.48で確認しました。
SQLiteではこの問題は発生しませんでした。