akishin999の日記

調べた事などを書いて行きます。

Cassandra と Ruby で簡易掲示板を作ってみる


そろそろ Cassandra で具体的なアプリケーションを作ってみたくなったので、簡単な掲示板を作成してみました。

作成環境は以下のようになります。

データモデル


まずはアプリケーションから使うデータモデルを設計します。
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 でのデータモデルに慣れていないので、設計的におかしいところもあるかも知れませんが、取り合えず具体的に動作するサンプルアプリケーションが作成できました。
「こうした方が良い」といった突っ込み大歓迎です。
ご指摘宜しくお願いします。


Cassandra
Cassandra
posted with amazlet at 13.06.05
Eben Hewitt
オライリージャパン
売り上げランキング: 273,748
Cassandra: The Definitive Guide
Eben Hewitt
Oreilly & Associates Inc
売り上げランキング: 70,221