ActiveRecordまとめ2

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#save
      def save
        create_or_update
      end

短いな。

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
  1. idを(シーケンスなどから)先にとるタイプのDBだったら、idを先にとる
  2. DBからカラムの情報を取ってくる(attributes_with_quotes)
  3. カラムの情報を取得できなかった場合
    • empty_insert_statementで"INSERT INTO #{quote_table_name} VALUES(DEFAULT)"を返す(ActiveRecord::ConnectionAdapters::DatabaseStatements)
  4. カラムの情報を取得できた場合
    • カラムの情報を使って、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つ終了。

  1. create
  2. save
  3. find