createからsaveの流れ
ActiveRecord::Base#create
def create(attributes = nil, &block) if attributes.is_a?(Array) attributes.collect { |attr| create(attr, &block) } else object = new(attributes) yield(object) if block_given? object.save object end end
newして、blockがあればsaveをする。それだけ。
ActiveRecord::Base#create_or_update
def create_or_update raise ReadOnlyRecord if readonly? result = new_record? ? create : update result != false end
new_recordかどうか調べて create/update を呼ぶ
次は、create.
ActiveRecord::Base#create
def create if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name) self.id = connection.next_sequence_value(self.class.sequence_name) end quoted_attributes = attributes_with_quotes statement = if quoted_attributes.empty? connection.empty_insert_statement(self.class.table_name) else "INSERT INTO #{self.class.quoted_table_name} " + "(#{quoted_column_names.join(', ')}) " + "VALUES(#{quoted_attributes.values.join(', ')})" end self.id = connection.insert(statement, "#{self.class.name} Create", self.class.primary_key, self.id, self.class.sequence_name) @new_record = false id end
- idを(シーケンスなどから)先にとるタイプのDBだったら、idを先にとる
- DBからカラムの情報を取ってくる(attributes_with_quotes)
- カラムの情報を取得できなかった場合
- empty_insert_statementで"INSERT INTO #{quote_table_name} VALUES(DEFAULT)"を返す(ActiveRecord::ConnectionAdapters::DatabaseStatements)
- カラムの情報を取得できた場合
- カラムの情報を使って、INSERTクエリを作成
attributes_with_quotesのコードはこんなかんじ
ActiveRecord::Base#attributes_with_quotes
def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) quoted = {} connection = self.class.connection attribute_names.each do |name| if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) value = read_attribute(name) # We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML. if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time)) value = value.to_yaml end quoted[name] = connection.quote(value, column) end end
- read_attributeからカラムの情報を取得
- connection.quote() を使って、valueをquoteする。
read_attributeの処理内容が分からないので、コードをみると、
ActiveRecord::AttributeMethods#read_attribute
def read_attribute(attr_name) attr_name = attr_name.to_s if !(value = @attributes[attr_name]).nil? if column = column_for_attribute(attr_name) if unserializable_attribute?(attr_name, column) unserialize_attribute(attr_name) else column.type_cast(value) end else value end else nil end end
column_for_attributeを使ってカラムの情報をとってくるのかな。
ActiveRecord::Base::#column_for_attribute
def column_for_attribute(name) self.class.columns_hash[name.to_s] end def columns_hash @columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash } end def columns unless defined?(@columns) && @columns @columns = connection.columns(table_name, "#{name} Columns") @columns.each { |column| column.primary = column.name == primary_key } end @columns end
base.rbいったり、attribute_methods.rbに行ったり忙しい。
- column_for_attribute -> columns_hash -> columnsに行って、最終的にはconnectionのcolumnsを呼んでデータベースからカラム情報を取得。
- @column_hashはキャッシュ
ActiveRecord::ConnectionAdapters::MysqlAdapter
AbstractAdapterになかったので、MySQL Adapterを見てみたら、columnsがあった。
def columns(table_name, name = nil)#:nodoc: sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" columns = [] result = execute(sql, name) result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } result.free columns end
ふむ、やっぱり、DBからカラム情報をとってきている。
もし、他のConnectionAdapterを作なら、ここらへんを変更すれば良さそうだなぁ。
結果として、create時にはDBのカラム情報を取得して、quoteされたvalueを使ってINSERT文を組み立てる。
ActiveRecord::Base#create_or_update
def create_or_update raise ReadOnlyRecord if readonly? result = new_record? ? create : update result != false end
new_recordかどうか調べて create/update を呼ぶ
一旦戻って、update method
ActiveRecord::Base#update
quoted_attributes = attributes_with_quotes(false, false, attribute_names) return 0 if quoted_attributes.empty? connection.update( "UPDATE #{self.class.quoted_table_name} " + "SET #{quoted_comma_pair_list(connection, quoted_attributes)} " + "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}", "#{self.class.name} Update" ) end
これもまた単純だな
- attributes_with_quotesはさっき調べたので、軽くスルー
- UPDATE ... の中のそれぞれのメソッドを調べていきます。
- quoted_table_name
- quoted_comma_pair_list
- quote_column_name
ActiveRecord::ConnectionAdapters::Quoting#quote_table_name
def quote_table_name(table_name) quote_column_name(table_name) end
これも、quote_column_nameを呼んでいるのか。
quoted_comma_pair_listより先にquote_column_nameを調べてみよう。
ActiveRecord::ConnectionAdapters::Quoting#quote_column_name
def quote_column_name(column_name) column_name end
あれ、何もしていない。MysqlAdapterはどうなってるんだろう。
ActiveRecord::ConnectionAdapters::MysqlAdapter#quote_column_name
def quote_column_name(name) #:nodoc: @quoted_column_names[name] ||= "`#{name}`" end
ああ、あった。ちゃんと``でquoteしている。
@quoted_column_namesはキャッシュだね。
そして、quoted_comma_pair_listは
ActiveRecord::Base#quoted_comma_pair_list
comma_pair_list(quote_columns(quoter, hash))
ありゃ、これもcomma_pair_list(quote_columns(...))を呼び出しているだけだ。
ActiveRecord::Base#quite_columns
def quote_columns(quoter, hash) hash.inject({}) do |quoted, (name, value)| quoted[quoter.quote_column_name(name)] = value quoted end end
Hashを作って、quoteされたカラム名をキーにして値を入れる。
一度戻って
ActiveRecord::Base#quoted_comma_pair_list
comma_pair_list(quote_columns(quoter, hash))
quite_columnsで返したハッシュをcomma_pair_listに渡す。
def comma_pair_list(hash) hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ") end
あれ?pair.lastはquoteしなくていいの?
と、思ったけど、updateで取得する時に、attributes_with_quotesを呼んでいるから、すでにquoteされているんだな。
def update(attribute_names = @attributes.keys) quoted_attributes = attributes_with_quotes(false, false, attribute_names) return 0 if quoted_attributes.empty? connection.update( "UPDATE #{self.class.quoted_table_name} " + "SET #{quoted_comma_pair_list(connection, quoted_attributes)} " + "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}", "#{self.class.name} Update" ) end
それで、作成したクエリを connection.update() に渡す、と。
とりあえず、下記のメソッド3つ終了。
- create
- save
- find