Railsのルーティングを理解する

railsのルーティング周りについて、自分自身がルーティング設定をする時に迷わないようにするため、また内部でどのような処理がされているのかをイメージしながら設定を書けるようになるため、調べた内容をまとめてみる。

最初は、ネストしたルーティングの設定の仕方がよくわからなかったが、スコープを意識することで定義したいリソースを生成したい場合にどのようにルーティングを設定すればよいのかが段々とわかるようになってきた。

  • rails-4.2.5で動作確認した内容です。
  • 間違いがあれば、コメント等でお知らせいただけますと幸いです。
  • サンプルコードは、特に実用性を深く考慮したものではないです。

ルーターの役割

ルーターは,PATH_INFOの情報をもとに、HTTPリクエストを適切なコントローラーのアクションへディスパッチする(正確には、通常Dispatcherオブジェクトがdispatchする)。例えば,以下のような設定を考えてみる。

get '/books/:id', to: 'books#show', as: 'book'

上記設定で生成されるルーティングは以下のようになり,

book GET /books/:id(.:format) books#show

GET /books/1のようなHTTPリクエストを送ると、books_controllerのshowメソッドにディスパッチされる。

GET /books/1
=> books_controller#show
   params[:id] => 1

ルーティング設定

getやpostなどのHTTPメソッド宣言やmatchのような宣言でURLパターンを記述したり、resourcesやresourceのリソーススコープとcollectionやmemberなどのスコープレベルを組み合わせたりすることでルーティングを設定していく。

resources

リソースフルな一連のルーティングを生成する。デフォルトで以下のアクションが用意されている。@api_onlyは,rails5からAPI機能に特化した開発を行なうために追加されたオプションであり,apiモードの時に生成されるアクションでは:new:editが含まれない形になっている。

def default_actions
  if @api_only
    [:index, :create, :show, :update, :destroy]
  else
    [:index, :create, :new, :show, :update, :destroy, :edit]
  end
end

resourcesを使って以下のような設定をすると、上記のデフォルトアクションに対応するルーティングが生成される。

resources :books

この設定により以下のルーティングが生成される。

     books GET    /books(.:format)          books#index
           POST   /books(.:format)          books#create
  new_book GET    /books/new(.:format)      books#new
 edit_book GET    /books/:id/edit(.:format) books#edit
      book GET    /books/:id(.:format)      books#show
           PATCH  /books/:id(.:format)      books#update
           PUT    /books/:id(.:format)      books#update
           DELETE /books/:id(.:format)      books#destroy

param

セグメント:idは、paramオプションで変えることもできる。,

resources :books, param: :no

上記設定により以下のようなルーティングが生成される。

    books GET    /books(.:format)          books#index
          POST   /books(.:format)          books#create
 new_book GET    /books/new(.:format)      books#new
edit_book GET    /books/:no/edit(.:format) books#edit
     book GET    /books/:no(.:format)      books#show
          PATCH  /books/:no(.:format)      books#update
          PUT    /books/:no(.:format)      books#update
          DELETE /books/:no(.:format)      books#destroy

only、except

onlyやexceptオプションを使うと、ルーティングを特定のアクションに限定したり、ルーティングから特定のアクションを除外したりすることができる。以下はonlyオプションを使って:showアクションのみに限定した場合の例。

resources :books, only: [:show]

以下のルーティングが生成される。

book GET /books/:id(.:format) books#show

collection、 member、new

resourcesは、newおよび後述するcollectionとmemberを使っても同じように設定を記述することができる。実際、resourcesは呼び出しの中でこれらのスコープを使っている。以下のような設定を書いてみたところ、resources :booksと同様のルーティングが生成されるのを確認できる(実際にはこのような書き方はしないだろう。。)。

resources :books, only:[] do
  collection do
    get :index , to: 'books#index'
    post :create, to: 'books#create'
  end
  new do
    get :new, to: 'books#new'
  end
  member do
    get :edit , to: 'books#edit'
    get :show , to: 'books#show'
    patch :update , to: 'books#update'
    put :update , to: 'books#update'
    delete :destroy, to: 'books#destroy'
  end
end

nested

nestedスコープは、resourcesスコープで:#{singular}_#{param}の動的パスセグメントを生成する。例えば、以下ではパス/reviewsに対して、:book_idという動的パスセグメントを生成している。

resources :books do
  # この宣言は、nestedスコープで呼ばれるので、:book_idセグメントが使われる
  get 'reviews', to: 'reviews#index'
end

以下のルーティングが生成される。

book_reviews GET    /books/:book_id/reviews(.:format) reviews#index
       books GET    /books(.:format)                  books#index
             POST   /books(.:format)                  books#create
    new_book GET    /books/new(.:format)              books#new
   edit_book GET    /books/:id/edit(.:format)         books#edit
        book GET    /books/:id(.:format)              books#show
             PATCH  /books/:id(.:format)              books#update
             PUT    /books/:id(.:format)              books#update
             DELETE /books/:id(.:format)              books#destroy

:book_idでなく:idを使いたい場合は、memberスコープにすればよい。

resources :books do
  get 'reviews', to: 'reviews#index', on: :member
end

resource

単数形リソース(id参照を必要としないリソース)を使用する時に使う。デフォルトで以下のアクションが用意されている。

def default_actions
  [:show, :create, :update, :destroy, :new, :edit]
end

例えば、ログインしているユーザーに対するリソースフルな一連のルーティングを生成したい場合、以下のように設定できる。

resource :user

上記設定により生成されるルーティングは以下のようになる。

     user POST   /user(.:format)      users#create
 new_user GET    /user/new(.:format)  users#new
edit_user GET    /user/edit(.:format) users#edit
          GET    /user(.:format)      users#show
          PATCH  /user(.:format)      users#update
          PUT    /user(.:format)      users#update
          DELETE /user(.:format)      users#destroy

indexアクションのルーティングが含まれない点が、resourcesと異なる。

スコープで生成されるリソース

ActionDispatch::RoutingモジュールのMapper::Resources::Resource(resourcesで呼ばれる)やMapper::Resources::SingletonResourceクラス(resourceで呼ばれる)を見ると、各スコープでどのようなパスセグメントが生成されるのか把握することができる。

ActionDispatch::Routing::Mapper::Resources
from www.omniref.com

以下表にまとめてみる。

Scope Level Resources, Resource Resource (SingularResource)
collection path path
member “#{path}/:#{param}” path (default scope)
new “#{path}/new” “#{path}/new”
nested “#{path}/:#{singular}_#{param}” (default scope) path
Called Inside collection
・get :index
・post :create
new
・get :new
member
・get :edit
・get :show
・patch :update
・put :update
・delete :destroy
collection
・post :create
new
・get :new
member
・get :edit
・get :show
・patch :update
・put :update
・delete :destroy

例えば以下の設定例では、下表のようになる。

resources :books do
  get 'reviews', to: 'reviews#index'
end
resource :user do
  get 'profile', to: 'users#profile'
  # 以下もok
  # get 'profile'
end
Scope Level Resources, Resource Resource (SingularResource)
collection /books /user
member /books/:id /user
/user/profile
new /books/new /user/new
nested /books/:book_id/reviews

scope-routing

collection

id参照のないリソースを使用したい時に使う。resourcesまたはresourceのスコープ外で宣言するとArgumentErrorになる。

resources :books do
  collection do
    get 'categories'
  end
end

# 上記は以下のようにも書ける。

resources :books do
  get 'categories', on: :collection
end

以下のルーティングが生成される。

categories_books GET    /books/categories(.:format) books#categories
           books GET    /books(.:format)            books#index
                 POST   /books(.:format)            books#create
        new_book GET    /books/new(.:format)        books#new
       edit_book GET    /books/:id/edit(.:format)   books#edit
            book GET    /books/:id(.:format)        books#show
                 PATCH  /books/:id(.:format)        books#update
                 PUT    /books/:id(.:format)        books#update
                 DELETE /books/:id(.:format)        books#destroy

member

id参照のあるリソースを使用したい時に使う。resourcesまたはresourceのスコープ外で使うとArgumentErrorになる。

resources :books do
  member do
    get 'details'
  end
end

# 上記は以下のようにも書ける。

resources :books do
  get 'details', on: :member
end

以下のルーティングが生成される。

details_book GET    /books/:id/details(.:format) books#details
       books GET    /books(.:format)             books#index
             POST   /books(.:format)             books#create
    new_book GET    /books/new(.:format)         books#new
   edit_book GET    /books/:id/edit(.:format)    books#edit
        book GET    /books/:id(.:format)         books#show
             PATCH  /books/:id(.:format)         books#update
             PUT    /books/:id(.:format)         books#update
             DELETE /books/:id(.:format)         books#destroy

ちなみに、memberを使わないと以下のルーティングとなる。

book_details GET    /books/:book_id/details(.:format) books#details
       books GET    /books(.:format)                  books#index
             POST   /books(.:format)                  books#create
    new_book GET    /books/new(.:format)              books#new
   edit_book GET    /books/:id/edit(.:format)         books#edit
        book GET    /books/:id(.:format)              books#show
             PATCH  /books/:id(.:format)              books#update
             PUT    /books/:id(.:format)              books#update
             DELETE /books/:id(.:format)              books#destroy

pathヘルパーメソッドのprefixおよび動的パスセグメント名が異なっているので注意。

root

アプリケーションルートのルーティングを生成する。以下の3つの定義は同じとなるが、1)の書き方が最もシンプル。

1) root 'welcome#index'
2) root to: 'welcome#index'
3) match '/', as: :root, via: :get, to: 'welcome#index'

namespaceやscope内でも宣言することができる。例えば、以下はscopeのブロック内で、3)の設定をしたことと同じである。

scope 'admin', as: 'admin' do
  root to: 'admin#index'
end

以下のルーティングが生成される。

admin_root GET /admin(.:format) admin#index

HttpHelpers

HttpHelpersモジュールには、代表的なHTTPメソッドのヘルパーメソッドが定義されている。これらのメソッドを使ってresourcesで生成されるルーティングの設定を記述すると以下のようになる。

get    '/books'    , to: 'books#index', as: 'books'
post   '/books'    , to: 'books#create'
get    '/books/new', to: 'books#index', as: 'new_book'
get    '/books/:id/edit', to: 'books#edit', as: 'edit_book'
get    '/books/:id', to: 'books#show'
patch  '/books/:id', to: 'books#update'
put    '/books/:id', to: 'books#update'
delete '/books/:id', to: 'books#destroy'

match

HttpHelpersモジュールのメソッドは内部でこのメソッドを呼んでいる。getやpostでは、呼び出しに対応するHTTPメソッドが1つに決定されるが、matchではviaオプションを使って複数のHTTPメソッドに対応するルーティングを生成することができる。

match '/books/:id', to: 'books#update', as: 'book', via: [:put, :patch]

以下のルーティングが生成される。

book PUT|PATCH /books/:id(.:format) books#update

anchor

anchorオプションをfals`に設定すると、pathで始まるリソースに一致するリクエスト全てで有効となる。

# /books
# /books/any/path/ok
match 'books', to: 'books#index', anchor: false, via: :get

scope

様々なスコープを作り、ルーティング設定を記述することができる。ここでのオプションは、matchやresourcesでも同様に使うことができる。例えば、以下はpath: '/books'オプションを保持したスコープを作り、そのスコープ内でさらにgetメソッドを使ってルーティングを設定している。

scope '/books' do
  get ':id', to: 'books#show', as: 'book'
end

以下のルーティングが生成される。

book GET /books/:id(.:format) books#show

path

scope <path> do
  # do something
end

# または

scope path: <path> do
  # do something
end

as

スコープ内でasオプションで指定される値をpathヘルパーメソッドのprefixに追加する。

scope as: <path_helper_name> do
  # do something
end

以下では、pathオプションを保持するスコープでasオプションを使ってpathヘルパーメソッドのprefixをそれぞれbooksとbookに設定している。

scope '/books' do
  scope as: 'books' do
    get    '/'    , to: 'books#index'
    post   '/'    , to: 'books#create'
  end
  get    '/new', to: 'books#new', as: 'new_book'
  get    '/:id/edit', to: 'books#edit', as: 'edit_book'
  scope as: 'book' do
    get    ':id', to: 'books#show'
    patch  ':id', to: 'books#update'
    put    ':id', to: 'books#update'
    delete ':id', to: 'books#destroy'
  end
end

以下のルーティングが生成される。

    books GET    /books(.:format)          books#index
          POST   /books(.:format)          books#create
 new_book GET    /books/new(.:format)      books#new
edit_book GET    /books/:id/edit(.:format) books#edit
     book GET    /books/:id(.:format)      books#show
          PATCH  /books/:id(.:format)      books#update
          PUT    /books/:id(.:format)      books#update
          DELETE /books/:id(.:format)      books#destroy

controller

スコープ内でcontrollerオプションで指定されるcontrollerを使う。

scope controller: <controller> do
  # do something
end

# 以下も同じ

controller <controller> do
  # do something
end

以下では、path、controller、asの3つのオプションを保持したスコープを作り、その中でいくつかのルーティングを設定している。

scope '/books', controller: :books, as: :books do
  get '/', to: :index
  post '/', to: :create
end

以下のルーティングが生成される。

books GET /books(.:format) books#index
     POST /books(.:format) books#create

action

スコープ内でactionオプションで指定されるactionを使う。

scope action: <action> do
  # do something
end

以下では、path、controller、action、asの4つのオプションを保持したスコープを作り、ルーティングを設定している。

scope '/books/:id', controller: :books, action: :update, as: :book do
  match '/', via: [:put, :patch]
end

以下のルーティングが生成される。

book PUT|PATCH /books/:id(.:format) books#update

module

スコープ内でmoduleオプションで指定されるmoduleを使う。

scope module: <module> do
  # do something
end

以下では、path、module、asの3つのオプションを保持したスコープを作り、adminモジュールのadminコントローラーのindexアクションに対するルーティングを設定している。

scope '/admin', module: :admin, as: :admin do
  get '/', to: 'admin#index'
end

以下のルーティングが生成される。

admin GET /admin(.:format) admin/admin#index

shallow_path

スコープ内でshallow_pathオプションで指定される値を、shallowオプションが指定されているリソースのメンバーリソース(id参照のある)の先頭に追加する。

scope shallow_path: <shallow_path> do
  # do something
end

以下では、reviewsの一連のid参照をするリソースにunpublishedというパスセグメントを追加している。

scope shallow_path: 'unpublished' do
  resources :books do
    resources :reviews, shallow: true
  end
end
   book_reviews GET    /books/:book_id/reviews(.:format)       reviews#index
                POST   /books/:book_id/reviews(.:format)       reviews#create
new_book_review GET    /books/:book_id/reviews/new(.:format)   reviews#new
    edit_review GET    /unpublished/reviews/:id/edit(.:format) reviews#edit
         review GET    /unpublished/reviews/:id(.:format)      reviews#show
                PATCH  /unpublished/reviews/:id(.:format)      reviews#update
                PUT    /unpublished/reviews/:id(.:format)      reviews#update
                DELETE /unpublished/reviews/:id(.:format)      reviews#destroy
          books GET    /books(.:format)                        books#index
                POST   /books(.:format)                        books#create
       new_book GET    /books/new(.:format)                    books#new
      edit_book GET    /books/:id/edit(.:format)               books#edit
           book GET    /books/:id(.:format)                    books#show
                PATCH  /books/:id(.:format)                    books#update
                PUT    /books/:id(.:format)                    books#update
                DELETE /books/:id(.:format)                    books#destroy

shallow_prefix

スコープ内でshallow_prefixオプションで指定される値を、shallowオプションが指定されているリソースのメンバーリソース(id参照のある)に対応するpathヘルパーメソッドのprefixに追加する。

scope shallow_prefix: <shallow_prefix> do
  # do something
end

以下の例では、unpublishedをpathヘルパーメソッドのprefixに追加している。

scope shallow_prefix: 'unpublished' do
  resources :books do
    resources :reviews, shallow: true
  end
end

以下のルーティングが生成される。

           book_reviews GET    /books/:book_id/reviews(.:format)     reviews#index
                        POST   /books/:book_id/reviews(.:format)     reviews#create
        new_book_review GET    /books/:book_id/reviews/new(.:format) reviews#new
edit_unpublished_review GET    /reviews/:id/edit(.:format)           reviews#edit
     unpublished_review GET    /reviews/:id(.:format)                reviews#show
                        PATCH  /reviews/:id(.:format)                reviews#update
                        PUT    /reviews/:id(.:format)                reviews#update
                        DELETE /reviews/:id(.:format)                reviews#destroy
                  books GET    /books(.:format)                      books#index
                        POST   /books(.:format)                      books#create
               new_book GET    /books/new(.:format)                  books#new
              edit_book GET    /books/:id/edit(.:format)             books#edit
                   book GET    /books/:id(.:format)                  books#show
                        PATCH  /books/:id(.:format)                  books#update
                        PUT    /books/:id(.:format)                  books#update
                        DELETE /books/:id(.:format)                  books#destroy

path_names

path_namesオプションで指定される値で、newやeditなどの自動生成されるパスセグメントをオーバーライドする。

scope path_names: {key: value, ...} do
  # do something
end
scope path_names: {new: 'init'} do
  resources :books
end

以下のルーティングが生成される。

    books GET    /books(.:format)          books#index
          POST   /books(.:format)          books#create
 new_book GET    /books/init(.:format)     books#new
edit_book GET    /books/:id/edit(.:format) books#edit
     book GET    /books/:id(.:format)      books#show
          PATCH  /books/:id(.:format)      books#update
          PUT    /books/:id(.:format)      books#update
          DELETE /books/:id(.:format)      books#destroy

shallow

浅いネストを設定する。

scope shallow: true do
  # do something
end

# 以下も同じ

shallow do
  # do something
end
scope shallow: true do
  resources :books do
    resources :reviews do
      resources :votes
    end
  end
end

以下のルーティングが生成される。

   review_votes GET    /reviews/:review_id/votes(.:format)     votes#index
                POST   /reviews/:review_id/votes(.:format)     votes#create
new_review_vote GET    /reviews/:review_id/votes/new(.:format) votes#new
      edit_vote GET    /votes/:id/edit(.:format)               votes#edit
           vote GET    /votes/:id(.:format)                    votes#show
                PATCH  /votes/:id(.:format)                    votes#update
                PUT    /votes/:id(.:format)                    votes#update
                DELETE /votes/:id(.:format)                    votes#destroy
   book_reviews GET    /books/:book_id/reviews(.:format)       reviews#index
                POST   /books/:book_id/reviews(.:format)       reviews#create
new_book_review GET    /books/:book_id/reviews/new(.:format)   reviews#new
    edit_review GET    /reviews/:id/edit(.:format)             reviews#edit
         review GET    /reviews/:id(.:format)                  reviews#show
                PATCH  /reviews/:id(.:format)                  reviews#update
                PUT    /reviews/:id(.:format)                  reviews#update
                DELETE /reviews/:id(.:format)                  reviews#destroy
          books GET    /books(.:format)                        books#index
                POST   /books(.:format)                        books#create
       new_book GET    /books/new(.:format)                    books#new
      edit_book GET    /books/:id/edit(.:format)               books#edit
           book GET    /books/:id(.:format)                    books#show
                PATCH  /books/:id(.:format)                    books#update
                PUT    /books/:id(.:format)                    books#update
                DELETE /books/:id(.:format)                    books#destroy

constraints

通常Dispatcherがルーティングエンドポイントとなっているところに、一層処理を挟む形となる。

これを利用すると、動的パスセグメントのURL形式に特定の制約を課すことができる。constraintsオプションのHashキーには、Requestクラスのメソッド名を指定することもでき、その場合、valueはメソッドの返り値の型と一致させる必要がある。

scope constraints: <constraints> do
  # do something
end

# 以下も同じ

constraints <constraints> do
  # do something
end

idパラメータが数値のみを許可する例。

scope constraints: {id: /\d+/ } do
  get '/books/:id', to: 'books#show'
  delete '/books/:id', to: 'books#destroy'
end

以下のルーティングが生成される。

GET    /books/:id(.:format) books#show {:id=>/\d+/}
DELETE /books/:id(.:format) books#destroy {:id=>/\d+/}

ローカルからのリクエストの時のみ許可する例。

Requestクラスのlocal?メソッドを使う。

scope constraints: {'local?': true} do
  get '/debug', to: 'debug#info'
end

ActionDispatch::Journey::Route
from www.omniref.com

constraintsに任意のオブジェクトを指定する

matches?メソッドに応答するオブジェクトを指定することができる。

class SmartPhoneConstraint
  def initialize
    @agent_tags = SmartPhone.agent_tags
  end

  def matches?(request)
    user_agent = request.headers["HTTP_USER_AGENT"]
    @agent_tags.any? {|tag| user_agent.include?(tag) }
  end
end

scope constraints: SmartPhoneConstraint.new do
  get '/smartphones', to: 'smartphones#index'
end

Procやlambdaなどのcallに応答するオブジェクトを指定することもできる。

scope constraints: Proc.new {|req| req.local? } do
  get '/local', to: 'local#index'
end

以下のルーティングが生成される。

local GET /local(.:format) books#index

defaults

パラメータのデフォルト値を設定する。

scope defaults: <default> do
  # do something
end

# 以下も同じ

defaults <default> do
  # do something
end
scope defaults: {format: :json} do
  get '/books/:id', to: 'books#show', as: 'book'
end

以下のルーティングが生成される。

book GET /books/:id(.:format) books#show {:format=>:json}

namespace

namespace :admin, as: 'admin' do
  # do something
end

scopeで以下のように書いたのと同じになる。

scope path: 'admin', as: 'admin', module: 'admin', shallow_path: 'admin', shallow_prefix: 'admin' do
  # do something
end

concern

ルーティング設定を再利用することができる。resourcesやresourceのオプションで指定できる。

concern :attachable do
  post '/file_upload', to: 'files#upload'
end

resources :pages, concerns: :attachable
resource :user, concerns: :attachable

以下のルーティングが生成される。

page_file_upload POST   /pages/:page_id/file_upload(.:format) files#upload
           pages GET    /pages(.:format)                      pages#index
                 POST   /pages(.:format)                      pages#create
        new_page GET    /pages/new(.:format)                  pages#new
       edit_page GET    /pages/:id/edit(.:format)             pages#edit
            page GET    /pages/:id(.:format)                  pages#show
                 PATCH  /pages/:id(.:format)                  pages#update
                 PUT    /pages/:id(.:format)                  pages#update
                 DELETE /pages/:id(.:format)                  pages#destroy
file_upload_user POST   /user/file_upload(.:format)           files#upload
            user POST   /user(.:format)                       users#create
        new_user GET    /user/new(.:format)                   users#new
       edit_user GET    /user/edit(.:format)                  users#edit
                 GET    /user(.:format)                       users#show
                 PATCH  /user(.:format)                       users#update
                 PUT    /user(.:format)                       users#update
                 DELETE /user(.:format)                       users#destroy

Rackアプリケーションの指定

toオプションにrackアプリケーションを指定することができる。

class MyApp
  def call(env)
    serve ActionDispatch::Request.new(env)
  end

  def matches?(req)
    req.local?
  end

  def serve(env)
    [200, {'Content-Type' => 'text/plain'}, ['hello myapp']]
  end
end

match '/myapp', to: MyApp.new, via: :all

以下のようなルーティングが生成される。

myapp /myapp(.:format) #<MyApp:0x007fa4d0c7def0>

コントローラーのアクションもrackのエンドポイント

toには、アクションを直接指定することもできる。

get '/books', to: BooksController.action(:index)

redirect

toオプションにredirectを使うと、動的セグメントを利用してリダイレクトさせることができる。

get '/readers/:name', to: redirect('/reviewers/%{name}'), as: 'reader'
get '/authors/:name', to: redirect(path: '/writers/%{name}', subdomain: 'book'), as: 'author'

以下のルーティングが生成される。

reader GET /readers/:name(.:format) redirect(301, /reviewers/%{name})
author GET /authors/:name(.:format) redirect(301, path: /writers/%{name}, subdomain: book)

mount

アプリケーションにRackアプリケーションやEngineをマウントすることができる。内部では、matchが呼び出されている。

Ex1) mount MyRackApp, at: 'myapp'
Ex2) mount MyRackApp => 'myapp'
Ex3) mount MyRackApp => 'myapp', as: 'my'
=> match 'myapp', to: MyRackApp, via: :all, format: false, anchor: false, as: 'my'
Ex4) mount MyApp::Engine => '/myapp'

Ex3の例では、以下のルーティングが生成され、/myappでアプリケーションにアクセスすることができる。

my /myapp #<MyApp:0x007fa4cba5a790>

ルーティング適用の順番

ルーティングは、先に設定しているものが優先される。

# こっちが先に定義されているので呼ばれる
get '/first', to: 'welcome#index'
# こっちは呼ばれない
get '/first', to: 'books#index'

参考リンク

  • http://railsguides.jp/routing.html
  • http://railsguides.jp/engines.html
Ruby on Rails 4 アプリケーションプログラミング
山田 祥寛
技術評論社
売り上げランキング: 23,409

byebyehaikikyou

日記やIT系関連のネタ、WordPressに関することなど様々な事柄を書き付けた雑記です。ITエンジニア経験があるのでプログラミングに関することなどが多いです。

シェアする

1件のコメント

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

コメントする

Translate »