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を使え状態です。
しかも使う機会があるのかさえ微妙です。
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ボタンも付けられる。これを利用してページ遷移することなく元のページに戻ることができる。
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を使って開発している為、デフォルトで扱うjavascriptをjQueryに変更する。
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を実現している。
* 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ではこの問題は発生しませんでした。