Rails抜きのActiveRecordに再挑戦

いまいちRailsの機能とActiveRecordの機能を区別できてない。create_tableでテーブル定義して# rake migrateなんかしても、Ruby的には意味不明みたいだ。これはたぶんRailsの機能。たぶん。おそらく。

マイグレーションは面倒そうだったのでまずはO/Rマッピングから攻めていくことにする。

RubyからSQLite3を操作する

sudo apt-get install ruby1.8-dev sqlite3 とかsudo gem install sqlite3 active_recordしておく。準備が出来たらこう書く。

require "rubygems"
require "sqlite3"

sql=<<SQL
CREATE TABLE students (
    id INTEGER PRIMARY KEY NOT NULL UNIQUE,
    test_text TEXT NOT NULL DEFAULT 'default_text',
    aa INTEGER NOT NULL DEFAULT 0
);
SQL

SQLite3::Database.new("aa.db").execute(sql)
students
id
test_text
aa

これくらいならわかる。SQLite3クラス(モジュール?)が他に何を出来るかは知らないけど、newでデータベース選択、executeでSQL実行がわかればそれでいいや。

ちなみにSQLiteでINTEGERかつPRIMARY KEYなフィールドは、MySQLでいうauto_increment相当になる。

ActiveRecord先生によるCRUD

require "rubygems"
require 'active_record'

ActiveRecord::Base.establish_connection(
  :adapter => "sqlite3", :database => "aa.db"
)

class Student < ActiveRecord::Base
end

Student.create({:aa => rand(100)})

p Student.find(:all)
p Student.find(:all,:conditions => "aa < 100")
p Student.find(:all,:conditions => ["aa > ?",50])
p Student.find(:first)

s=Student.find(:first)
s.test_text = "huhahahahahah"
s.save

p Student.find(:first,:conditions => "test_text <> 'default_text'")

Student.find(:first).destroy

まず最初にestablish_connectionでDB接続。続いてStudentクラスの定義、というか、これはクラス名を使って裏側でいろいろな処理をしてくれるので空でいい。楽だ。

Student.createでINSERT相当。

Student.find(:all)でSELECT * FROM students相当。find(2)ならWHERE id=2が付く。ActiveRecorde::Base#find のAPIはだいたいこんな感じ。

Student.find(:first)の返り値のプロパティがそのままカラムになってるので、s.test_textに適当な値を入れてsaveするとUPDATE相当。

destroyでDELETE相当。

これでひととおりのCRUDはわかった。

ActiveRecord先生の華麗なるユーティリティ

with_scope

ファイルを改めて、ここ を参考にまずwith_scopeを試してみる。

require "rubygems"
require 'active_record'

ActiveRecord::Base.establish_connection(
  :adapter => "sqlite3", :database => "aa.db"
)

class Student < ActiveRecord::Base
end

p Student.find(:all)

Student.with_scope(:find => {:conditions => "id < 8"}){
    p Student.find(:all)
}

実行するとidが8未満のやつだけが列挙されるはず。

/var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:1532:in `method_missing’: protected method `with_scope’ called for Student(id: integer, test_text: text, aa: integer):Class (NoMethodError)

えーprotected? 仕様変更? まあ意図はわからんでもないので別にいいけど、コピペ学習が止まるのは困るので公式 からサンプルをあさる。

require "rubygems"
require 'active_record'

ActiveRecord::Base.establish_connection(
  :adapter => "sqlite3", :database => "aa.db"
)

class Student < ActiveRecord::Base
    def self.scope_test
        with_scope(:find => {:conditions => "id < 8"}) {
            find(:all)
        }
    end
end

p Student.find(:all)

p Student.scope_test

ん、OK。

公式のサンプルにもある:createがよくわからないので、勘で試してみる。

class Student < ActiveRecord::Base
    def self.scope_test
        with_scope(:find => {:conditions => "id < 8"}) {
            find(:all)
        }
    end

    def self.scope_test_create
        with_scope(
                :find => {:conditions => "id < 8"},
                :create => {:test_text => "scope test"}
            ) {
            create({:aa => 999})
        }
    end
end

p Student.find(:all)

p Student.scope_test
p Student.scope_test_create #<Student id: 15, test_text: "scope test", aa: 999>

なるほどなるほど。なんかインデントが気持ち悪くなったけど意味はわかった。そのスコープ内でcreateするときのデフォルト値みたいなもんだな。

続いてscopeのネスト。

    def self.scope_test_nest
        with_scope(:find => {:conditions => "id < 30"}){
            with_scope(:find => {:conditions => "aa < 40"}){
                find(:all)
            }
        }
    end

id < 30かつaa < 40なスコープが定義できたのを確認。

ActiveRecord先生のリレーショナルユーティリティ

「リレーショナル」と前出の「華麗なる」が地味に韻を踏んでいる点に注意。

リレーション(公式にはアソシエーションらしい)は要するにhas_manyとかbelongs_toとか、一時期よく見かけたやつですね。

どうでもいいけどbelongって単語を見ると未だにall your base are belong to us が頭に浮かぶ。いい加減記憶をアップデートしたい。

require "rubygems"
require 'active_record'

ActiveRecord::Base.establish_connection(
  :adapter => "sqlite3", :database => "aaa.db"
)

class Student < ActiveRecord::Base
    belongs_to :group
end

class Group < ActiveRecord::Base
    has_many :students
end

if !Student.find(:first) # => nil
    10.times{|n|
        Student.create({:aa => n, group_id => n})
        Group.create({:group_name => "group #{n}"})
    }
end

p Student.find(:all)

の前にまずgroupsテーブル作らないと。どうせならってことでaa.dbじゃなくてaaa.dbを新規作成する。

require "rubygems"
require "sqlite3"

sql=<<SQL
CREATE TABLE students (
    id INTEGER PRIMARY KEY NOT NULL UNIQUE,
    test_text TEXT NOT NULL DEFAULT 'default_text',
    group_id INTEGER ,
    aa INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE groups (
    id INTEGER PRIMARY KEY NOT NULL UNIQUE,
    group_name VARCHAR NOT NULL DEFAULT '',
    ggg INTEGER NOT NULL DEFAULT 0
);
SQL

SQLite3::Database.new("aaa.db").execute(sql)

students groups
id id
test_text group_name
group_id ggg
aa  

な感じ。groups LEFT OUTER JOIN students ON (students.group_id = groups.id)される予定。

んで先のコードを実行すると

/var/lib/gems/1.8/gems/activerecord-2.0.2/lib/active_record/connection_adapters/sqlite3_adapter.rb:29:in `table_structure’: Could not find table ‘groups’ (ActiveRecord::StatementInvalid)

なんでやねん。

groupsテーブルがないと言われて考えられることはgroupsテーブルがないってことなので、ちょっと考えてCREATE TABLEのほうをこうしてみた。

require "rubygems"
require "sqlite3"

sql=<<SQL
CREATE TABLE students (
    id INTEGER PRIMARY KEY NOT NULL UNIQUE,
    test_text TEXT NOT NULL DEFAULT 'default_text',
    group_id INTEGER ,
    aa INTEGER NOT NULL DEFAULT 0
);
SQL

SQLite3::Database.new("aaa.db").execute(sql)

sql=<<SQL
CREATE TABLE groups (
    id INTEGER PRIMARY KEY NOT NULL UNIQUE,
    group_name VARCHAR NOT NULL DEFAULT 'alone',
    ggg INTEGER NOT NULL DEFAULT 0
);
SQL

SQLite3::Database.new("aaa.db").execute(sql)

んで再度実行すると、きちんとStudent.find(:all)が動いた。「;」で区切って複数同時に実行しようとしてもダメみたい。よく考えたらDatabase.newなんだから当然か。

何やってたっけ。ああそうそうbelongs_toとかか。

これ を参考にいろいろ試してみることにする。

g=Group.find(1)
p g.students # => [#<Student id: 1, test_text: "default_text", group_id: 1, aa: 5>]
puts g.students.size # => 1

s=Student.create({:test_text => "tesuto"})
g.students << s

p g.students # => [#<Student id: 1, test_text: "default_text", group_id: 1, aa: 5>, #<Student id: 11, test_text: "tesuto", group_id: 1, aa: 0>]
puts g.students.size # => 2

g.students.create({:test_text => "tset2"})
puts g.students.size #=> 3

だいたいわかった。1対1も多対多も似たようなものだろう。

まとめ

SQLを書きたい子なのでO/Rマッピングには消極的なほうですが、これはたしかに便利だなあ。ていうか、あんなにひどいテーブル定義かいといてSQL書きたいもないな。

ruby
名前

ほか