Railsで入れ子のリソースに対してCRUD操作を行うAPIを作成する


# はじめに

今回はRails5のAPIモードを利用してRest API(以下API)を作成する手順を説明します。
作成するAPIは1:多の関連を持つモデルに対してCRUD操作を行うAPIです。まずは親テーブル(1:多の1に該当するほう)に関するAPIを作成し、その後入れ子の形で子テーブル(1:多の多に該当するほう)を作成していきます。ソースコードの雛形はscaffoldで作成します。
また、APIの検証方法として`seeds.rb`にテストデータを用意し、`curl`コマンドで確認を行うという方法をとります。
CRUD操作を行うAPIを作成後、追加機能として検索機能を作成します。

# APIの概要

今回作成するAPIについてですが、先ほども説明をしたように、1:多の関連を持つモデルに対してCRUD操作を行うAPIです。
詳細は以下のようになっています。

## ルーティング

以下が最終的に作成するAPIのルーティングになっています。

```
$ rails routes
        Prefix Verb   URI Pattern                              Controller#Action
 search_events GET    /events/search(.:format)                 events#search
event_comments GET    /events/:event_id/comments(.:format)     comments#index
               POST   /events/:event_id/comments(.:format)     comments#create
 event_comment GET    /events/:event_id/comments/:id(.:format) comments#show
               PATCH  /events/:event_id/comments/:id(.:format) comments#update
               PUT    /events/:event_id/comments/:id(.:format) comments#update
               DELETE /events/:event_id/comments/:id(.:format) comments#destroy
        events GET    /events(.:format)                        events#index
               POST   /events(.:format)                        events#create
         event GET    /events/:id(.:format)                    events#show
               PATCH  /events/:id(.:format)                    events#update
               PUT    /events/:id(.:format)                    events#update
               DELETE /events/:id(.:format)                    events#destroy
```


## ER図

以下が今回登場するテーブルのER図です。Eventが親テーブルにあたり、Commentが子テーブルにあたります。

![ER.png](https://qiita-image-store.s3.amazonaws.com/0/140792/c3e45da3-370e-1c37-b7e4-e890854932cd.png)






# 親テーブルに対するAPIの作成

まずは、親テーブルにあたるEventモデルのCRUD APIを作成する手順について説明します。

## APIの作成

APIモードでプロジェクトを作成します。

```
$ rails new railsAPI-OneToMany --api
```

`scaffold`で親テーブルとなるEventモデルを作成します。

```
$ rails g scaffold Event title:string body:text
```

DBにテーブルを作成します。(Rails5からrakeコマンドがrailsに統合されています。)

```
$ rails db:migrate
```

DBに接続すると、`events`テーブルが作成されているのがわかります。

```
$ rails dbconsole

SQLite version 3.14.0 2016-07-26 15:17:14
Enter ".help" for usage hints.

sqlite> .tables
ar_internal_metadata  events                schema_migrations

sqlite> .schema events
CREATE TABLE "events" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar, "body" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL);
```

## テストデータの作成

`seeds.rb`にテストデータを作成します。

```db/seeds.rb
Event.create({title: 'event1 title', body: 'this is the body of event1'})
Event.create({title: 'event2 title', body: 'this is the body of event2'})
```

`seeds.rb`に作成したテストデータをDBに保存するには以下のコマンドで行います。

```
$ rails db:seed
```

## APIの検証

`rooutes`コマンドを確認すると、現時点では以下のようなルーティングになっているのがわかります。

```
$ rails routes
Prefix Verb   URI Pattern           Controller#Action
events GET    /events(.:format)     events#index
       POST   /events(.:format)     events#create
 event GET    /events/:id(.:format) events#show
       PATCH  /events/:id(.:format) events#update
       PUT    /events/:id(.:format) events#update
       DELETE /events/:id(.:format) events#destroy

```

サーバーを起動します。

```
$ rails s
```

http://localhost:3000/events にアクセスして、以下のような結果が表示されればOKです。

```
[
  {
    "id": 1,
    "title": "event1 title",
    "body": "this is the body of event1",
    "created_at": "2017-01-01T07:09:22.348Z",
    "updated_at": "2017-01-01T07:09:22.348Z"
  },
  {
    "id": 2,
    "title": "event2 title",
    "body": "this is the body of event2",
    "created_at": "2017-01-01T07:09:22.356Z",
    "updated_at": "2017-01-01T07:09:22.356Z"
  }
]
```

http://localhost:3000/events/1 にアクセスして`id`が`1`のEventのデータだけ返されればOKです。

```
{
  "id": 1,
  "title": "event1 title",
  "body": "this is the body of event1",
  "created_at": "2017-01-01T07:09:22.348Z",
  "updated_at": "2017-01-01T07:09:22.348Z"
}
```


# 子テーブルに対するAPIの作成

次に、子テーブルにあたるCommentモデルのCRUD APIを作成する手順について説明します。

## APIの作成

まずは先ほどと同様に`scaffold`で子要素となるCommentモデルを作成します

```
$ rails g scaffold Comment event:references body:text
```

DBにテーブルを作成します。

```
$ rails db:migrate
```

1:多の関係にするために親テーブルにあたるEventモデルを以下のように変更します。
これで、一つのEventに対して複数のCommentが紐づくようになります。

```diff::event.rb
class Event < ApplicationRecord
+ has_many :comments
end
```

親子関係のテーブルに対するAPIなので、ルーティングも入れ子になるようにroutes.rbを以下のように変更します。

```diff:config/routes.rb
Rails.application.routes.draw do
-  resources :comments
-  resources :events
+  resources :events do
+    resources :comments
+  end
end

```

Commentモデルの`comments_controller.rb`の編集をしていきます。
変更が必要になるメソッドは`index`、`create`、`set_user`です。
locationを削除する理由としては、削除しないと`NoMethodError: undefined method comment_url`というエラーが出るためです。


```diff

def create
-   @comment = Comment.new(comment_params)
+   @event = Event.where(:id => params[:event_id]).first
+   @comment = @event.comments.build(comment_params)
```

```diff

if @comment.save
-    render json: @comment, status: :created, location: @comment
+    render json: @comment, status: :created
```

```diff

def index
-   @comments = Comment.all
+   @event = Event.where(:id => params[:event_id]).first
+   @comments = @event.comments.all
  render json: @comments
end
```

```diff

def set_comment
-   @comment = Comment.find(params[:id])
+   @event = Event.where(:id => params[:event_id]).first
+   @comment = @event.comments.where(:id => params[:id]).first
end
```

## テストデータの作成

`seeds.rb`にテストデータを追加します。
 上半分は`id`が`1`の、下半分は`id`が`2`のEventに紐づくCommentのテストデータです。

```seeds.rb
Comment.create({body: 'comment1 body of Event1', event_id: 1})
Comment.create({body: 'comment2 body of Event1', event_id: 1})
Comment.create({body: 'comment3 body of Event2', event_id: 2})
Comment.create({body: 'comment4 body of Event2', event_id: 2})
```

`rails db:seed`を複数回実行することによって同じテストデータが入らないようにするためは、以下のように実行します。
はじめのコマンドで既存のテーブルに存在するデータをまっさらな状態になり、そのあとのコマンドでテストデータが作成されます。

```
$ rails db:migrate:reset
$ rails db:seed
```

## APIの検証

`rooutes`コマンドを確認すると、以下のようなルーティングになっているのがわかります。

```
$ rails routes
Prefix Verb   URI Pattern                              Controller#Action
event_comments GET    /events/:event_id/comments(.:format)     comments#index
       POST   /events/:event_id/comments(.:format)     comments#create
event_comment GET    /events/:event_id/comments/:id(.:format) comments#show
       PATCH  /events/:event_id/comments/:id(.:format) comments#update
       PUT    /events/:event_id/comments/:id(.:format) comments#update
       DELETE /events/:event_id/comments/:id(.:format) comments#destroy
events GET    /events(.:format)                        events#index
       POST   /events(.:format)                        events#create
 event GET    /events/:id(.:format)                    events#show
       PATCH  /events/:id(.:format)                    events#update
       PUT    /events/:id(.:format)                    events#update
       DELETE /events/:id(.:format)                    events#destroy
```

サーバーを起動します。

```
$ rails s
```


### Getメソッドの検証

以下のようにして、`id`が`1`のEventに紐づくCommentを取得できればOKです。

```
$ curl http://localhost:3000/events/1/comments/

[{"id":1,"event_id":1,"body":"comment1 body of Event1","created_at":"2017-01-01T07:09:22.392Z","updated_at":"2017-01-01T07:09:22.392Z"},{"id":2,"event_id":1,"body":"comment2 body of Event1","created_at":"2017-01-01T07:09:22.401Z","updated_at":"2017-01-01T07:09:22.401Z"}]%  
```



### POSTメソッドの検証

以下のようにして、新しいEventを作成できればOKです。

```
$ curl -v -H "Accept: application/json" -H "Content-type: application/json" POST -d '{"title":"event3 title", "body":"this is the body of event3"}' http://localhost:3000/events
```

以下のようにして、`id`が`1`のEventに紐づく新しいCommentを作成できればOKです。

```
$ curl -v -H "Accept: application/json" -H "Content-type: application/json" POST -d '{"body":"comment3 body of Event1"}' http://localhost:3000/events/1/comments
```

### DELETEメソッドの検証

以下のようにして、`id`が`3`のEventを削除できればOKです。

```
$ curl -X DELETE http://localhost:3000/events/3
```

以下のようにして、`id`が`1`のEventに紐づく`id`が`5`のCommnetを削除できればOKです。

```
$ curl -X DELETE http://localhost:3000/events/1/comments/5
```
### PUTメソッドの検証

以下のようにして、`id`が`2`のEventを編集できればOKです。

```
$ curl -X PUT -H 'Content-Type:application/json' -d '{ "event" : { "title": "Event2 title" } }' http://localhost:3000/events/2
```

## 検索機能の追加

上記の方法でCRUD操作を行うAPIを作成できました。
ここでは追加機能として、Eventをtitle名で検索するAPIを作成します。

まずはコントローラーに検索用のメソッドを作成します。

```events_controller.rb
def search
  @events = Event.find_by_title(params[:title])
  render json: @events
end
```

routes.rbを以下のように変えます。これで一番はじめに掲載したルーティングになります。

```diff:routes.rb
Rails.application.routes.draw do
  resources :events do
+    collection do
+     get :search
+    end
     resources :comments
  end
end
```

## APIの検証

URIを直接叩いて検証する場合は以下のようになります。
`event1 title`というタイトルのEventが返ってくればOKです。

`http://localhost:3000/events/search?title=event1 title` にアクセスして、以下のような結果が表示されればOKです。

```
{
  "id": 1,
  "title": "event1 title",
  "body": "this is the body of event1",
  "created_at": "2017-01-01T07:09:22.348Z",
  "updated_at": "2017-01-01T07:09:22.348Z"
}
```


curlなら以下のようにして確認します。

```
$ curl -X GET -d "title=event1 title" http://localhost:3000/events/search

{"id":1,"title":"event1 title","body":"this is the body of event1","created_at":"2017-01-01T07:09:22.348Z","updated_at":"2017-01-01T07:09:22.348Z"}%
```

## 参考

- [RailsのScaffoldでネストしたResourceを作る](http://sil.hatenablog.com/entry/rails-nested-resource-by-scaffold)
- [Railsでリソースを入れ子にする](http://qiita.com/weed/items/676e47e20bfe04f0e442)