2011年7月31日日曜日

(小ネタ)システムコントロールテーブルは必ず用意しましょう

システムを構築すると、必ず「設定値」が必要になります。

・1ページの表示行数
・XXした際の遷移先URL
・YY通信のタイムアウト時間
...etc

上記設定をどこに置くかは結構大きな問題です。

(例)
①propertiesファイル (javaの場合)
②xxx.rbファイル (rubyの場合)
③データベース
...

上記①や②は最速ですが、設定値の反映には「アプリケーションの再デプロイ or 再起動」が必要になります。よって気軽に変更値の変更という訳にはいきません。

③はデータベースへの問い合わせが必要ですが、アプリケーション実行時に設定値を切り替えられるという利点があります。この手の設定値テーブルを用意しておくと顧客との打合せ時に、無駄な値決めに時間を浪費する事を回避できるというメリットもあります(顧客は情報がそろってから意思決定できるようになるので)。

よって、システムを開発してusersテーブルを作成した次には(?)、上記テーブルを作成しましょう。(心の)師匠、渡辺幸三さんに敬意を評し、「システムコントロール」テーブルと名づけましょう。

system_controls
----
code (PK or UK)
value (設定値)
editable (ユーザが画面上より編集可能か?)
description (編集画面での説明文章)
type (文字列、数字、boolean等入力タイプの区分値。多くなるならドメインテーブル作成。入力チェックと絡めるのも良いかもですね)
updater_id (最終更新者)
updated_at (最終更新日時)

こんな感じかな?

スピードが気になる人は、上記のcode値を利用して、KVSとかキャッシュサーバに突っ込めば良いでしょう。
# 変更、破棄タイミングに気をつけて下さい

ちなみに上記目的のテーブルを横持ち(カラム)で持ちたがる人がいますが、全力で阻止しましょう。柔軟にcodeを追加出来るのがミソなので!!

後DB障害発生時に問い合わせられない事も意識する必要があります(例 例外メール送信先)ので注意が必要です。

2011年7月28日木曜日

backbone.jsがいつのまにかpjax対応していた

■概要

年初にbackbone.jsの調査をしていた頃は、ajaxで画面遷移を行うには、fragmentを利用する事しかできませんでした。

しかし0.5以降では、Backbone.Routerを利用する事によって、pjax(HTML5のpushStateを利用した話題のあれ)な画面遷移が出来るになっています。

pjaxについては、こちらをご参照ください。
またgithubのファイルブラウザはpjaxの良例として有名なのでチェックしてみて下さい。


■何処がpjaxなのか?

まずは下記手順を実施後、http://localhost:3000/を閲覧してみましょう。chrome等html5対応ブラウザで確認して下さい。


次にhelpをクリックしてみましょう。

注目すべきなのは、
①URLが変わっている
②画面下部のレンダリング日時が変わっていない。

次にaboutをクリックしてみましょう。helpの時と同じです。

ブラウザの「戻る」ボタンをクリックしてみましょう。
About => Help => Indexに遷移します。「進む」ボタンもいい感じに遷移します。

次にHelpが画面に表示されている状態で、「F5」を押下してみましょう。
レンダリング日時が変わったはずです。


■説明

ajaxは非同期に(高速に)画面の一部を書き換える技術ですが、ユーザーの利便性の観点から、ブラウザの戻る、進むを利用可能にする必要があります。これまでのajaxでは、#(フラグメント)を利用して実現する事が多かったのですが、あまり良い解決策では有りませんでした。

pjaxでは、HTML5のpushStateを利用する事により、ブラウザのURLを切り替えつつ、画面の一部のみ切り替える事ができます。無論、URLは通常のURLですので、「通常のHTTPリクエスト」として処理するように実装する事が可能です。下記のIndexControllerでは、Ajax通信かどうかでレイアウトの適用を変更しています。
これができたら何が嬉しいのか?!それは読者への宿題にしておきます(^.^;

ちなみに、IE(≒pushStateをサポートしていないブラウザ)でみたら fragment での遷移に自動的に切り替わってました。。

■体験手順

・まずは前回の記事を参考にプロジェクトを生成する。

$ ./gen.sh pjax

・移動して体験に必要なファイルを用意する

$ cd pjax

app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>pjax2</title>
    <meta charset="utf-8"/>
    <%= stylesheet_link_tag :all %>
    <%= javascript_include_tag :defaults %>
    <%= javascript_include_tag :backbone %>
    <script><%= yield :javascripts %></script>
    <%= csrf_meta_tag %>
  </head>
  <body>
    <%= content_for?(:content) ? yield(:content) : yield %>
  </body>
</html>

app/views/layouts/index.html.erb

<%= content_for :javascripts do %>
$(function() {
  var Workspace = Backbone.Router.extend({
    routes: {
      "":      "index",
      "help":  "help",
      "about": "about"
    },
    index: function() {
      $("#app").load("/");
    },
    help: function() {
      $("#app").load("/help");
    },
    about: function() {
      $("#app").load("/about");
    }
  });
  var ws = new Workspace();
  Backbone.history.start({pushState: true});

  $("#indexButton").click(function() {
    ws.navigate("", true);
  });
  $("#helpButton").click(function() {
    ws.navigate("help", true);
  });
  $("#aboutButton").click(function() {
    ws.navigate("about", true);
  });
});
<% end %>

<%= content_for :content do %>
  <div id="app"></div>
  <hr/>
  <input type="button" name="index" value="index" id="indexButton" />
  <input type="button" name="help"  value="help"  id="helpButton" />
  <input type="button" name="about" value="about" id="aboutButton" />
  最終レンダリング日時:<%= I18n.l Time.now %>
<% end %>

<%= render :file => "layouts/application" %>

app/views/index/index.html.erb

<h1>Index</h1>

app/views/index/help.html.erb

<h1>help</h1>

app/views/index/about.html.erb

<h1>about</h1>

app/controllers/index_controller.rb

class IndexController < ApplicationController
  layout "index"
  def index
    render :layout => !request.xhr?
  end
  def help
    render :layout => !request.xhr?
  end
  def about
    render :layout => !request.xhr?
  end
end

config/routes.rb

最後に
root :to      => "index#index"
match "help"  => "index#help"
match "about" => "index#about"

public/index.html <= 削除

サーバ起動!
$ rails server

backbone.jsをrails3で実行できるようにするシェルスクリプト

※この記事はrails3.0に対する記事です。rails3.1はこちらをご覧下さい。

何度も同じ事しているので貼っておきます。
./gen.sh アプリ名
で利用できます(ノーエラーハンドリング上等!)

●2011/08/15追記 rcov => simplecovに変更し、ci_reporterを追加しました。

$ cat gen.sh

railsでネストしたレイアウトを実現する

railsのviewはレイアウト機能があり、画面の共通化が可能です。

・画面のイメージ・レイアウトを共通化する
・メニューを表示する
...etc

非常に便利な機能ですが、「サイト全体で画面のイメージを共通化しつつ、XXX機能でも共通のレイアウトにする」事が簡単にはできないと思っていました(レイアウトの二重適用)。
ところがRuby on Rails Guidesに実現方法がのっていました。非常に簡単なのでご紹介しておきます。

■application.html.erb (全体的な共通レイアウト)

<html>
<head>
  <title><%= @page_title or 'Page Title' %></title>
  <%= stylesheet_link_tag 'layout' %>
  <style type="text/css"><%= yield :stylesheets %></style>
</head>
<body>
  <div id="top_menu">Top menu items here</div>
  <div id="menu">Menu items here</div>
  <div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div>
</body>
</html>
上記のポイントは二つ。

・yield :stylesheets
=> 機能側で定義したstylesheetをheadタグ内にレンダリングする例です
・content_for?(:content) ? yield(:content) : yield
=> 機能側で:contentがある場合は引数にしてyield、無い場合通常のyield

■news.html.erb(機能毎レイアウト)

<% content_for :stylesheets do %>
  #top_menu {display: none}
  #right_menu {float: right; background-color: yellow; color: black}
<% end %>
<% content_for :content do %>
  <div id="right_menu">Right menu items here</div>
  <%= yield(:news_content) or yield %>
<% end %>
<%= render :file => 'layouts/application' %>
・content_for :stylesheets
=>上記のstylesheetを準備します。同じようにjavascriptも定義可能です。
・content_for :content / yield(:news_content) or yield
=> :contentを定義し、更に:news_contentがある場合は引数にしてyield、無い場合通常のyield
・render :file => 'layouts/application'
=> 上記contentを準備して、全体レイアウトをレンダリング

■xxx.html.erb(各画面)

<h1>タイトル</h1>
...

各画面固有のレンダリング内容

■xxx_controller.rb

class XxxController < ApplicationController
   layout "news"
   def xxx
   end
end

layoutで機能毎共通レイアウトを指定
上記の様に設定すると
application.html.erb
  news.html.erb
    xxx.html.erb

とネストしたレンダリング可能です!

■備考

2011/07/21 22:06 なんてこったい! Rails3レシピブックにのっているじゃないか(^_^;)

2011年7月27日水曜日

coberturaが猛烈に遅い!...けど解決した

■概要

junitの網羅率測定する為に、coberturaを利用する事が多いと思います。

# 当方久しぶりにjavaを触ったので最近のトレンドにはとんと疎くなりました。もっと良いのがあれば教えてください。

しかしこれが猛烈に遅い!軽快に進めたいのですが、ネックになりそうなので調査しました。

■解説

coberturaの実行はantから行っています。通常

1. コンパイル
2. instrument (coberturaを埋め込む)
3. junit実行


の順番で実行する事になりますが、 3. が非常に遅いのです。

3. の実行は
<junit fork="yes" dir="${basedir}">
  <classpath location="${instrumented.dir}" />
  <classpath location="${classes.dir}" />
  <classpath refid="cobertura.classpath" />
  <formatter type="xml" />
  <test name="${testcase}" todir="${reports.xml.dir}" if="testcase" />
  <batchtest todir="${reports.xml.dir}" unless="testcase">
    <fileset dir="${src.dir}">
      <include name="**/*Test.java" />
    </fileset>
  </batchtest>
</junit>

の様な記載を行うと思いますが、上記だと遅くなってしまいます。

何故なら、antがjunitを起動する際にプロセスのforkを行うのですが、forkはTestCase毎に行われるようです。
複数回のforkを通して網羅率の測定を行う必要がある為、「cobertura.ser」というファイルを
かいして測定を行います。

ところがforkする毎にcobertura.serに、シリアライズ・デシリアライズを行うためのその
オーバーヘッドが大きくなります。実際topコマンド確認するとCPUバウンドな処理である事が分かります。


そこでant 1.6.2より導入された、forkmodeを設定します。

<junit fork="yes" forkmode="once" dir="${basedir}">
....

上記の設定により、junitを実行するプロセスは"一度だけ"起動される為、シリアライズ・デシリアライズのオーバーヘッドが無くなり、非常に高速にテストの実行が可能です。

2011年7月21日木曜日

(小ネタ)DBにはトランザクションがありますが

今回は超初心者向け

webでシステムを構築していると、「データ登録をしてメール送信」とか「データ登録してファイル削除」等の処理が必要になる場合があります。そこで

transaction do
  yyy処理      ①
  delete_files ②
  db登録処理   ③
  commit
end

とか

transaction do
  xxx処理    ①
  sendmail   ②
  db更新処理 ③
  commit
end

の様な処理をすると問題大有りです。何故でしょう?

...

データベース処理は通常ロールバックが可能ですが、「ファイルシステムへの変更」や、「送信してしまったメール」は元に戻す事ができません。

# ①②が正常に処理された後に、③で例外が発生した場合取り返しが付かない

よって上記の巻き戻し不能処理をする場合は、できるだけ処理の一番最後にもってくるべきです。例えば:
transaction do
  xxx処理
  db更新処理
  commit
end

sendmail

(小ネタ)IEの512byte問題。圧縮転送したらさらに問題

■概要

通常HTTPリクエストに対して成功した場合は、HTTPレスポンスコード200で返答します。しかし「ページが見つからない場合」は通常404で返します。

しかしカスタマイズした404.html等を準備し、レスポンスコード404で返しても、IEは下記のような残念な画面を表示される事があります。



準備したhtmlを表示したいので何とかする必要があります。

■説明

IEには特別な仕様があり、「HTTPエラーメッセージを簡易表示する(デフォルトはオン)」というオプションの機能により「512byte」以下のContent-Lengthで返信した場合は文字通り「簡易表示」してしまいます。

またこの仕様はたちの悪い事に「圧縮転送後のサイズが512byte」になります。よってgzip転送を行うとそれなりのサイズで返信しても「簡易表示」される可能性があります。

よって
  • 無駄な内容を追加して圧縮後も512byteを超えるようにする
  • 圧縮転送設定を上記の場合はオフにする
のような対処がいります。

■参考

http://neta.ywcafe.net/000558.html

2011年7月12日火曜日

(小ネタ)saとかsysとかdbaとかadministratorとかrootとか本番環境で使ってはダメ

分かっちゃいるけど表題の様なユーザ・ロールを、OS/DBレベルで利用しているケースをみます。
上記のユーザを利用してしまうと

・セキュリティ上潜在的に問題がある
・権限上強力すぎて、通常ユーザに許可されていない行為に気づかない(例 ファイルの書き込み)
・(oracleの)dbaを利用すると「exportされたdumpは、dbaロールを持った人でしかimportできない」という十字架を背負う
・見る人が見たら、考察が甘い事がばれる => 信用を失う
...etc

色々簡単にすましたいのは分かりますが、「正しく理解しないと後で後悔する」ので、一般ユーザ・ロールを利用してあるべき姿で設計しましょう!

(小ネタ)webアプリケーションからの時間取得について

webアプリケーションで、現在日時を取得する場面は多いと思います。

その際注意しなければいけないのは、「どこから時間を取得するか?」という事です。

通常考えられる時間の取得場所は

・DBサーバ(sql文からsysdate等の関数を利用して)
・APサーバ(プログラム開発言語からOS時間を取得)

になると思いますが、アプリケーション側からの一貫した時間取得方法を明確にしておく事は大きな意味があります。

1. インフラレベルで時間が同期できていなかったら?という観点から時間を取得する場所は一定にするべき

無論ntpdで同期しておけば...という話がありますが、意外と合っていない場合があります。

2. テストの観点から「日時を固定、もしくは作為的にある日に設定」をしなければいけない場合がある

業務上の特定日(例 締め日)に固定し、テストしたい事は良くあります。
その場合も日時の取得方法を一貫性のある設計にしておけば、簡単に問題解決を図る(例 mock化)事ができます。

上記を確実に実施する為に、下記のような事を考えておけば良いのではないでしょうか。

・どこでどのように時間を取得するかアーキテクチャ上明確にし規約化する
・規約に反した時間取得方法を行っているプログラムを抽出する為に、規約チェックツールを作成する
  例) / SYSDATE/等でgrepする

ではでは

2011年7月6日水曜日

rails on oracle (oracleで接続する手順) その2

■概要

rails on oracle でいくつかのテーマで検証してみます

●fetchループ

module FetchTest
  SQL = "select p1.* from posts p1 cross join posts p2"
  def self.run
    Post.delete_all
    200.times do |i|
      Post.create(:title => "title_#{i}", :body => "body_#{i}")
    end
    open("test.txt", "w") do |f|
      # ① Post.find_by_sql(SQL).each { |r| f.write "#{r}\r\n" }
      # ② Post.connection.raw_connection.exec(SQL) { |r| f.write "#{r}\r\n" }
    end
  end
end

FetchTest.run

上記の場合①は通常の検索処理。sqlの実行結果を全てメモリ上に展開します。
②の場合、fetchループで繰り返し処理が発生する為、"同時に必要となるメモリ"は少なくてすみます。

# それぞれ実行しながら、topコマンド等でメモリ利用状況を確認しましょう。

よって大量のcsv出力する場合は必ず②のようにする必要があります。

●DROP

railsで実装する場合、割と頻繁にDBスキーマの全削除、再作成をするかと思います(うちだけ?)。その際の機能は充実してきており、adapter内で多数のDROP文が実装されています。

但し適宜 purge recyclebin してあげないとゴミがたまりそうです。

●indexの表領域

性能向上の為、データ/インデックスを別HDDに保存する場合、データ/インデックスで表領域を分ける必要があります。oracle_enhanced_adapterは成長しており、migrationで

add_index :posts, :title, :tablespace => 'foo'

のように指定できます。

●varchar2問題

oracleのvarchar2/char2は歴史的な経緯で、"バイト単位"での長さ指定になります。
よってrailsからスキーマ作成する際は、string => nvarchar2へのマッピングを以前はしていたのですが、最近のoracle enhanced adapterは進化しています!

lib/active_record/connection_adapters/oracle_enhanced_adapter.rb によれば、nls_length_semantics というオプションがデフォルトで'CHAR'となっており、このオプションを指定する事によってvarchar2/char2を文字数単位で長さ指定出来る様になります。

よって何も考えなくても、mysql等と同じように指定できる様になりました。
実際下記の用にスキーマが出来ています。
SQL> desc posts;
 名前                     NULL?    型
 -------------------------- -------- ----------------------------
 ID                         NOT NULL NUMBER(38)
 TITLE                               VARCHAR2(255 CHAR) <= CHARで255
 BODY                                CLOB
 CREATED_AT                          DATE
 UPDATED_AT                          DATE
●oracle enhanced adapter独自項目の設定 oracle enhanced adapterには独自の設定項目があり...と思ったら良いページがありました。一部ソースを読んだ結果下記の様です。
# config/initializers/oracle.rb 等に下記内容を設置
ActiveSupport.on_load(:active_record) do
  ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
    # NUMBER(1)をbooleanにマッピングするか
    self.emulate_booleans = true
    # DATE型を(rubyの)Date型にマッピングするか
    self.emulate_dates = false
    # カラム名が(^|_)date(_|$)とマッチする場合Date型にマッピングするか
    self.emulate_dates_by_column_name = false
    # カラム名が(^|_)idとマッチする場合Integer型にマッピングするか
    self.emulate_integers_by_column_name = true
    # CHAR(1)かVARCHAR(1)、またはカラム名が_FLAG/_YNで終了する場合Booleanにマッピングするか
    self.emulate_booleans_from_strings = true
    # シーケンサー作成する際のオプションを指定する
    self.default_sequence_start_value = "1 NOCACHE INCREMENT BY 1"
  end
end
●雑感
ソース見て思ったのですが、oracle enhanced adapter マジで進化してます!
かなりきっちりしてる感じです(識別子30文字制限問題、in句の1000上限問題 ...etc)

2011年7月2日土曜日

履歴管理システムの無視ファイル

cvs/subversion/git等、色々な履歴管理システムがありますが、いずれも "履歴管理しないファイル" を適切に指定する事が大切です。

# *.classを無視していない為、すごい事になっているチーム結構みた事あります  (;´д`)トホホ…

いつも調べてる様な気がしたので、これを気にまとめようかなと調べたら

...

かなり良いの発見しました!

github/gitignore

githubのチームが作成しています。色々な言語・フレームワークのgitignoreファイルサンプルがたくさんあります。ちなみにRails.gitignore

*.rbc
*.sassc
.sass-cache
capybara-*.html
.rspec
/.bundle
/vendor/bundle
/log/*
/tmp/*
/db/*.sqlite3
/public/system/*
/coverage/
/spec/tmp/*
**.orig
config/*.yml
rerun.txt
pickle-email-*.html

のようですね。確かに一通りそろってる感じ!

■追記

2011/08/04 下記もいるような気がする
/spec/reports/

後上記の設定ならdatabase.ymlも消えるので、database.yml.example => database.ymlへのコピーが手順的に入りますね!