投稿日 2010年9月28日 火曜日 カテゴリ Ruby on Rails 投稿者 sugimotoコメント(0) » 

sugimotoです。

Ruby on Rails にはいろいろ便利な機能がありますが、前から便利だけど微妙。。と思っているのが、date_select や datetime_select です。どちらも ActionView::Helpers::DateHelper のmethod で日付入力を簡単に作れますが、年月日のプルダウン入力はちょっと。。。という感じです。

ただ、この入力、Modelのdate,datetime なカラムにそのまま突っ込んで保存してしまえるのがとっても便利です。Post.published_at というカラムに

params = {
    :post => {
        :published_at(1i) => "2010",
        :published_at(2i) => "09",
        :published_at(3i) => "28",
        :published_at(4i) => "15",
        :published_at(5i) => "20"
     }
}

みたいな感じでcontrollerにパラメータが渡されますが、

post = Post.new(params[:post])

とするだけで、post.published_at = ’2010-09-28 15:20′ なレコードができてしまいます。

前置きが長くなりましたが、この仕組を調べてみました。 なお、今回例示したソースは公開時のtrunk を参照しています。

仕組みを調べてみる

まずはレコードを作成する場所、つまり、 ActiveRecord::Base::initialize を見てみます。

       def initialize(attributes = nil)
        @attributes = attributes_from_column_definition
        @attributes_cache = {}
        @new_record = true
        @readonly = false
        @destroyed = false
        @marked_for_destruction = false
        @previously_changed = {}
        @changed_attributes = {}

        ensure_proper_type

        if scope = self.class.send(:current_scoped_methods)
          create_with = scope.scope_for_create
          create_with.each { |att,value| self.send("#{att}=", value) } if create_with
        end
        self.attributes = attributes unless attributes.nil?

        result = yield self if block_given?
        _run_initialize_callbacks
        result
      end

どうやら、受渡されたattributes を self.attributes=(attributes) でいれているだけですね。 では self.attributes=(v) では何をしているんでしょうか。

      def attributes=(new_attributes, guard_protected_attributes = true)
        return unless new_attributes.is_a?(Hash)
        attributes = new_attributes.stringify_keys

        multi_parameter_attributes = []
        attributes = sanitize_for_mass_assignment(attributes) if guard_protected_attributes

        attributes.each do |k, v|
          if k.include?("(")
            multi_parameter_attributes < < [ k, v ]
          else
            respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
          end
        end

        assign_multiparameter_attributes(multi_parameter_attributes)
      end

"(" が付いた attribute を複数パラメータの属性として次のような配列を作成し、 assign_multiparameter_attributes に渡しています。

[
    ["published_at(1i)", "2010"],
    ["published_at(2i)", "09"],
    ["published_at(3i)", "28"],
    ["updated_at(1i)", "2010"],
    ["updated_at(2i)", "09"],
    ["updated_at(3i)", "28"],
    ["updated_at(4i)", "15"],
    ["updated_at(5i)", "10"],
    ["published_at(4i)", "15"],
    ["published_at(5i)", "10"],
]

では、次にassign_multiparameter_attributes を見てみます。

      def assign_multiparameter_attributes(pairs)
        execute_callstack_for_multiparameter_attributes(
          extract_callstack_for_multiparameter_attributes(pairs)
        )
      end

method名から察するに、一度パラメータを展開して、それを処理しているようです。展開のmethodはどんな処理でしょう。

      def extract_callstack_for_multiparameter_attributes(pairs)
        attributes = { }

        for pair in pairs
          multiparameter_name, value = pair
          attribute_name = multiparameter_name.split("(").first
          attributes[attribute_name] = [] unless attributes.include?(attribute_name)

          parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
          attributes[attribute_name] < < [ find_parameter_position(multiparameter_name), parameter_value ]
        end

        attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
      end

published_at(2i) のような名前から attributes["published_at"] という配列を作成して、(1i), (2i), (3i) などの値とセットで配列に積んでいっています。 さらに、名前でhashになった attributes を名前ごとにソートしています。 次のような hash が出来上がります。

ソート前 - 1つ目の要素が文字列のまま渡されているので、xxxx(11i) みたいなのがあると並び替えを間違える気がしますが、どうなんでしょうね。。。

attributes = {
    "publised_at" => [["1", 2007], ["3", 28], ["4", 15], ["5", 10], ["2", 9], ],
    "updated_at" => [["1", 2007], ["3", 28], ["4", 15], ["5", 10], ["2", 9], ],
}

ソート後 (1つ目の要素でソートされ、2つ目の要素で collect された)

attributes = {
    "publised_at" => [2007, 9, 28, 15, 10 ],
    "updated_at" => [2007, 9, 28, 15, 10 ],
}

type_cast_attribute_value, find_parameter_position の2つのメソッドは名前から何をしているかなんとなく想像がつきますが、一応見てみます。

      def type_cast_attribute_value(multiparameter_name, value)
        multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
      end

xxxx(1i) のような名前のパラメータを to_i でtype cast した値を返します。どうやら、xxxx(1f) のような名前にすると Float にcastするようです。

      def find_parameter_position(multiparameter_name)
        multiparameter_name.scan(/\(([0-9]*).*\)/).first.first
      end

xxxx(1i) のようなパラメータ名から数字を取り出し、パラメータの順序を返しています。

そろそろ先が見えてきた感じがします。(1i) に従って配列にされたパラメータの処理をする execute_callstack_for_multiparameter_attributes を見てみます。

      def execute_callstack_for_multiparameter_attributes(callstack)
        errors = []
        callstack.each do |name, values_with_empty_parameters|
          begin
            klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
            # in order to allow a date to be set without a year, we must keep the empty values.
            # Otherwise, we wouldn't be able to distinguish it from a date with an empty day.
            values = values_with_empty_parameters.reject { |v| v.nil? }

            if values.empty?
              send(name + "=", nil)
            else

              value = if Time == klass
                instantiate_time_object(name, values)
              elsif Date == klass
                begin
                  values = values_with_empty_parameters.collect do |v| v.nil? ? 1 : v end
                  Date.new(*values)
                rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
                  instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
                end
              else
                klass.new(*values)
              end

              send(name + "=", value)
            end
          rescue => ex
            errors < < AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
          end
        end
        unless errors.empty?
          raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
        end
      end

なかなか長いです。。前半の klass = (....).klass でカラムの型を取ってきて、Time であれば、attributes の配列を instantiate_time_object で Date であれば、Date.new でインスタンス化して、最後に send(name + "=", value) でインスタンス化した値をカラムにセットしています。 Date でも Timeでもなかった場合は、klass.new(*values) で強引にインスタンス化しようとしますね。強気です。

最後に

なぜ、datetime_select を調べたかというと、datetime なカラムの入力フォームを作成するときに日付と時間を別にして日付部分をdatepicker にできないかな、と考えていたからでした。

xxxxx(1i) 的なパラメータ名を設定して、上記処理を通ったあとに、Date.new できる配列になれば良いってことで、やっぱり、datetime_select が生成するようなhashを作らないといけないんだな。。って感じです。

投稿日 2010年9月17日 金曜日 カテゴリ サーバーインフラ 投稿者 sugimotoコメントは受け付けていません。 
wget -nd http://www.qmailtoaster.com/download/stable/daemontools-toaster-0.76-1.3.6.src.rpm
yum install rpm-build
rpmbuild --rebuild daemontools-toaster-0.76-1.3.6.src.rpm
rpm -Uvh /usr/src/redhat/RPMS/x86_64/daemontools-toaster-0.76-1.3.6.x86_64.rpm
wget -nd http://www.emaillab.org/djb/daemontools/svscan
mv svscan /etc/init.d/.
chmod 755 /etc/init.d/svscan
投稿日 2010年8月25日 水曜日 カテゴリ Wiki, ソフトウェア開発, ツール 投稿者 syojiコメントは受け付けていません。 

ciklone(サイクロン)のリリース前に、自分たちが実際にプロジェクトで利用して、 ciklone(サイクロン)を改善してきました。 サイクロン開発プロジェクトの最後の6ヶ月間にソフトウェアのライフサイクルをくり返します。すべての機能を使いながら、「BTS/SCM/プロジェクト管理に必要なことができ、効率的か」、を試験してきました。

開発しながら、実際の現場で利用することで、バグがどんどん見つかったし、使いにくい点、改善すべきことがよく見えます。自分たちが作ったシステムを自分たちのプロジェクトで使う最大のメリットだと思います。

よくいわれる「ドッグフードを食べる」ことをやったことで、BTSやバージョン管理に必要な機能と必要ではない機能を切り分けることができ、開発の現場に必要なツールとして、 ciklone(サイクロン)がリリースできました。

「ドッグフードを食べる」とは、 自社で作ったソフトウェアを実際の業務で利用して、評価することです。有名なのはマイクロソフトのドッグフードです。また、Google ケータイも同様です。

シリコンバレー101[Googleケータイ販売で、Googleは何を変えたいのか?]

Googleでは、常に新しい製品や技術を実験しており、短期間でフィードバックと提案を集めるために社員にテストを依頼している。われわれは、これをドッグフーディング(“eating your own dogfood”から)と呼んでいる。

この「ドッグフードを食べる」経験は、お客様からのフィードバックと同じくらい重要なモノになったと感じています。 ベータ版が完成した頃から、特定のお客様にもご利用頂きました。 ドッグフードを食べ、お客様からのフィードバック経験によって、ciklone(サイクロン)はチームにとって、すばらしいソフトウェアを開発するためのプラットフォームとして十分に機能しました。

オープングルーヴでは、ciklone(サイクロン)を最大限に活用して頂き、他社の利用方法を紹介することで、みなさんのチームのソフトウェア開発を効率化とコストダウンしていく支援をさせて頂きます。

オープングルーヴでは、Wikiサイトを使って、実証済みのガイドとベストプラクティス(と考えられる)を提案していきます。

Wiki.ciklone.com

Wiki.ciklone.com

投稿日 2010年6月30日 水曜日 カテゴリ CakePHP, PHP 投稿者 sugimotoコメント(0) » 

sugimoto です。最近、CakePHPを使ってます。

しばらくRails を使っていたこともあり、わかりやすい所もある反面、衝撃的に違うところがあったりして試行錯誤な毎日です。

先日、連結テーブルに「並び順」カラムを持たせて、hasManyAndBelongsToなリレーションの順番をつけようとしたところ、うまく動きませんでした。

プロジェクトとユーザーを以下のようなテーブルで関連つけていました
projects
users
projects_users

このとき、「各プロジェクトの参加者ごとに並び順を設定したい」という要望があり、 projects_users に position カラムを持たせて並び順を設定することにしました。

positionカラムの更新をするために ProjectsUser という中途半端なmodel クラスを作ったところ、問題発生。

projects_controller のuses 句で ProjectsUser を設定しているにもかかわらず、ProjectsUser の関数が呼べず、controller内での $this->ProjectsUser の実体がなぜか AppModel となっていました。

AppController など、ソースを確認したところ、controller のイニシャライズ時、以下のようなロジックでした。

  1. Projectモデルのインスタンス を作成
  2. Projectモデルで設定されているリレーションに該当するモデルのインスタンスを作成(Userなど)
  3. リレーションテーブルを扱うのインスタンス ProjectsUserを作成
  4. その際、デフォルト実装である AppModel のインスタンスとして作成
  5. 上記作成したインスタンスはすべてキャッシュ
  6. ProjectsUserを作成するが、すでにキャッシュされているためそれを使用

つまり、上記処理がcontroller の uses句内で宣言された順に処理されているため、ProjectsUserをロードするとき、先に処理したProjectモデルのリレーションとして作成されたProjectsUserが優先されていたわけです。

今日の格言

リレーショナルテーブルのモデルを作ったら、uses 句内では必ず先に宣言すること。

ソフトウェアエンジニアのためのバグトラッキングシステム : Ciklone

ソフトウェアエンジニアのためのバグトラッキングシステム

ソフトウェアエンジニアのためのバグトラッキングシステム

投稿日 2010年6月28日 月曜日 カテゴリ Ruby on Rails 投稿者 morimotoコメントは受け付けていません。 

どうもお久しぶりです。

morimotoです。

今日は複雑なActiveRecordの条件句を作成する際に(もしかしたら)楽になるのではないかと思いconditionを生成するクラスを作ってみました。

  class ARCond
    module OPERATION
      EQUAL            = '='
      NOT_EQUAL        = '<>'
      IN               = 'IN'
      GREATER          = '>'
      GREATER_OR_EQUAL = '>='
      LESS             = '< '
      LESS_OR_EQUAL    = '<='
    end

    attr_reader :conditions, :parameters, :separator, :table_name

    def initialize(separator="AND", table_name=nil)
      @conditions = []
      @parameters = []
      @separator  = separator
      @table_name = table_name
    end

    # column = ?
    def equal(column, value, table=nil)
      add(column, value, OPERATION::EQUAL, table)
    end
    # column <> ?
    def not_equal(column, value, table=nil)
      add(column, value, OPERATION::NOT_EQUAL, table)
    end
    # column IN (?,?,?...)
    def in(column, value, table=nil)
      add(column, value, OPERATION::IN, table)
    end
    # column > ?
    def greater(column, value, table=nil)
      add(column, value, OPERATION::GREATER, table)
    end
    # column >= ?
    def greater_or_equal(column, value, table=nil)
      add(column, value, OPERATION::GREATER_OR_EQUAL, table)
    end
    # column < ?
    def less(column, value, table=nil)
      add(column, value, OPERATION::LESS, table)
    end
    # column <= ?
    def less_or_equal(column, value, table=nil)
      add(column, value, OPERATION::LESS_OR_EQUAL, table)
    end

    def << (other)
      cond = nil
      param = []
      if other.kind_of? ARCond
        c = other.build
        cond = c.delete_at(0)
        param = c
      elsif other.kind_of? String
        cond = other
      elsif other.kind_of? Array
        cond = other.delete_at(0)
        param = other
      end
      unless cond.blank?
        @conditions << cond
        @parameters.concat(param)
      end
      return self.build
    end

    # [conditions, param, param, param,...]
    def build
      [join_conditions].concat(@parameters)
    end

    private
    def join_conditions
      sep = " #{@separator} "
      "(#{@conditions.join(sep)})"
    end

    def column_fullname(column, table=nil)
      unless column =~ /^[\w]+\.[\w]+$/
        if table_name = table.blank? ? @table_name : table
          return "#{table_name}.#{column}"
        end
      end
      column
    end

    def add(column, value, operation, table=nil)
      parts = [column_fullname(column, table), operation]
      param = [value].flatten
      parts << ((operation == OPERATION::IN) ? "(#{param.map{'?'}.join(',')})" : "?")
      self << [parts.join(' ')].concat(param)
    end
  end

使い方の例)

# 基本スタイル
c1 = ARCond.new("AND")
c1.equal("col1", true)
  # => ["(col1 = ?)", true]
c1.not_equal("col2", nil)
  # => ["(col1 = ? AND col2 <> ?)", true, nil]
c1.in("col3", [31,32,33])
  # => ["(col1 = ? AND col2 <> ? AND col3 IN (?,?,?))", true, nil, 31, 32, 33]



# 連結文字を変更する
c2 = ARCond.new("OR")

# 指定した連結文字で結合されていく
c2.greater("col4", 100)
  # => ["(col4 > ?)", 100]
c2.less("col4", 200)
  # => ["(col4 > ? OR col4 < ?)", 100, 200]

# 配列みたいに追加していく
c1 << c2
  # => ["(col1 = ? AND col2 <> ? AND col3 IN (?,?,?) AND (col4 > ? OR col4 < ?))", true, nil, 31, 32, 33, 100, 200]



# 引数なしはANDで連結
c3 = ARCond.new()

# いつもどおりの配列の conditions もくっつけれる
c3 << ["col5 = ? AND col6 IN (?,?)", 11,22,33]
  # => ["(col5 = ? AND col6 IN (?,?))", 11, 22, 33]

# 任意文字列もくっつけれる
c3 < < "col7 > col8"
  # => ["(col5 = ? AND col6 IN (?,?) AND col7 > col8)", 11, 22, 33]


# テーブル名を指定する
c4 = ARCond.new("OR", "sample")

# カラム名の頭にテーブル名が付与される
c4.greater_or_equal("col9", 300)
  # => ["(sample.col9 >= ?)", 300]
c4.less_or_equal("col9", 400)
  # => ["(sample.col9 >= ? OR sample.col9 < = ?)", 300, 400]

# くっつけるときにテーブル名を指定する
c4.equal("col10", false, "banana")
  # => ["(sample.col9 >= ? OR sample.col9 < = ? OR banana.col10 = ?)", 300, 400, false]

c3 << c4
  # => ["(col5 = ? AND col6 IN (?,?) AND col7 > col8 AND (sample.col9 >= ? OR sample.col9 < = ? OR banana.col10 = ?))", 11, 22, 33, 300, 400, false]

c1 << c3
  # => ["(col1 = ? AND col2 <> ? AND col3 IN (?,?,?) AND (col4 > ? OR col4 < ?) AND (col5 = ? AND col6 IN (?,?) AND col7 > col8 AND (sample.col9 >= ? OR sample.col9 < = ? OR banana.col10 = ?)))", true, nil, 31, 32, 33, 100, 200, 11, 22, 33, 300, 400, false]


# 最終的に使いたいタイミングで build してあげたらよい
c1.build
  # => ["(col1 = ? AND col2 <> ? AND col3 IN (?,?,?) AND (col4 > ? OR col4 < ?) AND (col5 = ? AND col6 IN (?,?) AND col7 > col8 AND (sample.col9 >= ? OR sample.col9 < = ? OR banana.col10 = ?)))", true, nil, 31, 32, 33, 100, 200, 11, 22, 33, 300, 400, false]

作っておいてなんですが、テーブルが複数あったり、集計クエリが絡んできたりと、複雑になればなるほど自分でwhere句を書いたほうが早い気がします。

以上、morimoto でした。

次ページへ »