Cassandra と Ruby で簡易掲示板を作ってみる
そろそろ Cassandra で具体的なアプリケーションを作ってみたくなったので、簡単な掲示板を作成してみました。
作成環境は以下のようになります。
- ruby 1.8.7 (2010-01-10 patchlevel 249) [i386-mingw32]
- Rails 2.3.5
データモデル
まずはアプリケーションから使うデータモデルを設計します。
storage-conf.xml に Keyspace を以下のように定義しました。
- storage-conf.xml
<Keyspace Name="Examples"> <ColumnFamily Name="Entries" CompareWith="BytesType"/> <ColumnFamily Name="Boards" CompareWith="LongType"/> <ReplicaPlacementStrategy>org.apache.cassandra.locator.RackUnawareStrategy</ReplicaPlacementStrategy> <ReplicationFactor>1</ReplicationFactor> <EndPointSnitch>org.apache.cassandra.locator.EndPointSnitch</EndPointSnitch> </Keyspace>
ColumnFamily は、Twissanda のスキーマを参考に以下のイメージにしました。
Entries = { '3f7dac40-6038-11df-86e3-c06abadd121c': { 'id': '3f7dac40-6038-11df-86e3-c06abadd121c', 'board_id': 'board1', 'title': 'タイトル', 'content': '内容', 'created_at': '1273938054.404', }, } Boards = { 'board1': { 1273944798014000: '3f7dac40-6038-11df-86e3-c06abadd121c', 1273944824839000: '4fc227c0-603d-11df-881a-1f654b68b6fb', 1273944850714000: '5642bc90-603d-11df-9271-b18292bd5d1f', 1273944850717000: 'c2fe6e10-603d-11df-91a8-e6d54a3ad2a5', }, }
投稿の一つ一つを保持する Entries と、タイムスタンプをカラム名に Entries のキーを値を持つことで投稿順序を保持するための Boards になります。
ここで Cassandra が起動したままだった場合は、一旦再起動してください。
Rails アプリケーションの作成
それでは、rails コマンドを実行して Rails アプリケーションを作成します。
>rails cassandra_example create create app/controllers create app/helpers create app/models ・ ・ ・
作成したら、config/environment.rb を開き、以下の行を
- environment.rb
# config.frameworks -= [ :active_record, :active_resource, :action_mailer ]
を以下のように修正し、ActiveRecord を使わないようにします。
config.frameworks -= [ :active_record ]
それでは、実際にコードを書いていきます。
- lib/cassandra_utils.rb
Cassandra を操作するためのユーティリティクラスです。
require 'cassandra' class CassandraUtils @@client = Cassandra.new('Examples', '127.0.0.1:9160') # 指定された ColumnFamily の指定された key を持つレコードを取得 def self.find_by_key(column_family, key) @@client.get(column_family, key) end # 指定された ColumnFamily のレコードを取得 def self.find_all(column_family) list = @@client.get_range(column_family, :consistency => Cassandra::Consistency::QUORUM).map { |keyslice| tmp = keyslice.columns.map { |col| tmp << [col.column.name, col.column.value] } Hash[*tmp.flatten] } list end # 指定されたレコードを保存する def self.save(column_family, key, record = {}) @@client.batch { record.each { |k, v| @@client.insert(column_family, key, {k => v}) } } end # Key として使う為のタイムスタンプ値を取得 def self.timestamp (Time.now.to_f * 1e6).to_i end end
- app/models/entry.rb
掲示板への投稿を表すモデルクラスになります。
models の下に配置しましたが、ActiveRecord は継承していません。
require 'simple_uuid' class Entry include SimpleUUID COLUMN_FAMILY_BOARDS = :Boards COLUMN_FAMILY_ENTRIES = :Entries attr_accessor :id, :board_id, :title, :content, :created_at # 時刻のみ、文字列として格納しているので、 # 読み込み時に setter で Time に戻す。 def created_at=(value) case value when String @created_at = Time.at(value.to_f) when Time @created_at = value else @created_at = Time.at(value) end end # コンストラクタ def initialize(hash = nil) return unless hash hash.each { |key, value| self.send "#{key}=", value } end # 指定された BOARD_ID の全エントリを取得 def self.find_all_entries(board_id) list = [] CassandraUtils.find_by_key(COLUMN_FAMILY_BOARDS, board_id).each_value { |entry_id| list << Entry.new(CassandraUtils.find_by_key(COLUMN_FAMILY_ENTRIES, entry_id)) } list end # 自身を Cassandra に保存 def save return if @id @id = UUID.new.to_guid.to_s @created_at = Time.now CassandraUtils.save(COLUMN_FAMILY_ENTRIES, @id, { 'id' => @id, 'board_id' => @board_id, 'title' => @title, 'content' => @content, 'created_at' => @created_at.to_f.to_s } ) # 板に発言IDを追加 CassandraUtils.save(COLUMN_FAMILY_BOARDS, @board_id, { CassandraUtils.timestamp => @id } ) end end
- app/controllers/cassandra_controller.rb
コントローラです。
スキーマ的には複数掲示板も管理できるのですが、今回は簡単の為 BOARD_ID は固定にしています。
class CassandraController < ApplicationController # 掲示板 ID は取り合えず固定 BOARD_ID = 'board1' def index @entries = Entry.find_all_entries(BOARD_ID) end def new end def create @entry = Entry.new @entry.board_id = BOARD_ID @entry.title = params[:title] @entry.content = params[:content] @entry.save redirect_to(:action => :index) end end
- app/views/cassandra/new.html.erb
投稿画面です。
以下のようなイメージです。
<% form_tag( {:controller => :cassandra, :action => :create} ) do -%> 件名:<br /> <%= text_field_tag(:title) %><br /> <br /> 投稿内容:<br /> <%= text_area_tag(:content, nil, :size => "30x10") %><br /> <%= submit_tag '書込み' %> <% end -%>
- app/views/cassandra/index.html.erb
一覧画面です。
以下のようなイメージです。
<table border="1"> <tr> <th>タイトル</th> <th>内容</th> <th>投稿日時</th> </tr> <% @entries.each { |entry| -%> <tr> <td><%=h entry.title %></td> <td><%=h entry.content %></td> <td><%=h entry.created_at.to_s(:db) %></td> </tr> <% } -%> </table> <br/> <br/> <%= link_to '投稿する', :action => :new %>
動かしてみる
ファイルの作成が完了したら「ruby script/server」でアプリケーションを起動します。
起動したら「http://localhost:3000/cassandra/」にアクセスすると一覧画面が表示されると思います。
まだ KVS でのデータモデルに慣れていないので、設計的におかしいところもあるかも知れませんが、取り合えず具体的に動作するサンプルアプリケーションが作成できました。
「こうした方が良い」といった突っ込み大歓迎です。
ご指摘宜しくお願いします。
Oreilly & Associates Inc
売り上げランキング: 70,221