gae+jruby+sinatra+haml+DataMapperでアプリケーション

[gae/jruby] gae+jruby+sinatra+haml+DataMapperでアプリケーションを作ってみる

何がしたいの?

誰が使うの?

  • 動的なスケールアップが可能な環境でサービスを公開したい人
  • 自サーバやレンタルサーバは運用したくないけどサービスを公開したい人
  • rubyアプリケーションをgae上で公開したい人

どのように機能するの?

  • appengine-java-sdkの上でjrubyを動作させます。jrubyの上でrubyアプリケーションが動作します。
  • sinatra,haml,DataMapperとそのappengine用アダプタdm-appengineを使用します。

何を使うの?

gem "google-appengine"
gem "appengine-rack"
gem 'dm-appengine'
gem 'sinatra'
gem 'haml'

どこで確認したの?

いつ確認したの?

  • 2010年4月11日

どんなものができたの?

appspot.com => nnz-弁当店
お弁当を注文すると、24時間以内の注文数を表示するアプリケーションです。
お弁当は3つのメニューから選択可能で、注文した履歴が掲示板形式で表示されます。

何を参考にして作ったの?

gihyo.jp => 第9回 SinatraとSequel・Hamlで掲示板アプリを作るの記事を参考にしてサンプル的なお弁当注文サイトを作ってみました。
もちろん注文してもお弁当は届きませんのでご安心ください。
記事の順序に従ってGAEの掟に合わせた改造を施しながら進めていきます。元記事の筆者の方にはコードの部分転用の承諾をいただきました。
記事の中で「Sequel」を使用している部分は「DataMpper」で書き換えていきます。

リスト1 DataMpperによるモデル定義の例(model/order.rb)

require 'dm-core'

DataMapper.setup(:default, "appengine://auto")

class Orders
  include DataMapper::Resource
  property  :id,          Serial
  property  :name,        Text 
  property  :product,     Text
  property  :date,        Time
  
  def date_jst
    (self.date+9*3600).strftime("%Y/%m/%d(%a) %H:%M:%S")
  end
  
  def count 
    datastore = com.google.appengine.api.datastore.DatastoreServiceFactory.getDatastoreService() 
    query = com.google.appengine.api.datastore.Query.new("Orders") 
    datastore.prepare(query).countEntities() 
  end 
end

上記のメソッドdate_jstは表記上の時刻を日本時間に変換するために9時間加算しています。
お弁当の購入時の現在時刻をデータとして保存すると、UTMの現在時刻が保存されます。それをローカルタイムに変換しようにもjrubyが稼働しているマシンもUTMがローカルタイムですので一筋縄では行きません。
やっつけ仕事的ですが9時間の加算を行い、表示するときだけ日本時間に無理やり変換しています。
またメソッドcountですが、gem "dm-aggregates"を利用すればDataMapperのCountメソッドが使用できるハズなのですが、力及ばずでLowレベルのAPIを使用しました。


リスト2 HamlによるHTML表現の例(views/layout.haml

!!! XML
!!! Strict

%html
  %head
    %title nnz-弁当店
    %meta{:"http-equiv"=>"Content-Type", :content=>"text/html", :charset=>"utf-8"}
    %link{:rel=>"stylesheet", :type=>"text/css", :href=>"/style.css"}
  %body
    %table{:width => "100%"}
      .banner
        %h1 nnz-弁当店ご注文β
        
    != yield

    .footer
      #column-a
        .column
          %ul
            %h3 nnz
            %li 
              %a{:href => "/"} home
            %li 
              %a{:href => "/"} products
            %li 
              %a{:href => "/"} orders
            %li 
              %a{:href => "/"} contact us
             

リンク先はダミーですが、なんちゃってフッターもつけてみました。


リスト3 HamlによるHTML表現の例(views/index.haml

%form{:method => "POST", :action => '/order'}
  %input{:type => "hidden", :name => "_method", :value => "PUT"}
  %table
    %tr
      %fieldset
        %h3 お弁当何にする?
        .radio_group.toggle
          %p
            %input.radio{:type=>"radio", :name=>"product", :id=>"product_1", :value=>"のり弁", :checked=>true}/
            %label.radio{:for=>"product_1"} のり弁
          %p
            %input.radio{:type=>"radio", :name=>"product", :id=>"product_2", :value=>"唐揚げ弁当", }/
            %label.radio{:for=>"product_2"} 唐揚げ弁当
          %p
            %input.radio{:type=>"radio", :name=>"product", :id=>"product_3", :value=>"特盛りカレー", }/
            %label.radio{:for=>"product_3"} 特盛りカレー
      %td 名前
      %td 
        %input{:type => "text", :name => "name"}
      %td
        %input{:type => "submit"}

.today_order
  %h3 本日(24時間以内)のご注文は #{h @orders.count} 件です。
        

- @orders.each do |order|
  .comment
    %h2= h order.product
    .info
      %span.name== by #{h order.name}
      %span.date== (#{h order.date_jst})

掲示板を一般公開すると情報セキュリティ的にいろいろと心配ですので、ラジオボタンと名前欄だけのアプリケーションにしました。
自由入力が可能な名前欄はescape_html(hで別名定義済)でエスケープを行うこともセキュリティ上必要です。



リスト4 SassによるCSS表現の例(views/style.sass)

.banner
  margin: 0px 0%
  padding: 1em
  text-align: center
  color: #FFF
  background-color: #5998CF
  background: -webkit-gradient(linear, left top, left bottom, from(#70AEE5), to(#4080B6))
  background: -moz-linear-gradient(top, #70AEE5, #4080B6)  
   

body
  margin: 0px 0%
  padding: 0px
  focus: autoline none

h1
  margin: 1em

form
  margin: 0px 20%
  padding: 0.5em

.today_order
  margin: 10px 20%
  padding: 0.5em
  
.comment
  border: 1px solid black
  margin: 10px 20%
  padding: 0.5em
    
  h2
    margin: 0px
    font-size: medium
    float: left

  .info
    span.name
      padding-left: 0.5em
      font-size: small
    span.date
      font-size: small

  .message
    margin-top: 1em

.footer 
  background: #EEE
  width: 100%
  font-size: 15px
  margin: 0px
  padding-bottom: 20px
  text-align: left !important
  
  .column
    width: 120px
    padding: 10px 0 5px 20px

  h3
    font-size: 12px
    color: #444
    font-weight: bold
    
  li 
    font-size: 11px
    margin: 0
    padding: 0 0 0 10px

    a 
      color: #444
      text-decoration: none
      padding: 0 10px
      margin-left: -10px
      
ul
  margin-bottom: 20px
  list-style: none outside

CSSの代わりにSASSでスタイル指定を行います。
SASSを使えばタグ地獄から開放されるかな?思いきやイヤイヤこれも大変です。
メンテが楽になるのはおそらく間違いないと思いますが、新規作成には結構ひと苦労です。



リスト5 Sinatraによるコントローラ部分(start.rb)

require 'sinatra'
require 'model/order.rb'

helpers do
  include Rack::Utils
  alias_method :h, :escape_html
end

get '/style.css' do
  content_type 'text/css', :charset => 'utf-8'
  sass :style
end

get '/' do
  #24時間以内の注文
  @orders = Orders.all(:date.gt => Time.now-24*3600, :order => [:date.desc], :limit => 100)
  haml :index
end

put '/order' do
  Orders.create({
    :name     => request[:name],
    :product  => request[:product],
    :date     => Time.now,
  })
  
  redirect '/'
end

Orders.allの部分で発注時刻が24時間以内の最大100レコードを時刻降順で取り出しています。
発注時刻は"Time.now"部分で画面が呼ばれた時刻が記録されていますが、これはGAEが動作している環境に依存する時刻です。もちろん日本時間ではありません。



リスト6 (Gemfile)

# Critical default settings:
disable_system_gems
disable_rubygems
bundle_path ".gems/bundler_gems"

# List gems to bundle here:
gem "appengine-rack"
gem 'dm-appengine'
gem 'sinatra'
gem 'haml'

'dm-appengine'部分がDataMapperを含んでいます。使用するgem名をGemfileへ記述しておくと、ダウンロードからjarファイルへの取り込みまで全部面倒を見てくれます。