この記事では, Amon2入門 (第1部)に引き続き, Amon2を利用した簡単なWebアプリケーション(スケジュール管理サービス)の開発を通して, Amon2を利用したWebアプリケーション開発の流れを体験していきます.
アプリケーションの概要は, 次のようにしましょう.
/
にGETメソッドでアクセスすると, スケジュールを登録するためのフォームがある(スケジュールの登録)- フォームは, スケジュールのタイトルと, その日付を入力するテキストフォームがある
- ここでは単純化の為, 日付はテキストフォームを利用して, 「2015/3/10」のように入力することとする
- さらに単純化のため, 誤った日付(例えば, 「2015/30/10」など)は入力されないものとする
- もちろん, 実際のアプリケーションではチェックして, エラーを出すべきです!
- 更に更に, 単純化の為, フォームは空欄で入力されないものとする
- しつこいですが, もちろん実際のアプリケーションではチェックして, エラーを出すべきです!
- フォームを送信すると,
/post
にPOSTメソッドで送信される- ここで
schedules
テーブルにスケジュールを登録する - 登録が終わったら,
/
にリダイレクトする
- ここで
/
にGETメソッドでアクセスすると, 新しいスケジュールが上になる形でスケジュールの一覧を見る事ができる(スケジュールの一覧表示)- スケジュールのタイトルと日付, そして「削除」ボタンを表示する
- 削除ボタンをクリックすると,
/scheudles/スケジュールID/delete
にPOSTメソッドでアクセスするようにする
/schedules/スケジュールID/delete
にPOSTメソッドでアクセスすると, そのスケジュールIDに該当するスケジュールをデータベースから削除する(スケジュールの削除)- スケジュールを削除した後は,
/
にリダイレクトする
- スケジュールを削除した後は,
アプリケーションの開発に入る前に, 少しTipsを.
$ carton exec -- plackup -Ilib -R ./lib --access-log /dev/null -p 5000 -a ./script/scheduler-server
アプリケーションの起動コマンドをこのように書き換えておくと, アプリケーションのファイルを変更した際に自動的にサーバを再起動(コードの読み込み直し)をしてくれます. コードを書き換える度にアプリケーションを停止して, 起動しなおすといった処理を回避出来るので, 非常に楽です.
さて, まずはこのアプリケーションで利用するデータベースのスキーマを考えていきます. なお, Amon2ではデフォルトではデータベースにSQLiteを利用するようになっているので, 今回もそのままSQLiteを利用します. 実際のアプリケーションでは, MySQLを利用することが多いです(Amon2でも, 比較的簡単にMySQLを利用するように変更することができます).
SQLiteが使うデータベースのスキーマは, sql/sqlite.sql
に設置されています.
CREATE TABLE IF NOT EXISTS member (
id INTEGER NOT NULL PRIMARY KEY,
name VARCHAR(255)
);
今回は, このようなスキーマを使って開発を進めていくことにします.
CREATE TABLE IF NOT EXISTS schedules (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
title VARCHAR(255),
date INTEGER
);
スケジュールのID(id
)の他に, スケジュールのタイトル(title
)と日付(date
)を持たせます.
なお, 単純化の為, date
についてはUNIX時間(epoch秒)で持たせることにします.
書き換えが終わったら, 早速このスキーマを利用して, SQLite用のデータベースファイルを生成します.
Amon2の場合, 開発環境(development)ではdb/development.db
というファイルを利用するので,
$ sqlite3 db/development.db < sql/sqlite.sql
このようにします.
...ちなみに余談ですが, データベースのスキーマを書き換えたい場合ですが, 面倒ですが一旦rm db/development.db
でデータベースファイルを削除してから, sqlite3 db/development.db < sql/sqlite.sql
で新しくデータベースファイルを生成しなおすのが何だかんだいって楽です.
もちろん本番環境でこんな事をしてしまうとデータが全て消えてしまうので, 本番環境でMySQLを使っている場合は, ALTER TABLE
などを使ってよしなに既存のデータベースを変更するようにします.
次に, Tengのスキーマを変更します. Amon2がデフォルトで利用するORMのTengを利用する為には, 「Teng Schema」の設定をしなければなりません. 自動的に生成する方法もありますが(その一例), 今回は手動で書き換えることにします.
Tengのスキーマファイルは, lib/Scheduler/DB/Schema.pm
です.
package Scheduler::DB::Schema;
use strict;
use warnings;
use utf8;
use Teng::Schema::Declare;
base_row_class 'Scheduler::DB::Row';
table {
name 'member';
pk 'id';
columns qw(id name);
};
1;
1つのテーブルに関する情報を, table { ... };
の中に書いていきます(もし, データベースに複数個のテーブルがあれば, その数だけtable { ... };
が並ぶことになります).
columns
でそのテーブルが持つカラム(列)の名前を, そしてpk
でprimary keyの設定をすることができます.
そのため, 先程のデータベーススキーマでは, 次のように書き換えることになります.
package Scheduler::DB::Schema;
use strict;
use warnings;
use utf8;
use Teng::Schema::Declare;
base_row_class 'Scheduler::DB::Row';
table {
name 'schedules';
pk 'id';
columns qw(id title date);
};
1;
これで, データベースに関連する準備は完了です.
まずは, スケジュールを登録出来るように, トップページのテンプレートを書き換えていきましょう.
: cascade "include/layout.tx"
: override content -> {
<h1 style="padding: 70px; text-align: center; font-size: 80px; line-height: 1; letter-spacing: -2px;">Scheduler</h1>
<hr />
<form class="form-inline" method="POST" action="<: uri_for('/post') :>">
<div class="form-group">
<label>タイトル</label>
<input type="text" class="form-control" name="title" placeholder="例: Perlの勉強">
</div>
<div class="form-group">
<label>日付</label>
<input type="text" class="form-control" name="date" placeholder="例: 2015/03/10">
</div>
<button type="submit" class="btn btn-default">登録</button>
</form>
: }
この段階で, localhost:5000
にアクセスしてみると, 次のように表示されるはずです.
ただ, フォームに適切な文字を入力して, 「登録」ボタンを押すと...
このように, 404 Not found
になってしまいます.
これは, 言うまでもありませんが, 「POSTメソッドで, /post
にアクセスした時の処理」を, まだ実装していないからです.
それでは, Controllerに手を加えていきましょう.
「第1部」最後の練習問題の, 1. を解答した段階でのDispatcher.pm
は, このようになっているはずです.
package Scheduler::Web::Dispatcher;
use strict;
use warnings;
use utf8;
use Amon2::Web::Dispatcher::RouterBoom;
get '/' => sub {
my ($c) = @_;
return $c->render('index.tx');
};
1;
今回は, ここからスタートしていきます.
まず, 「POSTメソッドで, /post
にアクセスした時の処理」を書けるように...
package Scheduler::Web::Dispatcher;
use strict;
use warnings;
use utf8;
use Amon2::Web::Dispatcher::RouterBoom;
get '/' => sub {
my ($c) = @_;
return $c->render('index.tx');
};
post '/post' => sub { ... };
1;
こうなりますね. そして具体的な処理を, サブルーチンリファレンスの中に書いていきます.
package Scheduler::Web::Dispatcher;
use strict;
use warnings;
use utf8;
use Amon2::Web::Dispatcher::RouterBoom;
use Time::Piece; # (1)
get '/' => sub {
my ($c) = @_;
return $c->render('index.tx');
};
post '/post' => sub {
my ($c) = @_;
my $title = $c->req->parameters->{title}; #
my $date = $c->req->parameters->{date}; # (2)
my $date_epoch = Time::Piece->strptime($date, '%Y/%m/%d')->epoch; # (3)
$c->db->insert(schedules => { #
title => $title, #
date => $date_epoch, # (4)
}); #
return $c->redirect('/');
};
1;
...今回の場合は, こうなるでしょうか. 少し複雑な(1)〜(4)の部分を, 1つずつ見ていきましょう.
(3)の部分で, 「2014/05/10」といった文字列をepoch秒に変換する為に, Time::Pieceというモジュールを読み込んでいます.
なお, Time::PieceはPerl 5.9.5からコアモジュール(Perlをインストールした際に, 最初から入っているモジュール)になっています.
$ corelist Time::Piece
Data for 2014-09-14
Time::Piece was first released with perl v5.9.5
my $title = $c->req->parameters->{title};
my $date = $c->req->parameters->{date};
$c->req
からパラメータを取得して, それぞれ$title
と$date
という変数に格納しています.
my $date_epoch = Time::Piece->strptime($date, '%Y/%m/%d')->epoch;
この辺りは, 「こんな感じでやる」... と思って下さい.
Time::Piece->strptime($date, $format)
で, $format
に従って$date
を解析してTime::Pieceのオブジェクトを生成し, そのオブジェクトから利用出来るepoch
メソッドを利用して, 元の文字列$date
からepoch秒を生成しています.
生成したepoch秒は, $date_epoch
という変数に代入して利用します.
$c->db->insert(schedules => {
title => $title,
date => $date_epoch,
});
$c->db
からTengのオブジェクトを呼び出し, insert
メソッドで書き込んでいます.
Tengのinsert
メソッドは, 第1引数にデータを書き込むテーブル名を, 第2引数にハッシュリファレンスで書き込みたいパラメータを与えます.
なので, この場合はschedules
というテーブルに, title
の列に$title
の中身を, date
の列に$date_epoch
の中身を書き込む, という処理になります.
それでは, 動作を確認してみましょう!
テキストフォームに適切な文字列を入力し, 「登録」ボタンを押すと, 先程のようにエラー画面が表示されず, そのまま/
へリダイレクトされると思います.
それでは引き続き, 登録したスケジュールを表示出来るようにしていきましょう.
まずは, Controller側から変更していきます.
package Scheduler::Web::Dispatcher;
use strict;
use warnings;
use utf8;
use Amon2::Web::Dispatcher::RouterBoom;
use Time::Piece;
get '/' => sub {
my ($c) = @_;
my @schedules = $c->db->search('schedules'); # (1)
return $c->render('index.tx', { schedules => \@schedules });
};
post '/post' => sub {
my ($c) = @_;
my $title = $c->req->parameters->{title};
my $date = $c->req->parameters->{date};
my $date_epoch = Time::Piece->strptime($date, '%Y/%m/%d')->epoch;
$c->db->insert(schedules => {
title => $title,
date => $date_epoch,
});
return $c->redirect('/');
};
1;
(1)の部分で, またもやTengを利用して, schedules
テーブルの一覧を取得しています.
Tengのsearch
メソッドは, 第1引数に検索対象となるテーブルの名前を, 第2引数に検索条件(ハッシュリファレンス)を, 第3引数に検索オプション(ハッシュリファレンス)を指定できますが, 第2引数を省略した場合はテーブルに格納されている全てのデータを取得してくれます.
これを, $c
のrender
メソッドでテンプレート側に渡して, 表示してもらう訳です.
それでは, テンプレート側を見てみましょう.
: cascade "include/layout.tx"
: override content -> {
<h1 style="padding: 70px; text-align: center; font-size: 80px; line-height: 1; letter-spacing: -2px;">Scheduler</h1>
<hr />
<form class="form-inline" method="POST" action="<: uri_for('/post') :>">
<div class="form-group">
<label>タイトル</label>
<input type="text" class="form-control" name="title" placeholder="例: Perlの勉強">
</div>
<div class="form-group">
<label>日付</label>
<input type="text" class="form-control" name="date" placeholder="例: 2015/03/10">
</div>
<button type="submit" class="btn btn-default">登録</button>
</form>
<hr />
: for $schedules -> $schedule {
<: $schedule.title :><br>
: }
: }
とりあえず, 手っ取り早くデータベースに書き込みが出来ているか確認するために, title
だけを表示するようにしてみました.
: for $schedules -> $schedule {
<: $schedule.title :><br>
: }
この部分ですが, Perlで例えればこんな感じになるでしょうか.
for my $scheudle (@{ $schedules }) {
print $schedule->title;
}
テンプレート内で, for $arrays -> $object { ... }
のように書くと, $arrays
に含まれる全要素について, 1つずつ$object
に代入した上で{ ... }
内の処理を繰り返し実行します.
$schedule
については, データベースに格納された1つの行がTengのRowオブジェクトとして格納されています.
Rowオブジェクトからは, それぞれの列の名前のメソッドが使えるようになっているので, 例えばControllerにおいて,
get '/' => sub {
my ($c) = @_;
my @schedules = $c->db->search('schedules');
for my $schedule (@schedules) {
print STDERR $schedule->title . "\n";
}
return $c->render('index.tx', { schedules => \@schedules });
};
のように書くと, /
にアクセスする度に, scheudler-server
を実行しているコンソール上に登録したスケジュールのタイトルがズラっと表示されるはずです(時間があれば, やってみてください).
というわけで, この時点で適当にスケジュールを登録してから, /
にアクセスすると...
このように, タイトルが表示されるようになっているはずです.
続いて, スケジュールの表示部分をテーブルにしつつ, 日付も表示するようにしてみます.
: cascade "include/layout.tx"
: override content -> {
<h1 style="padding: 70px; text-align: center; font-size: 80px; line-height: 1; letter-spacing: -2px;">Scheduler</h1>
<hr />
<form class="form-inline" method="POST" action="<: uri_for('/post') :>">
<div class="form-group">
<label>タイトル</label>
<input type="text" class="form-control" name="title" placeholder="例: Perlの勉強">
</div>
<div class="form-group">
<label>日付</label>
<input type="text" class="form-control" name="date" placeholder="例: 2015/03/10">
</div>
<button type="submit" class="btn btn-default">登録</button>
</form>
<hr />
<table class="table">
<thead>
<tr>
<th>タイトル</th>
<th>日時</th>
</tr>
</thead>
<tbody>
: for $schedules -> $schedule {
<tr>
<td><: $schedule.title :></td>
<td><: $schedule.date :></td>
</tr>
: }
</tbody>
</table>
: }
それでは, ブラウザで/
にアクセスしてみましょう.
...そういえば, スケジュールの日付はepoch秒で保存するようにしていました. これでは非常に見栄えが悪いので, 「XXXX年XX月XX日」のように表示するようにしたいです.
この解決策としては, いろいろな手段があります. Controllerレイヤーで変換してしまう, JavaScriptで変換してしまう... などなど.
今回は, Tengのinflate/deflateという仕組みを使って解決してみようと思います. この機能の詳細については, Perl Advent Calendar 2011 Teng Tracのinflate / deflateという記事が詳しいです.
一言で言えば, inflateは「Tengでデータベースからデータを引っ張ってくる時に, フィルタリングするモノ」, 逆にdeflateは「Tengでデータベースにデータを書き込む時に, フィルタリングするモノ」と思って下さい.
今回は, inflateの機能を使って, Tengを使ってschedules
テーブルからデータを引っ張ってくる際に, 「date」カラムのデータをepoch秒から(そのepoch秒を格納した)Time::Pieceのオブジェクトに変換します.
そして, テンプレート側で, Time::Pieceのstrftime
メソッドを利用して, 好きなフォーマットで表示します.
package Scheduler::DB::Schema;
use strict;
use warnings;
use utf8;
use Teng::Schema::Declare;
use Time::Piece; # (1)
base_row_class 'Scheduler::DB::Row';
table {
name 'schedules';
pk 'id';
columns qw(id title date);
inflate 'date' => sub { #
my $col_value = shift; # (2)
Time::Piece->strptime($col_value, '%s'); #
}; #
};
1;
まず, (2)で使っているTime::Pieceモジュールを(1)でuse
しています.
そして肝心の(2)の部分. inflateの書式は, inflate $column_name => sub { ... };
のように書きます.
$column_name
はinfalteの処理をしたいカラムの名前になるので, この場合, date
カラムのみsub { ... }
で指定した処理(フィルター)が行われます.
サブルーチンリファレンスの第1引数には, その指定したカラムのデータが入ってきます.
例えば, schedules
テーブルからあるデータを引っ張ってきた時, そのデータのdate
カラムの中身が1234567890
であれば, (2)のコードの$col_value
には, その1234567890
が入ります.
そして, そのサブルーチンリファレンスの返り値が, 新たに$column_name
のデータとして上書きされます(この場合は, date
カラムの中身が上書きされる).
ここでは, $col_value
にはepoch秒が入っているはずなので, それをTime::Pieceのstrptime
メソッドを利用してTime::Pieceのオブジェクトにして, それを返しています.
これによって, Controllerで
my $schedule = $c->db->single('schedules'); # `single`は, データベースから1つの要素を取得するメソッドです.
print $schedule->date->strftime("%Y/%m/%d"); # => 2015/03/05 (例)
のように, $schedule->date
の中身が, epoch秒ではなくそのepoch秒のTime::Pieceオブジェクトになる, という訳です.
: for $schedules -> $schedule {
<tr>
<td><: $schedule.title :></td>
<td><: $schedule.date.strftime("%Y/%m/%d") :></td>
</tr>
: }
テンプレートは, このように書き換えます.
テンプレートにおいて, メソッド呼び出しの->
は.
で表現するので, 先程のTime::Pieceオブジェクトから任意のフォーマットの日付の文字列を生成するには, このように書けば良いです.
これらの変更を実装すると, /
にアクセスした時の表示は, 次のようになるはずです.
これで, 冒頭で定めたアプリケーション概要のうち, 「スケジュールの登録」と「スケジュールの一覧表示」を達成することが出来ました. 続いて, 「スケジュールの削除」に挑戦していきましょう!
それでは最後に, スケジュールを削除する機能を実装していきます.
まず, 削除ボタンを表示するようにテンプレートを書き換えましょう.
<thead>
<tr>
<th>タイトル</th>
<th>日時</th>
<th></th>
</tr>
</thead>
<tbody>
: for $schedules -> $schedule {
<tr>
<td><: $schedule.title :></td>
<td><: $schedule.date.strftime("%Y/%m/%d") :></td>
<td>
<form method="POST" action="<: uri_for('/schedules/'~$schedule.id~'/delete') :>">
<button type="submit" class="btn btn-danger">削除</button>
</form>
</td>
</tr>
: }
</tbody>
すると, このような形で削除ボタンが表示されます.
ここで唐突に出てきた, $schedule.id($schedule->id)
について解説しておきます.
このid
ですが, 最初にデータベーススキーマを設定した際には, schedules
テーブルの中のカラムとして存在しました.
CREATE TABLE IF NOT EXISTS schedules (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
title VARCHAR(255),
date INTEGER
);
しかし, Controllerでschedules
テーブルにデータを挿入する際には, id
を指定していませんでした.
$c->db->insert(schedules => {
title => $title,
date => $date_epoch,
});
しかし実際には, schedules
テーブルにデータを挿入するタイミングで, 適切な値がid
として格納されています.
これは, SQLiteのPRIMARY KEY
によるものです.
PRIMARY KEY
, 主キーは1テーブルにつき1カラムのみ設定することができ, データ型は大抵INTEGER
などを数値を格納出来る型にします.
なお, PRIMARY KEY
を設定した場合, 自動的にNOT NULL
制約を与えた場合と同じように, 重複した値が許されなくなります.
SQLiteの場合, データ型がINTEGER
のカラムに対してPRIMARY KEY
を設定すると, データ挿入時, そのカラムに格納する値が空だった場合のみ自動的に重複しない値が挿入されるようになっています.
なお, 今回はSQLiteを利用していますが, MySQLを利用している場合は, このような挙動をさせたい場合はPRIMARY KEY
に加えてAUTOINCREMENT
を指定しなければなりません.
さて, もしPRIMARY KEY
がない場合, 削除をする際にはtitle
やdate
で条件をかける必要がありますし, そもそもtitle
とdate
が同じカラムがあった場合, どちらを消していいのかわからなくなる, という場合も出てくるでしょう.
しかし, このようにPRIMARY KEY
としてid
を生成しておけば, 全てのスケジュールにユニークなIDを割り振ることが出来ます.
そのため, ここまで見てきたようにスケジュールの削除を(id
の指定のみで)簡単に実装することができるようになります.
...この辺りの詳細については割愛しますが, id
というカラムの有無で実装の難しさが変わってくる, という所は感じて頂けたと思います.
データベーススキーマの設計と, Webアプリケーションの実装は密接に関連していて, データベーススキーマをうまく設計出来ないと, Webアプリケーションの実装も難しくなる, という所は覚えておいて下さい.
続いて, Controllerを書き換えていきます.
ここで悩むのは, パスをどのように指定すればよいか? という所でしょう.
ここまで, 例えばpost '/post' => sub { ... };
のようにControllerに書いてきましたが, 今回のパス(左の例の場合, '/post'
)は可変です.
つまり, それぞれのスケジュールが持っているIDに応じて, /schedules/1/delete
とか/schedules/5/delete
のように, 複数のパスが存在する訳です.
こういう場合は, パスをこのように書けば解決できます.
post '/schedules/:id/delete' => sub {
my ($c, $args) = @_;
my $id = $args->{id};
...
};
パスの中に, :id
のように:
で始まる文字列を入れると, その部分は「全てのパターンにマッチ」するようになります.
そして, そのマッチした文字列は, サブルーチンリファレンスの第2引数にハッシュリファレンスの形で渡ってきます.
なので, 上記のコードは, 例えば/schedules/1/delete
にアクセスした場合は$id
の中身が1
に, /schedules/5/delete
にアクセスした場合は$id
の中身が5
になります.
後は, Tengのdelete
メソッドを利用して該当するIDのスケジュールを消してから, '/'
にリダイレクトすればOKです.
post '/schedules/:id/delete' => sub {
my ($c, $args) = @_;
my $id = $args->{id};
$c->db->delete('schedules' => { id => $id });
return $c->redirect('/');
};
それでは, 動作確認をしてみましょう.
ここから, 「テスト2」の削除ボタンを押すと...
「テスト2」が正しく消えましたね!
さて, これでアプリケーション概要で定めた全ての機能を実装することが出来ました. ...が, 複数のスケジュールを登録してみた時, 違和感を感じませんか?
日時の順番がバラバラになっていますね. これを, 「最新のスケジュールが上になる」ように変更して, Amon2チュートリアルを終わりにしたいと思います.
Tengのsearch
メソッドは, 第3引数にオプションを指定できると説明しました.
このオプションで, 取得するデータを並び替えるORDER BY
を, date
カラムについて適用するようにします.
get '/' => sub {
my ($c) = @_;
my @schedules = $c->db->search('schedules', {}, { order_by => 'date DESC'});
return $c->render('index.tx', { schedules => \@schedules });
};
第2引数, 検索条件はなし(全て取得する)なので, {}
のように空のハッシュリファレンスを渡しています.
そして第3引数で, { order_by => 'date DESC' }
を指定しています.
Tengは, これらの条件やオプションなどを元に, 自動的にSQLのクエリを組み立て, SQLiteやMySQLに対して実行してくれるのです.
さて, このように変更すると, '/'
にアクセスした際の結果はどうなるでしょうか?
...新しいスケジュールが上に表示されるようになりました!
GETメソッドで'/'
にアクセスした時はこれまで通り「新しいスケジュールが上」になるように表示しつつ, GETメソッドでクエリパラメータ(クエリ文字列)を使って'/?order=reverse'
でアクセスした時は, 「新しいスケジュールが下」になるように書き換えてみよう.
Controllerにおいて, GETメソッドで'/?order=reverse'
にアクセスした際のorder=reverse
のようなパラメータは, 次のようにして取得することが出来ます.
get '/' => sub {
my ($c) = @_;
my $order = $c->req->parameters->{order};
...
};
スケジュールの日付が今日だった場合, そのスケジュールのタイトルを赤色に変更するようにしてみよう.
今日の日付については, Time::Piece
をuseしている場合, localtime
というサブルーチンで今現在の時間のTime::Pieceオブジェクトを取得することができるので, ここから導くと良いでしょう(strftime
メソッドで, 適切なフォーマットで文字列を生成しましょう).
スケジュールを投稿する際, ControllerでTime::Pieceのオブジェクトからepoch秒を生成しています. これを, Tengのdeflateの機能を使って実装するように書き換えてみましょう.
うまく実装できれば, Controllerのスケジュール投稿に関するコードは, 次のように書き換えても正しく動くはずです.
post '/post' => sub {
my ($c) = @_;
$c->db->insert(schedules => {
title => $c->req->parameters->{title},
date => $c->req->parameters->{date},
});
return $c->redirect('/');
};
チャレンジ課題は必須ではありません. 少し難易度が高いので「チャレンジ問題」にしていますが, 時間があれば是非挑戦していただきたいです!
チャレンジ課題は, トップページに表示されている各スケジュールに「編集」ボタンを設置して, 任意のスケジュールのタイトルや日付を変更できる機能の追加です.
スケジュール編集をするためのテンプレートを作成するなど, 作業量は少なくありませんが, この問題をクリア出来れば「Amon2で作成されたWebアプリケーションに, 自力で機能を1つ追加できた」という経験が出来ます!