モデルの設計が終わったら、実際にモデルクラスを記述します。STI に絡む部分は今までと若干異なったので、メモをしておきます。
モデルクラスおよびテーブルは Generator を使うことで、自動的に構築されます。
r g model device type:integer keyword:string device_type:integer j_title:string e_title:string date:date minutes:integer seconds:integer sort_order:integer number:string jcomment:string ecomment:string year_id:integer singer_id:integer
この結果、migrate スクリプトが作成されます。検索高速化のために、必要に応じてインデックスを作成しておきます。サブクラスでの検索があるので type と sort_order の組でのインデックスを、履歴用に year_id と sort_order でのインデックスを作成しています。Rails 3.1 までは、up migration と down migration の二つのメソッドを書く必要がありましたが、3.2 からは Change 一つだけでよくなり、記述量が半減しています。
class CreateDevices < ActiveRecord::Migration def change create_table :devices do |t| t.string :type t.string :keyword t.integer :device_type t.string :j_title t.string :e_title t.date :date t.integer :minutes t.integer :seconds t.integer :sort_order t.string :number t.string :jcomment t.string :ecomment t.integer :year_id t.integer :singer_id t.timestamps end add_index :devices, :keyword add_index :devices, [:type, :sort_order] add_index :devices, [:year_id, :sort_order] end end
migration して、テストテーブルも準備をしておきます。
rake db:migrate
rake db:test:prepare
Device 自体は抽象クラスのようなもので、積極的に使うことはほとんどないため、仕様(テスト)の記述はサブクラス側で行います。今回は例としてアルバムの仕様(rspec)を示します。spec/models/album_spec.rb は以下のようになります。create_delete_check は、昨年度ここに書いた日記("model 関連テストの手抜き法")に記述したものです。今回は削除を行わないのですが、念のためテストをしています。
# vim:set fileencoding=utf-8 filetype=ruby: require 'spec_helper' describe Album do fixtures :years, :individuals before :each do @relations = { Year:years(:year_1987), Individual:individuals(:individual_chisato_moritaka) } end create_delete_check Album, attributes:{ device_type:0, j_title:'def', keyword:'ghi', minutes:1, number:'1st', seconds:2, sort_order:3 }, check_keys:[ :device_type, :keyword, :sort_order ], relation_strings:[ :Year, :Individual ], method_name: :albums describe "が存在するとき" do before :each do @asn = Album::AlbumStr2Num @album = Album.create(keyword:'akey1', j_title:'アルバム1', e_title:'Album1', sort_order:101, minutes:54, seconds:32, device_type:@asn[:Album], jcomment:'コメント1', ecomment:'comment1') @single = Single.create(keyword:'skey', j_title:'シングル1', e_title:'Single1', sort_order:102, minutes:12, seconds:34, device_type:Single::SingleStr2Num[:Single], jcomment:'コメント2', ecomment:'comment2') @album2 = Album.create(keyword:'akey2', j_title:'Album2', sort_order:103, minutes:43, seconds:21, device_type:@asn[:LyricAlbum] | @asn[:PartAlbum], jcomment:'コメント3', ecomment:'comment3') end it "e_title が存在するとき,etitle に e_title を返すこと" do @album.jtitle.should == 'アルバム1' @album.etitle.should == 'Album1' end it "e_title が存在しないとき,etitle に j_title を返すこと" do @album2.jtitle.should == 'Album2' @album2.etitle.should == 'Album2' end it "comment でコメントが取得できること" do @album.comment(true).should == 'コメント1' @album.comment(false).should == 'comment1' @album2.comment(true).should == 'コメント3' @album2.comment(false).should == 'comment3' end it "whole_time で演奏時間が取得できること" do @album.whole_time(true).should == '54分32秒' @album.whole_time(false).should == '54\'32"' @album2.whole_time(true).should == '43分21秒' @album2.whole_time(false).should == '43\'21"' end it "device_type_str にてアルバムの種別が表示できること" do I18n.locale = :ja; @album.device_type_str.should == "アルバム" I18n.locale = :en; @album.device_type_str.should == "Album" I18n.locale = :ja; @album2.device_type_str.should == 'その他のアルバム (作詞した曲が収録), その他のアルバム (演奏に参加した曲が収録)' I18n.locale = :en; @album2.device_type_str.should == 'Album that contains her songs(Lyric), Album that she took part in playing songs' I18n.locale = :ja end it "has_device_type? にてアルバムの種別が判別できること" do @album.has_device_type?(@asn[:Album]).should be_true @album.has_device_type?(@asn[:LyricAlbum]).should be_false @album.has_device_type?(@asn[:PartAlbum]).should be_false @album2.has_device_type?(@asn[:Album]).should be_false @album2.has_device_type?(@asn[:LyricAlbum]).should be_true @album2.has_device_type?(@asn[:PartAlbum]).should be_true end it "event_str が取得できること" do I18n.locale = :ja; @album.event_str.should == "アルバム発売" I18n.locale = :en; @album.event_str.should == "Album release" I18n.locale = :ja end it "device_type_hash にて device_type ごとの hash_array が取得できること" do Album.should_receive(:order_sort_order).and_return([@album, @album2]) dth = Album.device_type_hash dth[:Album].should == [ @album ] dth[:LyricAlbum].should == [ @album2 ] dth[:PartAlbum].should == [ @album2 ] end it "next_device にて次のアルバムが取得できること" do @album.next_device.should == @album2 end it "prev_device にて前のアルバムが取得できること" do @album2.prev_device.should == @album end end end
これらのテストを通すために、app/model/device.rb にメソッドを記述していきます。プログラムにはコメントを書いていないのですが,説明用に追記しておきます。
# vim:set fileencoding=utf-8 filetype=ruby: class Device < ActiveRecord::Base include MiscMethod # Title module を Mix-in しているので、jtitle, etitle, jcomment, ecomment は実装済み。 include Title # Rails3.2 から外部からアクセスしてよい属性を制御できるようになりました。 attr_accessible :type, :device_type, :date, :e_title, :j_title, :keyword, :minutes, :number, :seconds, :sort_order, :jcomment, :ecomment # Rails3.2 からは新しい validates という表記もあるのですが、こっちの方がまとめて設定できるので未だに使っています。 validates_presence_of :device_type, :keyword, :sort_order, message:"が空の状態で保存することはできません" # 所属側の関連。通常は関連名だけですが、singer はクラス名が異なるので記述が複雑になります。 belongs_to :year belongs_to :singer, class_name: :Individual, foreign_key: :singer_id # 対他関連。 has_many :media has_many :song_lists has_many :concert_halls # Arel 用の scope。 # Rails 3.0 から実装された Arel によって、SQL がメソッドチェーンで書けます。 # SQL の発行は実際に配列の中身が求められたとき(each や map 実行時)に遅延実行されます。 # このおかげでデータベースの検索が非常に楽に書けるようになりました。 scope :keyword_is, -> k { where 'devices.keyword' => k } scope :year_is, -> y { where 'devices.year_id' => y.id } scope :order_device_type, -> { order 'devices.device_type' } scope :order_sort_order, -> { order 'devices.sort_order' } scope :order_sort_order_desc, -> { order 'devices.sort_order desc' } scope :device_type_is, -> dt { where 'devices.device_type' => dt } scope :type_is, -> t { where 'devices.type' => t } scope :before_devices, -> d { where 'devices.sort_order < ?', d.sort_order } scope :after_devices, -> d { where 'devices.sort_order > ?', d.sort_order } DeviceTypes = [ :Single, :Album, :Video, :Youtube ] # 日付を取得 def whole_time(is_ja) if minutes && seconds is_ja ? "#{minutes}分#{seconds}秒" : "#{minutes}'#{seconds}\"" end end # デバイスタイプ文字列を取得 def device_type_str_from_table(table) ans = [] table.keys.sort.each do |k| ans << I18n.t(table[k]) if has_device_type?(k) end ans.join(', ') end # 該当するデバイスタイプが存在するか def has_device_type?(v) device_type & v != 0 end # デバイスタイプごとの hash_array を取得 def self.device_type_hash_from_table(table) ans = Hash.new self.order_sort_order.each do |d| table.each_pair do |k, v| array = (ans[v] ||= []) array << d if d.has_device_type?(k) end end ans end # 次のアルバムを取得 def next_device self.class.after_devices(self).order_sort_order.first end # 前のアルバムを取得 def prev_device self.class.before_devices(self).order_sort_order_desc.first end end
Device でほとんどのメソッドが記述できるので、サブクラスの Album ではアルバムに依存する定数やメソッドのみを実装すればよくなります。
# vim:set fileencoding=utf-8 filetype=ruby: class Album < Device include MiscMethod AlbumStr2Num = { Album:1, MiniAlbum:2, KaraokeCD:4, LyricAlbum:8, MusicAlbum:16, CoverAlbum:32, PartAlbum:64, CompilationAlbum:128 } AlbumNum2str = AlbumStr2Num.invert # get_parameter def self.gp(main_key = false) [self, main_key ? :id : :album_id ] end def device_type_str device_type_str_from_table(AlbumNum2str) end def event_str I18n.t :album_release end def self.device_type_hash device_type_hash_from_table(AlbumNum2str) end end
メソッドが正しく実装されると、rspec はオールグリーンになります。
Album を作成するとき 正しいアトリビュートに対して作成が成功すること device_type 属性が設定されていない場合にバリデーションに失敗すること keyword 属性が設定されていない場合にバリデーションに失敗すること sort_order 属性が設定されていない場合にバリデーションに失敗すること Year に所属するオブジェクトが一つ増えること Individual に所属するオブジェクトが一つ増えること を削除するとき Year に所属するオブジェクトが一つ減ること Individual に所属するオブジェクトが一つ減ること が存在するとき e_title が存在するとき,etitle に e_title を返すこと e_title が存在しないとき,etitle に j_title を返すこと comment でコメントが取得できること whole_time で演奏時間が取得できること device_type_str にてアルバムの種別が表示できること has_device_type? にてアルバムの種別が判別できること event_str が取得できること device_type_hash にて device_type ごとの hash_array が取得できること next_device にて次のアルバムが取得できること prev_device にて前のアルバムが取得できること Finished in 0.32582 seconds 18 examples, 0 failures Done.