Sinatra + ActiveRecord + sqlite3覚書
はじめに
sinatra + activerecord + sqlite3 の覚書なぞ。掲示板作りを例に取りながら。
準備
必要な環境
- ruby 1.9x 以上推奨:1.8.7 だと tag: content 形式が使えないため、いろいろエラーに悩まされます。ここでは 2.1.2を使ってます。
- sqlite3:データベース。手軽にこれでいきましょう。なければあらかじめ sudo yum install sqlite3 しておいてください。
その他必要な gem パッケージは、その都度追加していきます。
bundle による gem のインストール
まずは必要なgem をインストールする bundle から
% bundle init Writing new Gemfile to /home/tutorial/bbs/Gemfile % vi Gemfile source "https://rubygems.org" gem 'sinatra' gem 'sinatra-contrib' # for Sinatra/Reloader group :development do gem 'shotgun' gem 'tux' end % bundle install --path vendor/bundle ...
sinatra と、開発環境要にサーバー起動用 shotgun とインタプリター tux も入れておきます(使わなければ入れなくても可)。
bundle は --path vendor/bundle をつけてプロジェクトごと管理することを強く推奨。じゃないと後で conflict してワケワカになる未来が見えます。VM上で開発してる時にエラーが出たら Resolving bundle install Text file busy error | JoeQueryを参照。
アプリひな形の作成
アプリ本体のひな形を作っておきます。
% vi app.rb require 'bundler' Bundler.require module MyBbs class Application < Sinatra::Base configure :development do register Sinatra::Reloader end # routing get '/' do "hello world" end end end
モジュール方式の方が後々いいので、そっちで。 Bundler.require とすると、さきほど作った Gemfile 通りにrequire してくれるので便利です。
また、Sinatra::Reloader を register すると、ソースを書き換えると自動的にサーバーが読み込んでくれます。shotgunを使うのなら無くても可。
続いて config.ru
% vi config.ru require './app' run MyBbs::Application
決まり文句ですね。app.rb の方で run! if app_file == $0 させて ruby app.rb もいいのですが、一応作法に則ったほうがいいでしょう。
動作確認
ここまで準備ができたらとりあえず起動
% bundle exec rackup [2014-07-22 20:01:05] INFO WEBrick 1.3.1 [2014-07-22 20:01:05] INFO ruby 2.1.2 (2014-05-08) [x86_64-linux] [2014-07-22 20:01:05] INFO WEBrick::HTTPServer#start: pid=xxxx port=9292
bundle を使ってるので、通常のコマンドの前に "bundle exec" を付けなければなりません。毎回打つの面倒だったら、alias しておくといいかも。rackup も面倒なら、
% vi ~/.bashrc alias be="bundle exec" alias ru="bundle exec rackup"
などとするといいですね。以下これを使います。入り直すか source ~/.bashrc; hash -r するのを忘れずに。
localhost:9292 にアクセスすると "hello world" されるはず。rackup -p 8888 でポート指定。
掲示板アプリの作成
仕様
簡単な仕様:
- フロート式。新しいポストから順に表示
- 追加するだけ。最大1000件
- / で全部表示
- /l100 などで最新100件
- /23 で 23番のポスト表示
実装としては、
- データベースは sqlite3 で ActiveRecord を介して使用
- ビューは erb
データベースのレコードはpost として;
- id: integer primary key autoincrement
- username: string # ポストしたユーザ名
- content: string # 内容
- created_at, updated_at: datetime # 作成日、更新日。activerecord がよしなにしてくれる
必要パッケージの追加インストール
各種gemは bundle で入れます。2回目からは --path をつけなくても ./bundle/config を見るので不要です。
% sudo yum install sqlite3 % vi Gemfile ... gem 'sqlite3' gem 'activerecord' gem 'sinatra-activerecord' gem 'rake' ... % bundle install
sinatra-activerecord と rake は、データベースをマイグレートするのに使います。後で触れます。
大枠の実装(Controller, View)
データベースに入る前に、やっぱ外見から、つまりどのリクエストを受ける(controller)とどのように表示される(view)かを頭に置いておいたほうがやりやすいので、そちらから。
view は、haml の方が長期的にはいいのですが、ここは簡単のため erb で行きます。 また、いきなり views/*.erb にテンプレートファイルを作ってもいいのですが、最初はインラインで様子見します。長くなったりある程度固まってきたらファイルに移しましょう。
configure の中に enable :inline_templates を入れておくと、あとでファイルに分けた時でもちょこっとテストできるのでよいです。
% vi app.rb ... class Application < Sinatra::Base configure :development do enable :inline_templates ... get '/' do erb "view all" end get '/l:count' do erb "view latest" end get '/:id' do erb "view post" end post '/post' do # post end end
とルーティングを書きます。レイアウトのテンプレートを __END__
の後に書きます。
__END__ @@layout <html> <head><title>mybbs</title></head> <body> <h1>My BBS</h1> <div> [<a href="/l50">latest</a>] [<a href="/">all</a>] [<a href="/1">id:1</a>] <hr/> <div> <%= yield %> </div> <hr/> <form action="/post" method="post"> <div> <label for="username">Name:</label> <input type="text" name="username" id="username" size="50"> </div> <div> <textarea name="content" cols="80" rows="20"></textarea> </div> <div> <input type="submit" value="Post"> </div> </div> </body> </html>
localhost:9292 でアクセスすると、
My BBS [latest] [all] [id:1] --- view all --- Name: [ ] +---------------------------+ | | | | | | | | | | +---------------------------+ [Post] powered by my bbs system
のように表示されるかと。イメージがだいぶわきますね。レイアウトで大枠を作って、リクエストごと切り分けて内容を yield に差し込む、という感じですね。実際にデーターベースにどうアクセスし、表示させるかは、次項より。
データベースの作成
さて、投稿内容を表示したり保存するために、データベースが必要です。 まずは model だけ先に作ってしまいましょう。といってもこれだけ:
% vi app.rb ... module MyBbs class Post < ActiveRecord::Base end ...
ActiveRecord::Base から派生したクラスを通じてデータベースとアクセスできます。クラス名を Post としたので、データベースのテーブルは posts としましょう、rails的に。
で、データベースを作成します。 直接 sql を叩いてもいいんですが、せっかくですので rake db:migrate を使います。
...ruby module MyBbs class Application < Sinatra::Base ... configure do register Sinatra::ActiveRecordExtension set :database, {adapter: "sqlite3", database: "db/bbs.db"} end ...
データベースへの接続は、ActiveRecord::Base.establish_connection("adapter" => "sqlite3", "database" => "./db/posts.db")
と書くことが多いですが、Sinatra::ActiveRecordExtension を register して上げると set :database が使えるので、こっちのほうがスマートですね。これで、sqlite3を使って db/bbs.db にコネクトします。
次に、テーブルやらカラムを作らないと行けないのですが、rake の枠組みを利用します。gem 'sinatra-activerecord'に入っている 'sinatra/activerecord/rake' が助けてくれます。
% vi Rakefile require 'sinatra/activerecord' require 'sinatra/activerecord/rake' require './app'
と Rakefile を用意してあげ rake -T を走らせると、いろいろタスクが登録されているのがわかります。
% be rake -T rake db:create # Creates the database from DATABASE_URL or con... rake db:create_migration # Create a migration (parameters: NAME, VERSION) rake db:drop # Drops the database from DATABASE_URL or confi... rake db:fixtures:load # Load fixtures into the current environment's ... rake db:migrate # Migrate the database (options: VERSION=x, VER... rake db:migrate:status # Display status of migrations ...
この内、db:create_migration と db:migrate を使います。
% be rake db:create_migration NAME=create_posts VERSION=001 db/migrate/001_create_posts.rb % cat db/migrate/001_create_posts.rb class CreatePosts < ActiveRecord::Migration def change end end
としてあげると、データベースのテーブル、カラムを作る用のスクリプトを吐いてくれます。NAMEで与えたのが camelize されてクラス名に、VERSION がファイル名の頭につきます。NAME は必須で、VERSION はつけなければ日付その他から自動的に作ってくれるのですが、001 などと指定したほうが扱いやすいでしょう。
んで、これをいじってテーブルとカラムを作っていきます。
% vi db/migrate/001_create_posts.rb class CreateNotes < ActiveRecord::Migration def change create_table :posts do |t| t.string :username t.string :content t.timestamps end end end
ってなかんじで作ります。プライマリーキーのid は自動的に入りますので書かなくてもよいです。t.timestamps を入れると、 updated_at と created_at を datetime で加えてくれます。
昔は self.up(), self.down() でージョンを上げた時下げた時の処理を書いてたようですが、今は自動的に解析してくれるようですので、up 処理のみを change() に書きましょう。
このスクリプトを元にデータベースを更新します。
% be rake db:migrate == 1 CreatePosts: migrating =================================================== -- create_table(:posts) -> 0.0273s == 1 CreatePosts: migrated (0.0275s) ==========================================
すると ./db/bbs.db に作ってくれてるはずなので確認します。
SQLite version 3.6.20 Enter ".help" for instructions Enter SQL statements terminated with a ";" sqlite> .schema CREATE TABLE "posts" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "username" varchar(255), "content" varchar(255), "created_at" datetime, "updated_at" datetime); CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL); CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version"); sqlite> select * from posts; sqlite>
テーブル posts が作成されてますね。中身は当然空です。schema_migrations はバージョン管理用なのであまり気にしなくてよいかと。
データベースが出来たので、これを表示してみます。といってもまだ空だけど。
これでさきほど作成した model クラスの MyBbs::Post を使ってデータベースにアクセスできます。tux でいじくってみましょう。
% be tux Loading development environment (Rack 1.2) >> MyBbs::Post.all => #<ActiveRecord::Relation []> >> post1 = MyBbs::Post.new(:username=>'nanashi1', :content=>'hogehoge') => #<MyBbs::Post id: nil, username: "nanashi1", content: "hogehoge", created_at: nil, updated_at: nil> >> post1.save => true >> post1 => #<MyBbs::Post id: 1, username: "nanashi1", content: "hogehoge", created_at: "2014-07-22 05:28:47", updated_at: "2014-07-22 05:28:47"> >> post2 = MyBbs::Post.create => #<MyBbs::Post id: 2, username: nil, content: nil, created_at: "2014-07-22 05:29:17", updated_at: "2014-07-22 05:29:17"> >> MyBbs::Post.all => #<ActiveRecord::Relation [#<MyBbs::Post id: 1, username: "nanashi1", content: "hogehoge", created_at: "2014-07-22 05:28:47", updated_at: "2014-07-22 05:28:47">, #<MyBbs::Post id: 2, username: nil, content: nil, created_at: "2014-07-22 05:29:17", updated_at: "2014-07-22 05:29:17">]> >> post2.delete => #<MyBbs::Post id: 2, username: nil, content: nil, created_at: "2014-07-22 05:29:17", updated_at: "2014-07-22 05:29:17"> >> MyBbs::Post.all => #<ActiveRecord::Relation [#<MyBbs::Post id: 1, username: "nanashi1", content: "hogehoge", created_at: "2014-07-22 05:28:47", updated_at: "2014-07-22 05:28:47">]> >> quit
とこのように、記事一覧を出したり、記事を作ったり消したりできます。post1.content = "hogehogehoge" で内容を変更することも出来ますのでいろいろ試してみてください。
で、これらを app.rb に組み込んでいきます。
詳細処理の記述
さて、これである程度準備ができましたので、実際の処理を書き込んでいきます。まずは指定された記事を表示する:
% vi app.rb module MyBbs class Application < Sinatra::Base configure do include ERB::Util # h() 用 ... get '/:id' do if post = MyBbs::Post.where(:id=>params[:id].to_i).first erb :view, :locals => {:post => post} else erb "no such post, id: #{h params[:id]}" end end
Post.where(:id=>id).first でid の記事を取り出します。find(:id)だと見つからないとエラー吐くので、where で。取り出したpost オブジェクトをerb に locals で渡してあげます。
あ、かならず出力はエスケープするように。ERB::Util の中にエスケープする関数 h() が入ってるので include しておきます。
これを受けてview では、
@@view <div class="post"> <div> [<a href="/<%= post.id.to_i %>"><%= h post.id %></a>] <%= h post.username %> ( <%= h post.updated_at %>)</div> <div> <%= h post.content %></div> </div>
のように、username, content などを表示します。updated_at は activerecord が面倒みてくれてます。
後は同様に、データベースから引っ張ってきて view に渡すと、複数表示が出来ます。:layout=>false をつけると、レイアウトテンプレートを適用しません。入れ子にする時は指定すること。
get '/' do erb MyBbs::Post.order("id desc").limit(1000).map {|post| erb :view, :locals => {:post => post}, :layout=>false }.join("") end get '/l:id' do erb MyBbs::Post.order("id desc").limit([1000, params[:id].to_i].max).map {|post| erb :view, :locals => {:post => post}, :layout=>false }.join("") end
ポストは、
post '/post' do post = MyBbs::Post.new(:username=>params[:username], :content=>params[:content]) post.save redirect '/' end
のように new して save してあげます。ほんとは validation やsave の確認が必要ですけどね。 終わったら / にリダイレクトします。
まとめ
- gem はローカルに bundle で一括
- rake db:migrate を活用
- 単純なものから routing と view を作り上げていく
参考サイト
- sinatra-activerecord/README.md at master · janko-m/sinatra-activerecord · GitHub : https://github.com/janko-m/sinatra-activerecord/blob/master/README.md
- prima materia - diary : sinatra-activerecordを使ってマイグレーションを作る : http://materia.jp/blog/20121029.html