2010年12月29日水曜日

基本的に...

「基本的に」とか、「原則として」が、会議の中で出てくる事が多いが、(経験を積んだエンジニアは良く知っている事ですが)放置しておくと間違いなく危ない場合が多い。

基本的に~であるという事は、裏を返せば基本的に~ではないケースが存在するという事。

例1) 原則的には、そこの関係は、1:1です!
答え)  最低でも、1:nで考えておかないと後で不味い事に。。ヒアリング必須

例2) 基本的に、その資料は担当者に返します。
答え) 担当者以外に誰が受け取るかヒアリングする

ITシステムでは、ロジックで処理をしなければいけない為、処理がMECEになっている必要があります。「基本的に」とかではぐらかしてきたら、逆に突っ込んで聞きましょう!

distinctってそんなに使うのか?

SQLのdistinctは使い所によっては、必要なキーワードだが、訳も分からず使われている事も多い。「膨らんだから書いとけ!」見たいな感じで。

若者のSQLを見ていると、同じ使い方が多いことに気づいた。例えば

foo: id, name
bar: id, foo_id, flag

というテーブル構成で、「barでflagがたっているfooの行を取得」が課題だった場合

select
  f.id, f.name
from
  foo f inner join bar b on b.foo_id = f.id
where
  b.flag = true

と書いてしまったが為に、最終的にselect句にdistinctをつけるはめに。。

これは「fooからデータを取得する際の抽出条件」と認識すれば

select
  f.id, f.name
from
  foo f
where
  exists (select 'x' from bar b where b.foo_id = f.id and b.flag = true)

と書けるはずで、distinct は不要だ。

selectは、縦*横の新しい表を取得する物で、「1行が意味する概念」は何かを、はっきり意識できていないからではと思う。

2010年12月28日火曜日

CSVというフォーマット

データ交換の検討で、「ここはCSVで」とかで会議終わらせちゃう人もいますが、アフォかと思います。

"CSV"だけでは、フォーマットがあるようでないのと同じです。細かい事が詰まってないので、下記は最低決めちゃいましょう。

  • 文字列エンコーディング(例 Windows-31J)
  • 改行コード (例 CRLF)
  • "の使い方。業務上EXCELで作業される方が多いので、私は下記で話する事が多いです。
    • セル内に , がある場合は、"で囲む
    • セル内に " がある場合は、"で囲み、"は""とエスケープする
    • セル内に改行がある場合は、"で囲む
上記の仕様ってEXCELで色々なパターンを入力し、CSVを出力後テキストエディタで開けば一目瞭然なんですが、割と知られてないようですね。。

rails2.3.5のマルチパート解析ではまった

概要
rails2.3.5、正確にはrack1.0.1のマルチパート解析処理に問題があり、データの欠落する可能性があります。

問題
「チェックボックスがおかしな状態になる」という問い合わせがあり調査開始しました。ほどなく"ごくまれ"にチェックボックスの入力状態が正しく反映されないケースがある事に気付きました。

切り分けの為、問題箇所を確認した所
  • パケットダンパーでデータを確認すると、socket通信上は想定値が確認できた。
  • 例外メール上の記録から、railsでのparamsには、おかしな値が入っている。
  • 上記メールから、rackより引き継いだ段階でも、おかしな値が入っている。
という事が判明しました。これで犯人は、apache か passenger か rack に絞られました。

そこで、Rack::Utils::Multipart.parse_multipartメソッドにログを仕込んで確認した所
  • content_lengthは想定どおりの値が確認できた。
  • io上入ってきた内容も想定どおり確認できた。
  • 処理ループが想定よりもかなり早い段階で抜けている。但し例外は発生していない。
と言う事がわかりました。
上手くいかない場合は、変数の動きをみていると、buf変数に一度読み込まれるだけで、後続のデータが処理されていません。

解決
buf変数に読み込んだデータを逐次処理していきますが、バウンダリ+EOL(\r\n)で丁度処理が終了した場合、buf.empty?が真となり、bufに読み込まれていないデータがあっても処理終了になっているようです。よって上記をrack1.1.xで修正されているように

break if (buf.empty? && $1 != EOL) || content_length == -1

とする必要があるようです($1 != EOLを追加)。
しかしrack1.1とrails2.3.5は相性が悪いみたいなので、モンキーパッチで対処する必要がありますね。

下記のような内容をconfig/initializers/rack_patch.rbとかで配置して下さい。

module Rack
  module Utils
    module Multipart
      def self.parse_multipart(env)
        unless env['CONTENT_TYPE'] =~
            %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
          nil
        else
          boundary = "--#{$1}"

          params = {}
          buf = ""
          content_length = env['CONTENT_LENGTH'].to_i
          input = env['rack.input']
          input.rewind

          boundary_size = Utils.bytesize(boundary) + EOL.size
          bufsize = 16384

          content_length -= boundary_size

          read_buffer = ''

          status = input.read(boundary_size, read_buffer)
          raise EOFError, "bad content body"  unless status == boundary + EOL

          rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n

          loop {
            head = nil
            body = ''
            filename = content_type = name = nil

            until head && buf =~ rx
              if !head && i = buf.index(EOL+EOL)
                head = buf.slice!(0, i+2) # First \r\n
                buf.slice!(0, 2)          # Second \r\n

                filename = head[/Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;\s]*))/ni, 1]
                content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
                name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]

                if content_type || filename
                  body = Tempfile.new("RackMultipart")
                  body.binmode  if body.respond_to?(:binmode)
                end

                next
              end

              # Save the read body part.
              if head && (boundary_size+4 < buf.size)
                body << buf.slice!(0, buf.size - (boundary_size+4))
              end

              c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
              raise EOFError, "bad content body"  if c.nil? || c.empty?
              buf << c
              content_length -= c.size
            end

            # Save the rest.
            if i = buf.index(rx)
              body << buf.slice!(0, i)
              buf.slice!(0, boundary_size+2)

              content_length = -1  if $1 == "--"
            end

            if filename == ""
              # filename is blank which means no file has been selected
              data = nil
            elsif filename
              body.rewind

              # Take the basename of the upload's original filename.
              # This handles the full Windows paths given by Internet Explorer
              # (and perhaps other broken user agents) without affecting
              # those which give the lone filename.
              filename =~ /^(?:.*[:\\\/])?(.*)/m
              filename = $1

              data = {:filename => filename, :type => content_type,
                      :name => name, :tempfile => body, :head => head}
            elsif !filename && content_type
              body.rewind
              
              # Generic multipart cases, not coming from a form
              data = {:type => content_type,
                      :name => name, :tempfile => body, :head => head}
            else
              data = body
            end

            Utils.normalize_params(params, name, data) unless data.nil?

            # break  if buf.empty? || content_length == -1
            break  if (buf.empty? && $1 != EOL) || content_length == -1
          }

          input.rewind

          params
        end
      end
    end
  end
end