Rails 4.1のenumの挙動
みんな大好きenum型
DBのカラムでステータスとか使うとき、ぼくは数値派なんですけど、enumで掛けたら楽で良いですよねみたいのあります。ありますよね。
で、Rails 4.1ではそういうのが導入されたっぽいので試してみました。
rails 4.1をインストールしてプロジェクト作ったりする
$ gem install rails --pre --no-ri --no-rdoc $ rails new sample $ cd sample $ ./bin/rails g scaffold product name:string status:integer $ ./bin/rake db:migrate
rails c
でフツーに登録してみる(ここは何の変哲も無い)。
irb(main):005:0> Product.create!(name: "hoge", status: 99) (0.1ms) begin transaction SQL (0.2ms) INSERT INTO "products" ("created_at", "name", "status", "updated_at") VALUES (?, ?, ?, ?) [["created_at", "2014-03-26 01:25:21.026233"], ["name", "hoge"], ["status", 99], ["updated_at", "2014-03-26 01:25:21.026233"]] (1.9ms) commit transaction => #<Product id: 2, name: "hoge", status: 99, created_at: "2014-03-26 01:25:21", updated_at: "2014-03-26 01:25:21">
statusの値が99でも保存できちゃう。そのままでも良いんだけど、validatesのおさらいとして例えば0〜2までに制限してみようか。
class Product < ActiveRecord::Base validates :status, inclusion: {in: 0..2} end
ためしてみよう
Loading development environment (Rails 4.1.0.rc2) irb(main):001:0> Product.create!(name: "hoge", status: 99) (0.1ms) begin transaction (0.1ms) rollback transaction ActiveRecord::RecordInvalid: Validation failed: Status is not included in the list irb(main):003:0* Product.create!(name: "hoge", status: 2) (0.1ms) begin transaction SQL (0.4ms) INSERT INTO "products" ("created_at", "name", "status", "updated_at") VALUES (?, ?, ?, ?) [["created_at", "2014-03-26 01:27:51.238248"], ["name", "hoge"], ["status", 2], ["updated_at", "2014-03-26 01:27:51.238248"]] (1.6ms) commit transaction => #<Product id: 3, name: "hoge", status: 2, created_at: "2014-03-26 01:27:51", updated_at: "2014-03-26 01:27:51">
数値の範囲指定ができましたね。
それではenumを使ってみよう
enumの設定をしてみよう。ややこしくなるので、validatesは消します。ついでに書いておくと、enumの項目自体はテキトウなのでスルー推奨です。
class Product < ActiveRecord::Base enum status: { normal: 0, # 通常 sale: 1, # 特売 empty: 2, # 在庫切れ } end
これで試してみよう。まずは正常系で。
irb(main):005:0* Product.create!(name: "hoge", status: 2) (0.1ms) begin transaction SQL (0.2ms) INSERT INTO "products" ("created_at", "name", "status", "updated_at") VALUES (?, ?, ?, ?) [["created_at", "2014-03-26 01:33:06.501462"], ["name", "hoge"], ["status", 2], ["updated_at", "2014-03-26 01:33:06.501462"]] (1.6ms) commit transaction => #<Product id: 6, name: "hoge", status: 2, created_at: "2014-03-26 01:33:06", updated_at: "2014-03-26 01:33:06"> irb(main):006:0> Product.create!(name: "hoge", status: :sale) (0.1ms) begin transaction SQL (0.6ms) INSERT INTO "products" ("created_at", "name", "status", "updated_at") VALUES (?, ?, ?, ?) [["created_at", "2014-03-26 01:33:13.877152"], ["name", "hoge"], ["status", 1], ["updated_at", "2014-03-26 01:33:13.877152"]] (1.5ms) commit transaction => #<Product id: 7, name: "hoge", status: 1, created_at: "2014-03-26 01:33:13", updated_at: "2014-03-26 01:33:13"> irb(main):005:0> Product.create!(name: "hoge", status: 'sale') (0.1ms) begin transaction SQL (0.6ms) INSERT INTO "products" ("created_at", "name", "status", "updated_at") VALUES (?, ?, ?, ?) [["created_at", "2014-03-26 01:57:26.957196"], ["name", "hoge"], ["status", 1], ["updated_at", "2014-03-26 01:57:26.957196"]] (1.4ms) commit transaction => #<Product id: 15, name: "hoge", status: 1, created_at: "2014-03-26 01:57:26", updated_at: "2014-03-26 01:57:26">
このように、シンボルや文字列、そして数値でも保存してくれるようになった。便利。
では範囲外の値を入力すると?
irb(main):007:0> Product.create!(name: "hoge", status: 99) ArgumentError: '99' is not a valid status irb(main):009:0* Product.create!(name: "hoge", status: :hoge) ArgumentError: 'hoge' is not a valid status irb(main):011:0* Product.create!(name: "hoge", status: 'hogehoge') ArgumentError: 'hogehoge' is not a valid status
ことごとくArgumentErrorとなる。create!
で試しているけど、create
やnew
でも試してみよう。
irb(main):007:0> Product.create(name: "hoge", status: 99) ArgumentError: '99' is not a valid status irb(main):009:0* Product.new(name: "hoge", status: 99) ArgumentError: '99' is not a valid status
create!だけに限らず、newメソッドとかでも引数に異なる範囲の値が入るとArgumentErrorになる。
普通のvalidatesに条件を書く場合は ActiveRecord::RecordInvalid: Validation failed: Status is not included in the list
みたいにActiveRecord::RecordInvalidの例外クラスが送出されるけど、そうではない(例外クラス的にも、発生タイミング的にも)ので気をつけた方が良さそう。
validatesではないから挙動が異なるのは当然といえば当然だけど、使い勝手としては同じような挙動のほうが嬉しかったんだけどそうではなかったという備忘録エントリーです。
Rails詳しくないのでよくわかりませんが、ここら辺、正式リリースまでに挙動が変わったりするかしら?
書かなくてはならない事柄(追記)
こちらも何卒よろしくお願い致します。
![Webアプリエンジニア養成読本[しくみ、開発、環境構築・運用…全体像を最新知識で最初から! ] (Software Design plus) Webアプリエンジニア養成読本[しくみ、開発、環境構築・運用…全体像を最新知識で最初から! ] (Software Design plus)](http://ecx.images-amazon.com/images/I/51VavR1gcyL._SL160_.jpg)
Webアプリエンジニア養成読本[しくみ、開発、環境構築・運用…全体像を最新知識で最初から! ] (Software Design plus)
- 作者: 和田裕介,石田絢一(uzulla),すがわらまさのり,斎藤祐一郎
- 出版社/メーカー: 技術評論社
- 発売日: 2014/03/11
- メディア: 大型本
- この商品を含むブログ (3件) を見る
SQLite3の実際の型
SQLiteの型のなぞ
SQLite3の型はスゲーざっくりしていると聞いていたのだけど、RailsでデータベースをSQLiteにして、適当なテーブルを作成したら、なんか varchar とか出てくる。どういうことなんだってばよ? というのを調査してみた。
まずはRailsでテーブルを作るところまで
結論から言うと、別にRailsを使う必要は無いんだけども、まぁせっかくなんでね。
ちなみに、-T は unit/test のテンプレートを作らないというオプションで、単なる手クセなので気にしないで(普段はrspecを使うので)。
% rails new sample -d sqlite3 -T (snip) cd sample % rails g scaffold blog title:string body:text like:integer % bundle exec rake db:migrate
ちなみに、ここの例ではテキトーなブログシステムを作る、みたいのを想定してもらいたい。likeはそのブログに対するいいね数みたいなものだと思って。
データの確認
まず、念のためRails側のmigrateでどんな型が指定されているかを確認する。
% cat db/migrate/20111113064850_create_blogs.rb class CreateBlogs < ActiveRecord::Migration def change create_table :blogs do |t| t.string :title t.text :body t.integer :like t.timestamps end end end
あたりまえだけど、指定した通りである。
さて、SQLiteでDBを開いてみよう(rails db とかでも大丈夫だと思うけど)。
% sqlite3 db/development.sqlite3
では、さっきのテーブルの中身を見てみよう。
sqlite> .dump blogs PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE "blogs" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(255), "body" text, "like" integer, "created_at" datetime, "updated_at" datetime); COMMIT;
整形してなくてアレだけど、varchar や datetime とかが指定されてる。無いんじゃないの。
ここからが本題
こういう時は公式のドキュメントにあたるのが一番ですね。
で、ここをきちんと読んでみると、1.0 Storage Classes and Datatypes に、データ型として5つあるよと書いてある(このデータ型はカラムの型ではなく、実データを扱う際の型らしい?)。
- NULL
- INTEGER
- REAL
- TEXT
- BLOB
さらに、1.2 では Date型を保持する型としては TEXT の場合と REAL の場合と INTEGER の場合があるとか書いてある。
というわけで
今回の場合だと以下のような型に変換されているような気がする。
- title => TEXT
- body => TEXT
- like => INTEGER
- created_at => NUMERIC
- updated_at => NUMERIC
なんとなくわかったのだけど、その変換された型を実際に確認する場合ってどうすれば良いんですかね。。。
ActiveRecord と実際のDBの型の対応を確認する
rails console で確認できる
確認方法
ActiveRecord::Base.connection.native_database_types
でアクセスすると確認できる。
実際にやってみる
rails console して、上記で書いたプロパティを確認する。
ruby-1.9.2-head > pp ActiveRecord::Base.connection.native_database_types { :primary_key=>"int(11) DEFAULT NULL auto_increment PRIMARY KEY", :string=>{:name=>"varchar", :limit=>255}, :text=>{:name=>"text"}, :integer=>{:name=>"int", :limit=>4}, :float=>{:name=>"float"}, :decimal=>{:name=>"decimal"}, :datetime=>{:name=>"datetime"}, :timestamp=>{:name=>"datetime"}, :time=>{:name=>"time"}, :date=>{:name=>"date"}, :binary=>{:name=>"blob"}, :boolean=>{:name=>"tinyint", :limit=>1} }
こんな感じになる。
Rails3で困っちゃった事リスト
Rails2.x 系の情報だと動かない!!!
Rackアプリケーションになったことやモジュール化を進めた影響だと思うのだけど、Rails2.x系ではうまく行くと書いてあるやり方が Rails3 になってうまくいかなかったりしたので、実際にハマった事をメモしておくよ。
ちなみに、環境は以下の通り
以下はあくまでも自分自身のログなので、Rails3……というか Rails の作法としてはこうするんだよ、というベストアンサーがあったらぜひ教えてほしいです。
Session をブラウザが終了しても終わらないようにしたい
ログインした後のいわゆる Session の有効期限を変更したい
今までなら
に書いてあるように
ActionController::Base.session_options[:session_expires] = 1.months.from_now
こんな感じで更新できた。
が、Rails3 では ActionController::Base に session_options なんてネーよと言われる。
対応
request.session_options[:expire_after] = 1.months.from_now
このように、 request オブジェクトに設定させる。
アクセス毎に request.session_options.inspect してもなぜか :expire_after は nil になってしまっていて果たしてこれが全うな設定方法なのかは疑問。……たまたま別の要因でうまく行ってるだけかもしれないけれど。
Production モードで動かしたら画像ファイルが見れなくなった
Production モードで send_file で表示してる画像が0byteで見れない
いきなり見れなくなる(エラーとかでない)のでビビった。 public ディレクトリが見れなくなるのとは別の問題で、どうやら Rails3 での不具合?らしい。
この Rails プロジェクトを作ったときは Rails3 RC だったので、解消されているかもしれない。
このような状態になる。
対応
上記のサイトにある通り、 config/environments/production.rb にある
config.action_dispatch.x_sendfile_header = "X-Sendfile"
をコメントアウトにして事なきをえた。
深追いしていないからわからないけれど
routes.rb でネストした URL を作りたい
管理者画面を作るときとか
テーブル構成はネストしていない状態で、 controller だけネストさせたい。
例えば、以下のようなURLでhogeテーブルに対する CRUD 操作をしたいとか。
/admin/hoge
対応
routes.rb には以下のように設定する
scope :module => 'admin' do match 'admin/hoge' => 'hoge#show' end
controller はディレクトリ構成とモジュール構成に注意する
app/controllers/admin/hoges_controller.rb
class Admin::HogesController < ApplicationController (...) end
view は model_path メソッドを変更する必要がある。
model_path にすると、 /admin にネストしないので、 admin_model_path のようにする必要がある。
Rails.root/lib が読み込まれない
Rails3 から読み込まれなくなったらしい
過去の事をしらないので良くわかってないが、こんな風にすると良いらしい
config/application.rb
# RAILS_ROOT/lib/hoge_dir/fuga.rb があるような場合を想定 config.autoload_paths += %W(#{config.root}/lib/hoge_dir)
log ファイルのパスはドコ
Rails が現在使っている log ファイルのパスを知りたい
決め打ちじゃなくて、logger が使ってるパスを知りたい場合。以下のサイトが参考になった。
対応
log.paths ってことは複数存在することを前提にしているようだけど、用途は良くわかってない。少なくとも、狙いのファイルは先頭にあるので*2、以下で取れる。
Rails.configuration.paths.log.paths.first
log ファイルのローテーションしたい
log ファイルを適宜変更させる
ここだと config/environment.rb に書けとあるけど、 Rails3 だと有効ではないっぽい。
対応
しかたないので config/environments/*.rb にそれぞれ記述することにした。
記述内容自体は上記のサイト同様、以下のように書けば良い*3。
config.logger = Logger.new(Rails.configuration.paths.log.paths.first, 'daily')
追記1
id:pinzolo さんにコメント頂きました。
application.rb に書くと良いらしいです。
> ここだと config/environment.rb に書けとあるけど、 Rails3 だと有効ではないっぽい。
以前の config/environment.rb の内容は config/application.rb に変更されてますので、config/application.rb に書けば全環境で読み込んでくれますよ。
メールの受信(パース)
メールを Rails アプリ側で取り込む場合
ここの「メールの受信」を ISO2022-JP な Subject のメールを食わせるとエンコーディングの不一致で死ぬ*5。
正確には Mail クラスのフィールドの取扱い方がまずいようだ。
しょうがないのでモンキーパッチを当てた
ただ、これだと元々の Mail ライブラリのテストで何件か fail してしまうので、良い直しでは無い。本当に、その場しのぎである。
また、この対応でサブジェクトは対応できるが、添付ファイル名のエンコーディングにはうまく対応できていないので、そちらはそちらで別途対応を考える必要がある。
今回は妥協できる範囲だったので、上記の対応で辞めてしまったのですが、これ、みんな困ってないのかしら???
モデルにメソッドを持たせる
こんな感じの投稿システムがあったとします
sqlite> .tables
comments posts
む、sqlite3には mysql とかの describe 相当のコマンドが無いのだろうか。
とりあえずデータを突っ込んだ select で勘弁してね。
id|title|body|created_at|updated_at 1|title1|body|2010-09-25 05:55:18.912592|2010-09-25 05:55:18.912592 2|title2|body|2010-09-25 05:55:31.281979|2010-09-25 05:55:31.281979
いわゆるブログのエントリ的なものを想定して、titleとbodyっていうカラムがある。
そして、その各エントリにcommentが付く。
id|post_id|name|body|created_at|updated_at 1|1|hogehoge|comment1|2010-09-25 05:57:32.057127|2010-09-25 05:57:32.057127 2|1|hogehoge|comment2|2010-09-25 05:57:39.476946|2010-09-25 05:57:39.476946 3|1|hogehoge|comment3|2010-09-25 05:57:42.828758|2010-09-25 05:57:42.828758 4|2|unko|comment1|2010-09-25 05:57:53.740980|2010-09-25 05:57:53.740980 5|2|hogehoge|comment2|2010-09-25 05:58:03.020882|2010-09-25 05:58:03.020882
コメントには、対象となる post_id をもって紐付けさせる。
モデルでの関連付け
vim app/models/post.rb
has_manyを付ける
class Post < ActiveRecord::Base has_many :comments end
rails console で以下のように試すと、コメントデータも取れる。
Post.find(1).comments
=> [#, # , # ]
投稿メッセージにコメント数をつけたい
いわゆる commnet(1) とかをつけたい場合ね。
上記が試せていれば、
Post.find(1).comments.length
とかで取れる訳だけど、json などで渡すときに不都合ですよね。
まず、モデルにメソッドを定義する
一応、何回も呼ばれたときの為にキャッシュしておく。
class Post < ActiveRecord::Base has_many :comments def comment_count @comment_count ||= comments.length end end
rails console で確認してみよう。
ruby-1.9.2-head > Post.find(1).comment_count
=> 3
おお、できてる。
to_json(to_xml)でメソッド呼び出しをする
:methods オプションで呼べる。
ruby-1.9.2-head > puts post.to_json(:methods => :comment_count)
{"post":{"body":"body","created_at":"2010-09-25T05:55:18Z","id":1,"title":"title1","updated_at":"2010-09-25T05:55:18Z","comment_count":3}}
整形するとこんな感じで、最後に comment_count っていうのが付く
{ post: { body: body (string) ,created_at: 2010-09-25T05:55:18Z (string) ,id: 1 (number) ,title: title1 (string) ,updated_at: 2010-09-25T05:55:18Z (string) ,comment_count: 3 (number) } }
整形は例のごとく JSON整形 を使った。
ちなみに、:methods で複数のメソッドを呼びたい場合は [:method名, :method名]のような感じで配列にすると良い。
これで
データを弄り回さずに出力用の形式に整形できますね。
enjoy!
Rails のモデル関係と to_json(to_xml)
テーブル間のリレーションについて
ユーザ情報テーブルと、ユーザが持ってる所持品のテーブル的なものがあるとして。
面倒なのでスゲー適当だけど以下のような定義で作った。
rails g scaffold user name:string item_id:integer
rails g scaffold item name:string
item_id は items テーブルの id が入るってことね。
- users
- name:string
- item_id:integer
- items
- name:string
これで、ユーザはitem_idに items テーブルのidを一つもつ感じ。
これを json にして出力したいぜ
例えば以下のようなデータがあるとして
- users
- id => 1
- name => aaaa
- item_id => 1
- items
- id => 1
- name => item1
irbで確認するとこんな感じ。
ruby-1.9.2-head > User.find_by_id(1)
=> #
ruby-1.9.2-head > User.find_by_id(1).item
=> #
うん、予定通りのデータが入ってるね。じゃあ、これを json で出力してみましょう。
ruby-1.9.2-head > User.find_by_id(1).to_json
{ user: { created_at: 2010-09-13T21:18:44Z (string) ,id: 1 (number) ,item_id: 1 (number) ,name: aaaa (string) ,updated_at: 2010-09-13T21:18:44Z (string) } }
たしかに json 形式なんだけど、item の値が入っていない。あんまりだ。
ちなみに、json の整形は JSON整形 を使った。
to_json にはオプションがいろいろある
テストデータがアレな気がするけど、オプションについて詳しく載っていてとても良い。
つまり :include しろってことだってばよ
ruby-1.9.2-head > puts User.find_by_id(1).to_json(:include => :item)
{ user: { created_at: 2010-09-13T21:18:44Z (string) ,id: 1 (number) ,item_id: 1 (number) ,name: aaaa (string) ,updated_at: 2010-09-13T21:18:44Z (string) ,item: { created_at: 2010-09-13T21:17:10Z (string) ,id: 1 (number) ,name: item1 (string) ,updated_at: 2010-09-13T21:17:10Z (string) } } }
やった、itemのデータも引っ張ってこれた!
ただ、これを出力データとするにはちょっと冗長すぎる。updated_at とか必要かっつーと入らなかったりする。
:only や :except で出力を制限できる
例えばこんな感じだ
User.find(:all)[0].to_json({:include => {:item => {:only => [:id,:name]}}, :only => [:name]})
{ user: { name: aaaa (string) ,item: { id: 1 (number) ,name: item1 (string) } } }
これ、only が items にも users にも指定してあって、ちょっと重複した感じがしてイヤなのだけど、上位(この場合 users)で指定した場合、その値が子の要素にも only の設定が伝播してしまう。
今回の場合、両方とも name しかないからあまり実害はないけれど、実際の場合はそんな訳はないので、個別に出力する属性を指定する感じの方が良いと思う。
ちなみに、to_xml でも同様です。
じゃあ、respond_with の場合ってどうなるのよ
こんな感じで実装できた。
class UsersController < ApplicationController respond_to :html, :xml, :json # GET /users # GET /users.xml def index @users = User.all respond_with(@users, {:include => {:item => {:only => [:id,:name]}}, :only => [:name]}) end (略) end
従来の respond_to の場合はこんな感じだろうか(面倒なので、とりあえず json の場合のみ)。
respond_to do |format| format.html # index.html.erb format.xml { render :xml => @users } format.json { render :js => @users.to_json({:include => {:item => {:only => [:id,:name]}}, :only => [:name]}) } end
こう見ると respond_with の cool 感が際立ってやべぇすな。
*1:意味的にあったるかは微妙な気がする。itemの所属するって変じゃね!?
Railsで定数を別ファイルで管理したい? よろしい、ならば Settingslogic だ。
ソースの中で定数として扱うのではなく、設定ファイル的に扱いたい
Rubyはそれなりに長い間使っていますが、Railsはメジャーバージョン毎にちょっと触るくらいで、まったくベストプラクティスとか知らないんですね。
で、いまは作る際のお手本として 日本Ruby会議 2010, 8月27日〜29日 のソースである ruby-no-kai/rubykaigi · GitHub を見てパクれそうなのはパクるというスタンスでやってます。
で、本題の設定ファイルの扱いについて
上記 rubykaigi の Gemfile を見ると 'configatron' っていうのがある。
なので、これが cool な設定ファイルを扱うライブラリなのかしら、と思ってたのですが、ちょうど id:ursm さんから
@sugamasao 最近は Settingslogic を使っています
2010-09-02 21:35:47 via Brizzly to @sugamasao
という助言を頂いたので、そっちで試してみることにしました。
Settingslogicの使い方
binarylogic/settingslogic · GitHub の README にある通りですが……
- Gemfile に 'settingslogic' を追加
- bundle update でインストール
設定ファイル用 model を作る
vim app/models/settings.rb
class Settings < Settingslogic source "#{Rails.root}/config/application.yml" namespace Rails.env end
ただ、グローバルな設定をしたいなら namepsace は無くても良い(詳しくは後述)。
設定ファイルを作成する
上記の source で指定しているパスと合わせる必要があるので注意。
vim config/application.yml
# app/config/application.yml defaults: &defaults cool: saweet: nested settings neat_setting: 24 awesome_setting: <%= "Did you know 5 + 5 = #{5 + 5}?" %> development: <<: *defaults neat_setting: 800 test: <<: *defaults production: <<: *defaults
こうすることで、 Rails の環境変数によって defaults + test などを変化させることができるので、環境によって値を変えたい場合でも特に余計な手順を踏むことなく、柔軟に対応できる。
また、 namespace で値を変える必要ないから不要だよ、という場合は namespace をコメントアウトして、 application.yml のほうで defaults などを記載せず直接 yml で値を書いてしまえば使える(逆に言えば、namespace を指定する場合は、対応する namespace の定義が無いと何も読み込んでくれない)。
読み出し方
app/controllers/hoge_controller.rb 内から普通に読み出すことができる。
logger.debug Settings.cool.saweet
#=> nested settings
Settingslogic 気に入った
model を挟むというのがユニークですけど、yml で書いたのが特に意識せずそのまま使えてラクで良いですね。これは良い!
enjoy!