├── README.md ├── amon2 ├── 1.md ├── 2.md └── img │ ├── 1-1.png │ ├── 2-1.png │ ├── 2-10.png │ ├── 2-2.png │ ├── 2-3.png │ ├── 2-4.png │ ├── 2-5.png │ ├── 2-6.png │ ├── 2-7.png │ ├── 2-8.png │ └── 2-9.png ├── author.md ├── ci ├── wercker.md └── wercker │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 14.png │ ├── 15.png │ ├── 16.png │ ├── 17.png │ ├── 18.png │ ├── 19.png │ ├── 2.png │ ├── 20.png │ ├── 21.png │ ├── 22.png │ ├── 23.png │ ├── 24.png │ ├── 25.png │ ├── 26.png │ ├── 27.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── examples └── react-todo │ ├── README.md │ └── index.html ├── exception.md ├── git.md ├── git ├── 1.png └── 2.png ├── homebrew.md ├── infrastructure-as-code ├── ansible.md ├── img │ ├── vagrant-1.png │ ├── vagrant-2.png │ ├── vagrant-3.png │ ├── vagrant-4.png │ ├── vagrant-5.png │ ├── vagrant-6.png │ ├── vagrant-7.png │ └── vagrant-8.png ├── serverspec.md └── vagrant.md ├── oop.md ├── orm.md ├── reactjs.md ├── reactjs ├── 1.png ├── 2.png └── 3.png ├── test.md └── webapp-test.md /README.md: -------------------------------------------------------------------------------- 1 | # Perl入学式の教科書 2 | 3 | Perl入学式卒業後の学習に, また企業や研究室の研修の資料などとしてご活用下さい. 4 | 5 | ## ライセンス 6 | このリポジトリ内にある資料のライセンスについては, Perl入学式公式サイトの[License](http://www.perl-entrance.org/license.html)ページに記載されているライセンスに準じます. 7 | 8 | ## 目次 9 | - Perlチュートリアル [(2017年度 Perl入学式資料)](http://www.perl-entrance.org/handout.html#handout-2017) 10 | - [Perl入学式 第1回 : 環境構築編](https://github.com/perl-entrance-org/workshop-2017/blob/master/1st/part1.md) 11 | - [Perl入学式 第1回 : Shell入門](https://github.com/perl-entrance-org/workshop-2017/blob/master/1st/part2.md) 12 | - [Perl入学式 第1回 : Hello, world!](https://github.com/perl-entrance-org/workshop-2017/blob/master/1st/part3.md) 13 | - [Perl入学式 第2回](https://github.com/perl-entrance-org/workshop-2017/blob/master/2nd/slide.md) 14 | - [Perl入学式 第3回](https://github.com/perl-entrance-org/workshop-2017/blob/master/3rd/slide.md) 15 | - [Perl入学式 第4回](https://github.com/perl-entrance-org/workshop-2017/blob/master/4th/slide.md) 16 | - [Perl入学式 第5回](https://github.com/perl-entrance-org/workshop-2017/blob/master/5th/slide.md) 17 | - [Amon2入門 (第1部)](/amon2/1.md) 18 | - [Amon2入門 (第2部)](/amon2/2.md) 19 | - [Homebrewによる環境構築](/homebrew.md) (Mac OS Xユーザ向けの資料です) 20 | - Infrastructure as Code 21 | - [Vagrant](/infrastructure-as-code/vagrant.md) 22 | - [Ansible](/infrastructure-as-code/ansible.md) 23 | - [Serverspec](/infrastructure-as-code/serverspec.md) 24 | - [Git入門](/git.md) 25 | - Bitbucket入門 26 | - Github入門 27 | - [OOP入門](/oop.md) 28 | - [Perlテスト入門](/test.md) 29 | - [ORM入門(Teng)](/orm.md) 30 | - [CI入門(Wercker)](/ci/wercker.md) 31 | - [例外基礎](exception.md) 32 | - バリデーション基礎 33 | - [WebAppテスト入門](webapp-test.md) 34 | - [React.js入門](reactjs.md) 35 | 36 | ## Author / Contributor 37 | 「Perl入学式の教科書」に掲載されている各資料の執筆者と貢献者の一覧は[こちら](/author.md)にまとめています. 38 | 39 | ## Issue / Pull Requestについて 40 | このリポジトリに対するIssueやPull Requestについては, 日本語でコメントを書いて頂いて構いません. 41 | また, コミットログに関しても日本語で記載して頂いて構いません. 42 | -------------------------------------------------------------------------------- /amon2/1.md: -------------------------------------------------------------------------------- 1 | # Amon2入門 (第1部) 2 | 3 | PerlのWebアプリケーションフレームワーク(Web Application Framework, 以降「WAF」と省略)の一種「Amon2」の使い方について紹介します. 4 | このチュートリアルは二部構成になっています. 5 | 第1部となるこの記事では, Amon2のインストールや初期設定, そして基本的な機能の解説を行います. 6 | 次の第2部では, 第1部で解説したAmon2の基本的な機能を使って, 簡単なスケジュール管理サービスを作成しながら, Amon2を使ったWebアプリケーションの開発の流れを体験してみます. 7 | 8 | ## Amon2の特徴 9 | 10 | Amon2は, [@tokuhirom](https://twitter.com/tokuhirom)さんを中心に開発されている, Perl製のWAFです. 11 | 様々な言語の様々なWAFの中で, Amon2がどのようなポジションにいるのかを見てみる為に, Amon2を含むPerlの代表的なWAFを中心に, その「重さ(機能の充実性)」で分類し, 表にしてみました. 12 | (ただしこの表は, だいぶ @papix の主観がはいっています...) 13 | 14 | |クラス |具体的なWAF |概要 | 15 | |:------|:---------------------------------------------------|:--------------------------------------| 16 | |重量級 | Ruby on Rails (Ruby) | 単体で完結できる, フルスタックなWAF | 17 | |中量級 | Mojolicious(Perl), Catalyst (Perl), FuelPHP (PHP) | RoR程ではないが高機能なWAF | 18 | |軽量級 | Amon2 (Perl) | WAFとして非常にシンプル, その分拡張性に富む | 19 | |超軽量級| Sinatra (Ruby), Amon2::Lite (Perl) | ページ数が少ない(2〜3)アプリケーション向け | 20 | 21 | 上の表にもあるように, Amon2は, WAFの中ではかなり軽量な分類に入ります. 22 | Ruby on Railsのような重量級, 或いはPerlのMojoliciousやPHPのFuelPHPのような中量級のWAFに比べて, Amon2そのものが備えている機能は少ないです. 23 | シンプルではありますが, WAFとして必要最低限の機能は備えていますし, 軽量ゆえに拡張性があり, プロダクトに最適なWAFを構築することが出来ます. 24 | そういう意味で, Amon2は「WAFのフレームワーク」と言うことが出来るかもしれません. 25 | 26 | このチュートリアルでは, Amon2の基本的な機能を利用しながら, 簡単なスケジュール管理システムを作っていきます. 27 | 時間の制約上, 「WAF(Amon2)を拡張していく」という部分については割愛し, 別の機会に紹介したいと思います. 28 | 29 | チュートリアルに利用するAmon2は2015年3月10日時点の最新版であるAmon2 6.11を利用します. 30 | Amon2は基本的には後方互換性を維持しながら開発が進んでいるので, 6.11以降のAmon2でも動作すると思います. 31 | 32 | ### コラム: WAFの選定について 33 | 34 | 先程の表にもあったように, Amon2に比べて, Ruby on Railsは様々な機能を自前で持っているフルスタックなWAFと言えます. 35 | そのため, 「常にRuby on Railsを使えばいいのでは?」という意見もあるかと思いますが, 個人的にはWAFは適材適所で使い分けるべきだと思っています. 36 | 37 | 例えば, Ruby on Railsは高性能な代わりに, その進化は非常に早いです(進化の速さについては, Rubyという言語そのものにも当てはまるでしょう). 38 | 常に手を入れ続けるプロダクトであれば, その進化に対応しながら, 新しく追加された機能を使いつつ, 効率的に開発を進めることが出来るでしょう. 39 | 40 | しかし社内ツールであったり, 或いはプロダクトやエンジニアチームの支援をするようなツール(社内向けプロダクト)であれば, 一旦開発をしきった後はそこまで手を加えずに, 長く使うという場合もあると思います. 41 | そういう場合は, 言語そのものやライブラリの後方互換性を意識(重視)したPerl製のWAFを採用した方が, セキュリティフィックスなどによる言語/モジュールの更新を実施しやすい... という見方も出来るでしょう. 42 | 43 | ...このように, WAF1つをとっても様々な選択肢が可能で, チームやプロダクトにとっての「適切な技術選択」というのは非常に難しいものです. 44 | この研修の中でも, きっと様々な「技術選択」の機会があると思いますので, その都度その都度, しっかり考えていきましょう. 45 | 46 | ## Amon2のセットアップ 47 | 48 | Perlチュートリアルの[Perl環境の構築](https://github.com/perl-entrance-org/workshop-2014-01/blob/master/build_perl.md)内の, [モジュールとCPAN](https://github.com/perl-entrance-org/workshop-2014-01/blob/master/build_perl.md#%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%A8cpan)という部分で, Perlのモジュールをインストールするための`cpanm`というコマンドをインストールしているはずです. 49 | 50 | ``` 51 | $ which cpanm 52 | /Users/username/.plenv/shims/cpanm 53 | ``` 54 | 55 | Amon2も[CPANに公開されている](http://search.cpan.org/~tokuhirom/Amon2-6.11/lib/Amon2.pm)ので, `cpanm`コマンドからインストールすることが出来ます. 56 | 57 | ``` 58 | $ cpanm Amon2 59 | --> Working on Amon2 60 | Fetching http://www.cpan.org/authors/id/T/TO/TOKUHIROM/Amon2-6.11.tar.gz ... OK 61 | Configuring Amon2-6.11 ... OK 62 | Building and testing Amon2-6.11 ... OK 63 | Successfully installed Amon2-6.11 64 | 1 distribution installed 65 | ``` 66 | 67 | もしAmon2の依存モジュールが導入されていない場合, `cpanm`は自動的にそれらのインストールも実行します. 68 | 69 | ## 雛形生成 70 | 71 | Amon2を使ったWebアプリケーションの雛形は, `amon2-setup.pl`コマンドから生成出来ます. 72 | 73 | ``` 74 | $ amon2-setup.pl 75 | % amon2-setup.pl MyApp 76 | 77 | --flavor=Basic basic flavour (default) 78 | --flavor=Lite Amon2::Lite flavour (need to install) 79 | --flavor=Minimum minimalistic flavour for benchmarking 80 | --flavor=Standalone CPAN uploadable web application(EXPERIMENTAL) 81 | 82 | --vc=Git setup the git repository (default) 83 | 84 | --list-flavors (or -l) Shows the list of flavors installed 85 | 86 | --help Show this help 87 | ``` 88 | 89 | Amon2には, Sinatraのような超軽量の`Amon2::Lite`を利用する, Liteフレーバー(雛形の元)も用意されていますが, 今回はBasicフレーバーを利用します. 90 | Amon2で中規模〜大規模なWebアプリケーションを開発する場合, Basicフレーバーで雛形を生成し, 必要に応じて改変していく... という流れが多いです. 91 | 92 | それでは早速, 雛形を生成していきましょう. 93 | `amon2-setup.pl`を利用した雛形生成時には, このWebアプリケーションの名前を引数として与える必要があります. 94 | 今回は, スケジュール管理のアプリケーションなので, 「Scheduler」という名前にしましょうか. 95 | 96 | ``` 97 | $ amon2-setup.pl Scheduler 98 | -- Running flavor: Basic -- 99 | [main] Loading asset: jQuery 100 | [main] Loading asset: Bootstrap 101 | [main] Loading asset: ES5Shim 102 | [main] Loading asset: MicroTemplateJS 103 | [main] Loading asset: StrftimeJS 104 | ... 中略 ... 105 | create mode 100644 tmpl/include/layout.tx 106 | create mode 100644 tmpl/include/pager.tx 107 | create mode 100644 tmpl/index.tx 108 | create mode 100644 xt/01_pod.t 109 | create mode 100644 xt/02_perlcritic.t 110 | -------------------------------------------------------------- 111 | 112 | Setup script was done! You are ready to run the skelton. 113 | 114 | You need to install the dependencies by: 115 | 116 | > carton install 117 | 118 | And then, run your application server: 119 | 120 | > carton exec perl -Ilib script/scheduler-server 121 | 122 | -------------------------------------------------------------- 123 | ``` 124 | 125 | カレントディレクトリに`Scheduler`というディテクトリが生成され, その中に雛形が生成されました. 126 | 127 | ## Carton 128 | 129 | 雛形の中身を詳しく見る前に, Cartonを使って依存モジュールをインストールしておきましょう. 130 | 131 | Cartonは, RubyのBundlerのようなもので, Webアプリケーションの依存モジュールを, `cpanm`コマンドでインストールしたモジュールとは別に管理してくれるアプリケーションです. 132 | Cartonを利用すれば, Webアプリケーションが動作する際に利用するライブラリとそのバージョンを固定することが出来るので, 開発環境と本番環境でライブラリのバージョンに差異が生じるなどといった事態を防ぐことができます. 133 | 134 | ``` 135 | $ cpanm Carton 136 | ``` 137 | 138 | まず, `cpanm`コマンドでCartonをインストールします. 139 | Amon2の時と同様, Cartonの依存モジュールも自動的にインストールしてくれます. 140 | 141 | ``` 142 | $ cd Scheduler 143 | $ carton install 144 | ``` 145 | 146 | Cartonのインストールが終わったら, `amon2-setup.pl`が生成した雛形が格納されている`Scheduler`ディレクトリに移動し, `carton install`コマンドで依存モジュールをインストールします. 147 | 自分の環境では, 次のような出力が得られました. 148 | 149 | ``` 150 | $ carton install 151 | Installing modules using /Users/username/Scheduler/cpanfile 152 | Successfully installed CPAN-Meta-2.150001 (upgraded from 2.140640) 153 | Successfully installed Module-Build-0.4211 (upgraded from 0.4205) 154 | Successfully installed URI-1.67 155 | ... 中略 ... 156 | Successfully installed Test-LongString-0.17 157 | Successfully installed Test-WWW-Mechanize-1.44 158 | Successfully installed Test-WWW-Mechanize-PSGI-0.35 159 | 115 distributions installed 160 | Complete! Modules were installed into /Users/username/Scheduler/local 161 | ``` 162 | 163 | Cartonは, アプリケーションのルートディレクトリ(今回の場合, `/Users/username/Scheduler`)の直下に, `local`というディレクトリを生成し, この中に依存モジュールをインストールします. 164 | 165 | `carton install`でインストールしたモジュールを利用しながら, Perlのスクリプトを動かすには, `carton exec`を利用します. 166 | `carton exec -- `の後に, 例えば`perl hello.pl`など, `任意のPerlスクリプトを動かすコマンドを入力すると, そのPerlスクリプトは`local`ディレクトリ以下のモジュールを利用しながら実行してくれます. 167 | 168 | 「Perlスクリプトを動かすコマンド」ですが, このSchedulerの場合, `amon2-setup.pl`で雛形を生成した際に`script/scheduler-server`という起動用スクリプトが生成されているので, これを使います. 169 | 170 | ``` 171 | $ carton exec -- perl -Ilib script/scheduler-server 172 | Scheduler: http://127.0.0.1:5000/ 173 | ``` 174 | 175 | ブラウザで, `localhost:5000`に繋いでみて下さい. 176 | 177 | ![](/amon2/img/1-1.png) 178 | 179 | ...このような画面が確認出来ましたか? 180 | 出来たのであれば, Amon2を使ったWebアプリケーションの開発の, 第一歩を踏み出すことが出来ました! 181 | 182 | ## Amon2の構成 183 | 184 | 動作確認が出来たところで, Amon2の構成について学んでいきます. 185 | Amon2は, Webアプリケーションについて考える時によく出てくる「MVC」, すなわちModel, View, Controllerのうち, ViewとControllerを提供するWAFです. 186 | Modelに関しては, 「それぞれが使いやすい形で実装せよ」というのが, Amon2の方針になっています. 187 | 188 | ### Controller 189 | 190 | まずはControllerの部分を見て行きましょう. 191 | SchedulerアプリケーションのControllerは, `lib/Scheduler/Web/Dispatcher.pm`の部分になります. 192 | 193 | ```perl:lib/Scheduler/Web/Dispatcher.pm 194 | package Scheduler::Web::Dispatcher; 195 | use strict; 196 | use warnings; 197 | use utf8; 198 | use Amon2::Web::Dispatcher::RouterBoom; 199 | 200 | any '/' => sub { 201 | my ($c) = @_; 202 | my $counter = $c->session->get('counter') || 0; 203 | $counter++; 204 | $c->session->set('counter' => $counter); 205 | return $c->render('index.tx', { 206 | counter => $counter, 207 | }); 208 | }; 209 | 210 | post '/reset_counter' => sub { 211 | my $c = shift; 212 | $c->session->remove('counter'); 213 | return $c->redirect('/'); 214 | }; 215 | 216 | post '/account/logout' => sub { 217 | my ($c) = @_; 218 | $c->session->expire(); 219 | return $c->redirect('/'); 220 | }; 221 | 222 | 1; 223 | ``` 224 | 225 | このコードでは, `Amon2::Web::Dispatcher::RouterBoom`というモジュールが提供する`any`や`post`というメソッドを利用して, 「あるメソッドで」, 「あるパスに接続したら」, 「ある処理をする」という組み合わせを記述しています. 226 | 227 | ```perl 228 | any '/' => sub { 229 | my ($c) = @_; 230 | my $counter = $c->session->get('counter') || 0; 231 | $counter++; 232 | $c->session->set('counter' => $counter); 233 | return $c->render('index.tx', { 234 | counter => $counter, 235 | }); 236 | }; 237 | ``` 238 | 239 | この部分を中心に見ていきましょう. 240 | そもそも, 「これ, 正しい記法なの?」と思うかもしれませんが, こう書き換えてみるとどうでしょう? 241 | 242 | ```perl 243 | any ('/' => sub { ... }); 244 | ``` 245 | 246 | `any`というメソッドに, `'/'`と, `sub { ... }`という引数を渡している訳です. 247 | ちなみに, `sub { ... }`の部分は, サブルーチンのリファレンスになっています. 248 | 249 | さて, 先程このコードでは「あるメソッドで」, 「あるパスに接続したら」, 「ある処理をする」という組み合わせを書いている, と書きました. 250 | これをこのコードに当てはめると... 251 | 252 | - あるメソッド : `any` (anyは, HTTPのメソッドのうち, GETとPOSTの両方) 253 | - あるパス : `'/'` 254 | - ある処理 : `sub { ... }` 255 | 256 | となります. 257 | なお, メソッドについては, ここで紹介した`any`の他にGETメソッドを利用する時の`get`, POSTメソッドを利用する時の`post`を利用することができます. 258 | 259 | 例えば... 260 | 261 | ```perl 262 | get '/hoge/fuga' => sub { ... }; 263 | ``` 264 | 265 | このように書いた場合, サブルーチンリファレンスに書かれた処理はGETメソッドで`/hoge/fuga`にアクセスした際に実行されます. また, 266 | 267 | ```perl 268 | post '/hoge/fuga/new' => sub { ... }; 269 | ``` 270 | 271 | このように書いた場合, サブルーチンリファレンスに書かれた処理はPOSTメソッドで`/hoge/fuga/new`にアクセスした際に実行されます. 272 | 273 | #### Context 274 | 275 | それでは次に, サブルーチンリファレンスの中身について詳しく見て行きましょう. 276 | 277 | ```perl 278 | sub { 279 | my ($c) = @_; 280 | my $counter = $c->session->get('counter') || 0; 281 | $counter++; 282 | $c->session->set('counter' => $counter); 283 | return $c->render('index.tx', { 284 | counter => $counter, 285 | }); 286 | }; 287 | ``` 288 | 289 | ここで重要なのは, `$c`で受けている「コンテキスト」です. 290 | コンテキストの重要性については, Amon2の作者であるtokuhiromさんもPerl Hackers Hubでの[「Amon2によるWebアプリケーション開発の高速開発(2)」](http://gihyo.jp/dev/serial/01/perl-hackers-hub/001802)において, 「Amon2の世界では,コンテキストというものの存在が非常に重要です。コンテキストを理解すればAmon2の80%を理解したと言ってもよいでしょう。」と述べています. 291 | 292 | なぜコンテキストが重要かと言うと, Amon2を利用したWebアプリケーションは, この`$c`から利用できるメソッドを利用して組み立てていく必要があるからです. 293 | ここでは, その一部をここで紹介します(なお, ここで紹介するメソッドはBasicフレーバーで雛形を生成した際に利用出来るものの一部です. Amon2を拡張した結果, 利用できなくなったメソッドや, 新しく使えるようになったメソッドがある場合もあります). 294 | 295 | #### `$c`から利用出来るメソッド 296 | 297 | ##### `$c->req` 298 | 299 | リクエストに関する情報を取得することができます. 300 | よく使うのは, フォームに入力したパラメータをWebアプリケーション側で取得する時です. 301 | 302 | ```html 303 |
304 | 305 | 306 |
307 | ``` 308 | 309 | このようなHTMLがあった時, Controllerにおいて 310 | 311 | ```perl 312 | post '/post' => sub { 313 | my ($c) = @_; 314 | my $name = $c->req->parameters->{name}; 315 | ... 316 | }; 317 | ``` 318 | 319 | のように書くと, `$name`の中には, テキストフォームに入力した文字列が入ります. 320 | 321 | ちなみに, `$c->req->param('name')`でも同じように取得することが出来ますが, このような書き方は脆弱性を招く可能性があるので使わないようにしましょう: [Perl 初心者がウェブアプリケーションを書く時に気をつけるべきこと](http://blog.64p.org/entry/2014/09/04/125301) 322 | 323 | ##### `$c->render($template, $parameters)` 324 | 325 | レスポンスを生成します. 326 | `render`については, Viewの機能と密接に繋がる部分が多いので, 詳しくは後述します. 327 | 328 | ##### `$c->redirect($path)` 329 | 330 | `$path`で指定した別のページへ遷移します(リダイレクト). 331 | `$c->render()`も同様ですが, これらのメソッドは「実行したタイミングで処理される」のではなく, 「このメソッドの返り値をサブルーチンリファレンスの返り値にした場合に適用される」ものです. 332 | 333 | そのため, 次のように書くと期待通りの動作をしてくれません. 334 | 335 | ```perl 336 | sub { 337 | my ($c) = @_; 338 | 339 | ... 340 | 341 | $c->redirect('/'); # '/'に遷移したいが... 342 | print "ok!"; # サブルーチンリファレンスの返り値は`print "ok!"`の実行結果, すなわち`1`になるので, リダイレクトしない! 343 | }; 344 | ``` 345 | 346 | ##### `$c->db` 347 | 348 | [Teng](https://metacpan.org/pod/Teng)と呼ばれる[O/R Mapper(ORM)](http://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E9%96%A2%E4%BF%82%E3%83%9E%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0)を利用して, DBを操作します(正確に言えば, `$c->db`でTengのオブジェクトを取得することが出来るので, これを元にしてDBを操作することができます). 349 | Tengの使い方についての詳細は割愛しますが, Perl Advent Calendar Japan 2011の[Teng Trac](http://perl-users.jp/articles/advent-calendar/2011/teng/)が詳しいです. 350 | 351 | 例えば, Tengの`search`メソッドを使って, `schedules`テーブルの全てのレコードを取得するには, 次のように書きます. 352 | 353 | ```perl 354 | sub { 355 | my ($c) = @_; 356 | ... 357 | my @schedules = $c->db->search('schedules'); 358 | ... 359 | }; 360 | ``` 361 | 362 | ### View 363 | 364 | これまでControllerにあたる部分のコードを見てきました. 続いてViewについて見て行きましょう. 365 | 366 | #### ControllerとView 367 | 368 | Webアプリケーションは, ユーザ(のブラウザ)からリクエストがあると, それを処理して, HTMLをレンダリングして返します. 369 | この辺りの処理を担うのが, 「View」の役割です. 370 | 大雑把な説明になりますが, Viewは「Controllerからもらったパラメータを使って, HTMLを適切に描画する」部分を担っている, と思えば良いでしょう. 371 | 372 | ```perl 373 | sub { 374 | my ($c) = @_; 375 | my $counter = $c->session->get('counter') || 0; 376 | $counter++; 377 | $c->session->set('counter' => $counter); 378 | return $c->render('index.tx', { 379 | counter => $counter, 380 | }); 381 | }; 382 | ``` 383 | 384 | これまで見てきたControllerのサブルーチンリファレンスの中において, `$c->render`の部分でHTMLをレンダリングしています. 385 | この`render`メソッドは, 2つの引数を持ちます. 第1引数がレンダリング時に使うテンプレートの指定で, 第2引数はテンプレートに与えるパラメータです. 386 | 387 | #### テンプレート 388 | 389 | 次に, このサブルーチンリファレンスで使われている, `index.tx`というテンプレートを見てみましょう. 390 | テンプレートは, Webアプリケーションのルートにある`tmpl`ディレクトリの中にあります. 391 | 392 | ```html:tmpl/index.tx 393 | : cascade "include/layout.tx" 394 | 395 | : override content -> { 396 | 397 |

Hello, Amon2 world!

398 | 399 | ... 中略 ... 400 | 401 |

Session counter demo

402 | 403 |

You seen this page <: $counter :> times.

404 | 405 |
406 | 407 |
408 | 409 | : } 410 | 411 | ``` 412 | 413 | Amon2では, テンプレートエンジンとして[Text::Xslate](https://metacpan.org/pod/distribution/Text-Xslate/script/xslate)を使っています. 414 | Text::Xslateは, [@\_\_gfx\_\_](https://twitter.com/__gfx__)さんが開発した, Perl界最速のテンプレートエンジンです. 415 | 速さと高性能さを兼ね備えたテンプレートエンジンで, PerlでWebアプリケーションを開発するのであれば, このテンプレートエンジンを使っておけばまず間違いはないでしょう. 416 | 417 | Text::Xslateには, いくつかのシンタックスがありますが, 今回はデフォルトのKolonを利用します. 418 | 詳しい使い方は, Text::Xslate::Syntax::Kolonの[POD(ドキュメント)](https://metacpan.org/pod/Text::Xslate::Syntax::Kolon)を見ると良いでしょう. 419 | 420 | ここでは, その中でも重要かつ有用なcascade/overrideと変数の展開についてだけ説明しておきます. 421 | 422 | ```html 423 | : cascade "include/layout.tx" 424 | 425 | : override content -> { 426 | ... 427 | : } 428 | ``` 429 | 430 | まずこの部分ですが, cascade/overrideの部分です. 431 | 簡単に言えば`: override content -> {`から`: }`で囲まれた部分を, `include/layout.tx`の`<: block content -> { } :>`の部分に埋め込む, という意味になります. 432 | このように書くことで, 例えばHTMLのヘッダや, Webサービスのナビゲーションバーの部分を共通化することが出来ます(詳しくは, `tmpl/include/layout.tx`を見てみましょう). 433 | 434 | 次に変数の展開の部分ですが, テンプレート内部に次のような部分があると思います. 435 | 436 | ```html 437 |

You seen this page <: $counter :> times.

438 | ``` 439 | 440 | この, `<: $counter :>`の部分で, 先程`render`メソッドで渡したパラメータを展開することができます. 441 | 442 | ```perl 443 | return $c->render('index.tx', { 444 | counter => $counter, 445 | }); 446 | ``` 447 | 448 | この, `counter`というキーに紐付いた値が展開されます. 449 | 例えば, `$counter`が「3」であれば... 450 | 451 | ```html 452 |

You seen this page 3 times.

453 | ``` 454 | 455 | のようにHTMLはレンダリングされます. 456 | 457 | ## 練習問題 458 | 459 | ### 1. Controllerの整理 460 | 461 | `index.tx`を, 次のように書き換えてシンプルにしましょう. 462 | 463 | ```html:tmpl/index.tx 464 | : cascade "include/layout.tx" 465 | 466 | : override content -> { 467 | 468 |

Scheduler

469 | 470 | : } 471 | ``` 472 | 473 | また, これによってControllerにおける`/reset_counter`や`/account/logout`といったパスへの処理は不要になりました. 474 | これらのコードを削除し, Controller(`Scheduler/Web/Dispatcher.pm`)のコードを, 「GETメソッドで」, 「`/`にアクセスしたとき」, 「`index.tx`をレンダリングする」というコード**だけ**を含むように書き換えましょう. 475 | 476 | ### 2. プロフィールページの作成 477 | 478 | GETメソッドで`'/user'`にアクセスした時, 自分のプロフィール(名前, 趣味, 年齢, 出身校など...)を表示するように, テンプレート(`tmpl/user.tx`)を作成し, Controllerにおいて必要なコードを用意してみましょう. 479 | 480 | なお, プロフィールの具体的なパラメータについては, テンプレートに直接書くのではなく, Controllerから渡して, レンダリングするようにしましょう. 481 | 482 | -> [Amon2入門 (第2部)](/amon2/2.md)に続く 483 | -------------------------------------------------------------------------------- /amon2/2.md: -------------------------------------------------------------------------------- 1 | # Amon2入門 (第2部) 2 | 3 | この記事では, [Amon2入門 (第1部)](/amon2/1.md)に引き続き, Amon2を利用した簡単なWebアプリケーション(スケジュール管理サービス)の開発を通して, Amon2を利用したWebアプリケーション開発の流れを体験していきます. 4 | 5 | ## アプリケーション概要 6 | 7 | アプリケーションの概要は, 次のようにしましょう. 8 | 9 | - `/`にGETメソッドでアクセスすると, スケジュールを登録するためのフォームがある(スケジュールの登録) 10 | - フォームは, スケジュールのタイトルと, その日付を入力するテキストフォームがある 11 | - ここでは単純化の為, 日付はテキストフォームを利用して, 「2015/3/10」のように入力することとする 12 | - さらに単純化のため, 誤った日付(例えば, 「2015/30/10」など)は**入力されない**ものとする 13 | - もちろん, 実際のアプリケーションではチェックして, エラーを出すべきです! 14 | - 更に更に, 単純化の為, フォームは空欄で**入力されない**ものとする 15 | - しつこいですが, もちろん実際のアプリケーションではチェックして, エラーを出すべきです! 16 | - フォームを送信すると, `/post`にPOSTメソッドで送信される 17 | - ここで`schedules`テーブルにスケジュールを登録する 18 | - 登録が終わったら, `/`にリダイレクトする 19 | - `/`にGETメソッドでアクセスすると, 新しいスケジュールが上になる形でスケジュールの一覧を見る事ができる(スケジュールの一覧表示) 20 | - スケジュールのタイトルと日付, そして「削除」ボタンを表示する 21 | - 削除ボタンをクリックすると, `/scheudles/スケジュールID/delete`にPOSTメソッドでアクセスするようにする 22 | - `/schedules/スケジュールID/delete`にPOSTメソッドでアクセスすると, そのスケジュールIDに該当するスケジュールをデータベースから削除する(スケジュールの削除) 23 | - スケジュールを削除した後は, `/`にリダイレクトする 24 | 25 | ### Tips 26 | 27 | アプリケーションの開発に入る前に, 少しTipsを. 28 | 29 | ``` 30 | $ carton exec -- plackup -Ilib -R ./lib --access-log /dev/null -p 5000 -a ./script/scheduler-server 31 | ``` 32 | 33 | アプリケーションの起動コマンドをこのように書き換えておくと, アプリケーションのファイルを変更した際に自動的にサーバを再起動(コードの読み込み直し)をしてくれます. 34 | コードを書き換える度にアプリケーションを停止して, 起動しなおすといった処理を回避出来るので, 非常に楽です. 35 | 36 | ## データベーススキーマの編集 37 | 38 | さて, まずはこのアプリケーションで利用するデータベースのスキーマを考えていきます. 39 | なお, Amon2ではデフォルトではデータベースにSQLiteを利用するようになっているので, 今回もそのままSQLiteを利用します. 40 | 実際のアプリケーションでは, MySQLを利用することが多いです(Amon2でも, 比較的簡単にMySQLを利用するように変更することができます). 41 | 42 | SQLiteが使うデータベースのスキーマは, `sql/sqlite.sql`に設置されています. 43 | 44 | ```sql:sql/sqlite.sql 45 | CREATE TABLE IF NOT EXISTS member ( 46 | id INTEGER NOT NULL PRIMARY KEY, 47 | name VARCHAR(255) 48 | ); 49 | ``` 50 | 51 | 今回は, このようなスキーマを使って開発を進めていくことにします. 52 | 53 | ```sql:sql/sqlite.sql 54 | CREATE TABLE IF NOT EXISTS schedules ( 55 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 56 | title VARCHAR(255), 57 | date INTEGER 58 | ); 59 | ``` 60 | 61 | スケジュールのID(`id`)の他に, スケジュールのタイトル(`title`)と日付(`date`)を持たせます. 62 | なお, 単純化の為, `date`については[UNIX時間(epoch秒)](http://ja.wikipedia.org/wiki/UNIX%E6%99%82%E9%96%92)で持たせることにします. 63 | 64 | 書き換えが終わったら, 早速このスキーマを利用して, SQLite用のデータベースファイルを生成します. 65 | Amon2の場合, 開発環境(development)では`db/development.db`というファイルを利用するので, 66 | 67 | ``` 68 | $ sqlite3 db/development.db < sql/sqlite.sql 69 | ``` 70 | 71 | このようにします. 72 | 73 | ...ちなみに余談ですが, データベースのスキーマを書き換えたい場合ですが, 面倒ですが一旦`rm db/development.db`でデータベースファイルを削除してから, `sqlite3 db/development.db < sql/sqlite.sql`で新しくデータベースファイルを生成しなおすのが何だかんだいって楽です. 74 | もちろん本番環境でこんな事をしてしまうと**データが全て消えてしまう**ので, 本番環境でMySQLを使っている場合は, `ALTER TABLE`などを使ってよしなに既存のデータベースを変更するようにします. 75 | 76 | ### Tengのスキーマの変更 77 | 78 | 次に, Tengのスキーマを変更します. 79 | Amon2がデフォルトで利用するORMのTengを利用する為には, 「Teng Schema」の設定をしなければなりません. 80 | 自動的に生成する方法もありますが([その一例](http://masteries.papix.net/entry/2014-05-19-teng-schema-dumper.html)), 今回は手動で書き換えることにします. 81 | 82 | Tengのスキーマファイルは, `lib/Scheduler/DB/Schema.pm`です. 83 | 84 | ```perl:lib/Scheduler/DB/Schema.pm 85 | package Scheduler::DB::Schema; 86 | use strict; 87 | use warnings; 88 | use utf8; 89 | 90 | use Teng::Schema::Declare; 91 | 92 | base_row_class 'Scheduler::DB::Row'; 93 | 94 | table { 95 | name 'member'; 96 | pk 'id'; 97 | columns qw(id name); 98 | }; 99 | 100 | 1; 101 | ``` 102 | 103 | 1つのテーブルに関する情報を, `table { ... };`の中に書いていきます(もし, データベースに複数個のテーブルがあれば, その数だけ`table { ... };`が並ぶことになります). 104 | `columns`でそのテーブルが持つカラム(列)の名前を, そして`pk`でprimary keyの設定をすることができます. 105 | 106 | そのため, 先程のデータベーススキーマでは, 次のように書き換えることになります. 107 | 108 | ```perl:lib/Scheduler/DB/Schema.pm 109 | package Scheduler::DB::Schema; 110 | use strict; 111 | use warnings; 112 | use utf8; 113 | 114 | use Teng::Schema::Declare; 115 | 116 | base_row_class 'Scheduler::DB::Row'; 117 | 118 | table { 119 | name 'schedules'; 120 | pk 'id'; 121 | columns qw(id title date); 122 | }; 123 | 124 | 1; 125 | ``` 126 | 127 | これで, データベースに関連する準備は完了です. 128 | 129 | ## トップページの編集 130 | 131 | まずは, スケジュールを登録出来るように, トップページのテンプレートを書き換えていきましょう. 132 | 133 | ```html:tmpl/index.tx 134 | : cascade "include/layout.tx" 135 | 136 | : override content -> { 137 | 138 |

Scheduler

139 | 140 |
141 | 142 |
143 |
144 | 145 | 146 |
147 |
148 | 149 | 150 |
151 | 152 |
153 | 154 | : } 155 | ``` 156 | 157 | この段階で, `localhost:5000`にアクセスしてみると, 次のように表示されるはずです. 158 | 159 | ![](/amon2/img/2-1.png) 160 | 161 | ただ, フォームに適切な文字を入力して, 「登録」ボタンを押すと... 162 | 163 | ![](/amon2/img/2-2.png) 164 | 165 | このように, `404 Not found`になってしまいます. 166 | これは, 言うまでもありませんが, 「POSTメソッドで, `/post`にアクセスした時の処理」を, まだ実装していないからです. 167 | 168 | ## スケジュールの登録 169 | 170 | それでは, Controllerに手を加えていきましょう. 171 | 「第1部」最後の練習問題の, 1. を解答した段階での`Dispatcher.pm`は, このようになっているはずです. 172 | 173 | ```perl:lib/Scheduler/Web/Dispatcher.pm 174 | package Scheduler::Web::Dispatcher; 175 | use strict; 176 | use warnings; 177 | use utf8; 178 | use Amon2::Web::Dispatcher::RouterBoom; 179 | 180 | get '/' => sub { 181 | my ($c) = @_; 182 | return $c->render('index.tx'); 183 | }; 184 | 185 | 1; 186 | ``` 187 | 188 | 今回は, ここからスタートしていきます. 189 | まず, 「POSTメソッドで, `/post`にアクセスした時の処理」を書けるように... 190 | 191 | ```perl:lib/Scheduler/Web/Dispatcher.pm 192 | package Scheduler::Web::Dispatcher; 193 | use strict; 194 | use warnings; 195 | use utf8; 196 | use Amon2::Web::Dispatcher::RouterBoom; 197 | 198 | get '/' => sub { 199 | my ($c) = @_; 200 | return $c->render('index.tx'); 201 | }; 202 | 203 | post '/post' => sub { ... }; 204 | 205 | 1; 206 | ``` 207 | 208 | こうなりますね. 209 | そして具体的な処理を, サブルーチンリファレンスの中に書いていきます. 210 | 211 | ```perl:lib/Scheduler/Web/Dispatcher.pm 212 | package Scheduler::Web::Dispatcher; 213 | use strict; 214 | use warnings; 215 | use utf8; 216 | use Amon2::Web::Dispatcher::RouterBoom; 217 | 218 | use Time::Piece; # (1) 219 | 220 | get '/' => sub { 221 | my ($c) = @_; 222 | return $c->render('index.tx'); 223 | }; 224 | 225 | post '/post' => sub { 226 | my ($c) = @_; 227 | 228 | my $title = $c->req->parameters->{title}; # 229 | my $date = $c->req->parameters->{date}; # (2) 230 | 231 | my $date_epoch = Time::Piece->strptime($date, '%Y/%m/%d')->epoch; # (3) 232 | 233 | $c->db->insert(schedules => { # 234 | title => $title, # 235 | date => $date_epoch, # (4) 236 | }); # 237 | 238 | return $c->redirect('/'); 239 | }; 240 | 241 | 1; 242 | ``` 243 | 244 | ...今回の場合は, こうなるでしょうか. 245 | 少し複雑な(1)〜(4)の部分を, 1つずつ見ていきましょう. 246 | 247 | ### (1) モジュールの呼び出し 248 | 249 | (3)の部分で, 「2014/05/10」といった文字列をepoch秒に変換する為に, [Time::Piece](https://metacpan.org/pod/Time::Piece)というモジュールを読み込んでいます. 250 | 251 | なお, Time::PieceはPerl 5.9.5からコアモジュール(Perlをインストールした際に, 最初から入っているモジュール)になっています. 252 | 253 | ```perl 254 | $ corelist Time::Piece 255 | 256 | Data for 2014-09-14 257 | Time::Piece was first released with perl v5.9.5 258 | ``` 259 | 260 | ### (2) テキストフォームに入力した文字列の取得 261 | 262 | ```perl 263 | my $title = $c->req->parameters->{title}; 264 | my $date = $c->req->parameters->{date}; 265 | ``` 266 | 267 | `$c->req`からパラメータを取得して, それぞれ`$title`と`$date`という変数に格納しています. 268 | 269 | ### (3) 日付文字列のepoch秒への変換 270 | 271 | ```perl 272 | my $date_epoch = Time::Piece->strptime($date, '%Y/%m/%d')->epoch; 273 | ``` 274 | 275 | この辺りは, 「こんな感じでやる」... と思って下さい. 276 | 277 | `Time::Piece->strptime($date, $format)`で, `$format`に従って`$date`を解析してTime::Pieceのオブジェクトを生成し, そのオブジェクトから利用出来る`epoch`メソッドを利用して, 元の文字列`$date`からepoch秒を生成しています. 278 | 279 | 生成したepoch秒は, `$date_epoch`という変数に代入して利用します. 280 | 281 | ### (4) データベースへの書き込み 282 | 283 | ```perl 284 | $c->db->insert(schedules => { 285 | title => $title, 286 | date => $date_epoch, 287 | }); 288 | ``` 289 | 290 | `$c->db`からTengのオブジェクトを呼び出し, `insert`メソッドで書き込んでいます. 291 | Tengの`insert`メソッドは, 第1引数にデータを書き込むテーブル名を, 第2引数にハッシュリファレンスで書き込みたいパラメータを与えます. 292 | 293 | なので, この場合は`schedules`というテーブルに, `title`の列に`$title`の中身を, `date`の列に`$date_epoch`の中身を書き込む, という処理になります. 294 | 295 | ## 動作確認 296 | 297 | それでは, 動作を確認してみましょう! 298 | テキストフォームに適切な文字列を入力し, 「登録」ボタンを押すと, 先程のようにエラー画面が表示されず, そのまま`/`へリダイレクトされると思います. 299 | 300 | それでは引き続き, 登録したスケジュールを表示出来るようにしていきましょう. 301 | 302 | ## スケジュール一覧の表示 303 | 304 | まずは, Controller側から変更していきます. 305 | 306 | ```perl:lib/Scheduler/Web/Dispatcher.pm 307 | package Scheduler::Web::Dispatcher; 308 | use strict; 309 | use warnings; 310 | use utf8; 311 | use Amon2::Web::Dispatcher::RouterBoom; 312 | 313 | use Time::Piece; 314 | 315 | get '/' => sub { 316 | my ($c) = @_; 317 | 318 | my @schedules = $c->db->search('schedules'); # (1) 319 | return $c->render('index.tx', { schedules => \@schedules }); 320 | }; 321 | 322 | post '/post' => sub { 323 | my ($c) = @_; 324 | 325 | my $title = $c->req->parameters->{title}; 326 | my $date = $c->req->parameters->{date}; 327 | 328 | my $date_epoch = Time::Piece->strptime($date, '%Y/%m/%d')->epoch; 329 | 330 | $c->db->insert(schedules => { 331 | title => $title, 332 | date => $date_epoch, 333 | }); 334 | 335 | return $c->redirect('/'); 336 | }; 337 | 338 | 1; 339 | ``` 340 | 341 | (1)の部分で, またもやTengを利用して, `schedules`テーブルの一覧を取得しています. 342 | Tengの`search`メソッドは, 第1引数に検索対象となるテーブルの名前を, 第2引数に検索条件(ハッシュリファレンス)を, 第3引数に検索オプション(ハッシュリファレンス)を指定できますが, 第2引数を省略した場合はテーブルに格納されている全てのデータを取得してくれます. 343 | 344 | これを, `$c`の`render`メソッドでテンプレート側に渡して, 表示してもらう訳です. 345 | それでは, テンプレート側を見てみましょう. 346 | 347 | ```html:tmpl/index.tx 348 | : cascade "include/layout.tx" 349 | 350 | : override content -> { 351 | 352 |

Scheduler

353 | 354 |
355 | 356 |
357 |
358 | 359 | 360 |
361 |
362 | 363 | 364 |
365 | 366 |
367 | 368 |
369 | : for $schedules -> $schedule { 370 | <: $schedule.title :>
371 | : } 372 | : } 373 | ``` 374 | 375 | とりあえず, 手っ取り早くデータベースに書き込みが出来ているか確認するために, `title`だけを表示するようにしてみました. 376 | 377 | ```html 378 | : for $schedules -> $schedule { 379 | <: $schedule.title :>
380 | : } 381 | ``` 382 | 383 | この部分ですが, Perlで例えればこんな感じになるでしょうか. 384 | 385 | ```perl 386 | for my $scheudle (@{ $schedules }) { 387 | print $schedule->title; 388 | } 389 | ``` 390 | 391 | テンプレート内で, `for $arrays -> $object { ... }`のように書くと, `$arrays`に含まれる全要素について, 1つずつ`$object`に代入した上で`{ ... }`内の処理を繰り返し実行します. 392 | 393 | `$schedule`については, データベースに格納された1つの行がTengのRowオブジェクトとして格納されています. 394 | Rowオブジェクトからは, それぞれの列の名前のメソッドが使えるようになっているので, 例えばControllerにおいて, 395 | 396 | ``` 397 | get '/' => sub { 398 | my ($c) = @_; 399 | 400 | my @schedules = $c->db->search('schedules'); 401 | for my $schedule (@schedules) { 402 | print STDERR $schedule->title . "\n"; 403 | } 404 | 405 | return $c->render('index.tx', { schedules => \@schedules }); 406 | }; 407 | ``` 408 | 409 | のように書くと, `/`にアクセスする度に, `scheudler-server`を実行しているコンソール上に登録したスケジュールのタイトルがズラっと表示されるはずです(時間があれば, やってみてください). 410 | 411 | というわけで, この時点で適当にスケジュールを登録してから, `/`にアクセスすると... 412 | 413 | ![](/amon2/img/2-3.png) 414 | 415 | このように, タイトルが表示されるようになっているはずです. 416 | 417 | ## 日付の表示 418 | 419 | 続いて, スケジュールの表示部分をテーブルにしつつ, 日付も表示するようにしてみます. 420 | 421 | ```html:tmpl/index.tx 422 | : cascade "include/layout.tx" 423 | 424 | : override content -> { 425 | 426 |

Scheduler

427 | 428 |
429 | 430 |
431 |
432 | 433 | 434 |
435 |
436 | 437 | 438 |
439 | 440 |
441 | 442 |
443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | : for $schedules -> $schedule { 453 | 454 | 455 | 456 | 457 | : } 458 | 459 |
タイトル日時
<: $schedule.title :><: $schedule.date :>
460 | : } 461 | ``` 462 | 463 | それでは, ブラウザで`/`にアクセスしてみましょう. 464 | 465 | ![](/amon2/img/2-4.png) 466 | 467 | ...そういえば, スケジュールの日付はepoch秒で保存するようにしていました. 468 | これでは非常に見栄えが悪いので, 「XXXX年XX月XX日」のように表示するようにしたいです. 469 | 470 | ### inflateとdeflate 471 | 472 | この解決策としては, いろいろな手段があります. 473 | Controllerレイヤーで変換してしまう, JavaScriptで変換してしまう... などなど. 474 | 475 | 今回は, Tengのinflate/deflateという仕組みを使って解決してみようと思います. 476 | この機能の詳細については, Perl Advent Calendar 2011 Teng Tracの[inflate / deflate](http://perl-users.jp/articles/advent-calendar/2011/teng/11)という記事が詳しいです. 477 | 478 | 一言で言えば, inflateは「Tengでデータベースからデータを引っ張ってくる時に, フィルタリングするモノ」, 逆にdeflateは「Tengでデータベースにデータを書き込む時に, フィルタリングするモノ」と思って下さい. 479 | 480 | 今回は, inflateの機能を使って, Tengを使って`schedules`テーブルからデータを引っ張ってくる際に, 「date」カラムのデータをepoch秒から(そのepoch秒を格納した)Time::Pieceのオブジェクトに変換します. 481 | そして, テンプレート側で, Time::Pieceの`strftime`メソッドを利用して, 好きなフォーマットで表示します. 482 | 483 | ### Tengのスキーマの書き換え 484 | 485 | ```perl:lib/Scheduler/DB/Schema.pm 486 | package Scheduler::DB::Schema; 487 | use strict; 488 | use warnings; 489 | use utf8; 490 | 491 | use Teng::Schema::Declare; 492 | use Time::Piece; # (1) 493 | 494 | base_row_class 'Scheduler::DB::Row'; 495 | 496 | table { 497 | name 'schedules'; 498 | pk 'id'; 499 | columns qw(id title date); 500 | 501 | inflate 'date' => sub { # 502 | my $col_value = shift; # (2) 503 | Time::Piece->strptime($col_value, '%s'); # 504 | }; # 505 | }; 506 | 507 | 1; 508 | ``` 509 | 510 | まず, (2)で使っているTime::Pieceモジュールを(1)で`use`しています. 511 | そして肝心の(2)の部分. inflateの書式は, `inflate $column_name => sub { ... };`のように書きます. 512 | `$column_name`はinfalteの処理をしたいカラムの名前になるので, この場合, `date`カラムのみ`sub { ... }`で指定した処理(フィルター)が行われます. 513 | 514 | サブルーチンリファレンスの第1引数には, その指定したカラムのデータが入ってきます. 515 | 例えば, `schedules`テーブルからあるデータを引っ張ってきた時, そのデータの`date`カラムの中身が`1234567890`であれば, (2)のコードの`$col_value`には, その`1234567890`が入ります. 516 | 517 | そして, そのサブルーチンリファレンスの返り値が, 新たに`$column_name`のデータとして上書きされます(この場合は, `date`カラムの中身が上書きされる). 518 | 519 | ここでは, `$col_value`にはepoch秒が入っているはずなので, それをTime::Pieceの`strptime`メソッドを利用してTime::Pieceのオブジェクトにして, それを返しています. 520 | 521 | これによって, Controllerで 522 | 523 | ```perl 524 | my $schedule = $c->db->single('schedules'); # `single`は, データベースから1つの要素を取得するメソッドです. 525 | print $schedule->date->strftime("%Y/%m/%d"); # => 2015/03/05 (例) 526 | ``` 527 | 528 | のように, `$schedule->date`の中身が, epoch秒ではなくそのepoch秒のTime::Pieceオブジェクトになる, という訳です. 529 | 530 | ### テンプレートでの描画 531 | 532 | ```html:tmpl/index.tx(抜粋) 533 | : for $schedules -> $schedule { 534 | 535 | <: $schedule.title :> 536 | <: $schedule.date.strftime("%Y/%m/%d") :> 537 | 538 | : } 539 | ``` 540 | 541 | テンプレートは, このように書き換えます. 542 | テンプレートにおいて, メソッド呼び出しの`->`は`.`で表現するので, 先程のTime::Pieceオブジェクトから任意のフォーマットの日付の文字列を生成するには, このように書けば良いです. 543 | 544 | これらの変更を実装すると, `/`にアクセスした時の表示は, 次のようになるはずです. 545 | 546 | ![](/amon2/img/2-5.png) 547 | 548 | これで, 冒頭で定めたアプリケーション概要のうち, 「スケジュールの登録」と「スケジュールの一覧表示」を達成することが出来ました. 549 | 続いて, 「スケジュールの削除」に挑戦していきましょう! 550 | 551 | 552 | ## スケジュールの削除 553 | 554 | それでは最後に, スケジュールを削除する機能を実装していきます. 555 | 556 | ### テンプレートの編集 557 | 558 | まず, 削除ボタンを表示するようにテンプレートを書き換えましょう. 559 | 560 | ```html:tmpl/index.tx(抜粋) 561 | 562 | 563 | タイトル 564 | 日時 565 | 566 | 567 | 568 | 569 | : for $schedules -> $schedule { 570 | 571 | <: $schedule.title :> 572 | <: $schedule.date.strftime("%Y/%m/%d") :> 573 | 574 |
575 | 576 |
577 | 578 | 579 | : } 580 | 581 | ``` 582 | 583 | すると, このような形で削除ボタンが表示されます. 584 | 585 | ![](/amon2/img/2-6.png) 586 | 587 | #### コラム: `$schedule.id`の正体 588 | 589 | ここで唐突に出てきた, `$schedule.id($schedule->id)`について解説しておきます. 590 | 591 | この`id`ですが, 最初にデータベーススキーマを設定した際には, `schedules`テーブルの中のカラムとして存在しました. 592 | 593 | ```sql:sql/sqlite.sql 594 | CREATE TABLE IF NOT EXISTS schedules ( 595 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 596 | title VARCHAR(255), 597 | date INTEGER 598 | ); 599 | ``` 600 | 601 | しかし, Controllerで`schedules`テーブルにデータを挿入する際には, `id`を指定していませんでした. 602 | 603 | ```perl:lib/Scheduler/Web/Dispatcher.pm(抜粋) 604 | $c->db->insert(schedules => { 605 | title => $title, 606 | date => $date_epoch, 607 | }); 608 | ``` 609 | 610 | しかし実際には, `schedules`テーブルにデータを挿入するタイミングで, 適切な値が`id`として格納されています. 611 | これは, SQLiteの`PRIMARY KEY`によるものです. 612 | 613 | `PRIMARY KEY`, [主キー](http://ja.wikipedia.org/wiki/%E4%B8%BB%E3%82%AD%E3%83%BC)は1テーブルにつき1カラムのみ設定することができ, データ型は大抵`INTEGER`などを数値を格納出来る型にします. 614 | なお, `PRIMARY KEY`を設定した場合, 自動的に`NOT NULL`制約を与えた場合と同じように, 重複した値が許されなくなります. 615 | SQLiteの場合, データ型が`INTEGER`のカラムに対して`PRIMARY KEY`を設定すると, データ挿入時, そのカラムに格納する値が空だった場合のみ自動的に重複しない値が挿入されるようになっています. 616 | なお, 今回はSQLiteを利用していますが, MySQLを利用している場合は, このような挙動をさせたい場合は`PRIMARY KEY`に加えて`AUTOINCREMENT`を指定しなければなりません. 617 | 618 | さて, もし`PRIMARY KEY`がない場合, 削除をする際には`title`や`date`で条件をかける必要がありますし, そもそも`title`と`date`が同じカラムがあった場合, どちらを消していいのかわからなくなる, という場合も出てくるでしょう. 619 | しかし, このように`PRIMARY KEY`として`id`を生成しておけば, 全てのスケジュールにユニークなIDを割り振ることが出来ます. 620 | そのため, ここまで見てきたようにスケジュールの削除を(`id`の指定のみで)簡単に実装することができるようになります. 621 | 622 | ...この辺りの詳細については割愛しますが, `id`というカラムの有無で実装の難しさが変わってくる, という所は感じて頂けたと思います. 623 | データベーススキーマの設計と, Webアプリケーションの実装は密接に関連していて, データベーススキーマをうまく設計出来ないと, Webアプリケーションの実装も難しくなる, という所は覚えておいて下さい. 624 | 625 | ### Controllerの書き換え 626 | 627 | 続いて, Controllerを書き換えていきます. 628 | 629 | ここで悩むのは, パスをどのように指定すればよいか? という所でしょう. 630 | ここまで, 例えば`post '/post' => sub { ... };`のようにControllerに書いてきましたが, 今回のパス(左の例の場合, `'/post'`)は可変です. 631 | つまり, それぞれのスケジュールが持っているIDに応じて, `/schedules/1/delete`とか`/schedules/5/delete`のように, 複数のパスが存在する訳です. 632 | 633 | こういう場合は, パスをこのように書けば解決できます. 634 | 635 | ```perl 636 | post '/schedules/:id/delete' => sub { 637 | my ($c, $args) = @_; 638 | 639 | my $id = $args->{id}; 640 | 641 | ... 642 | }; 643 | ``` 644 | 645 | パスの中に, `:id`のように`:`で始まる文字列を入れると, その部分は「全てのパターンにマッチ」するようになります. 646 | そして, そのマッチした文字列は, サブルーチンリファレンスの第2引数にハッシュリファレンスの形で渡ってきます. 647 | 648 | なので, 上記のコードは, 例えば`/schedules/1/delete`にアクセスした場合は`$id`の中身が`1`に, `/schedules/5/delete`にアクセスした場合は`$id`の中身が`5`になります. 649 | 650 | 後は, Tengの`delete`メソッドを利用して該当するIDのスケジュールを消してから, `'/'`にリダイレクトすればOKです. 651 | 652 | ```perl:lib/Scheduler/Web/Dispatcher.pm 653 | post '/schedules/:id/delete' => sub { 654 | my ($c, $args) = @_; 655 | my $id = $args->{id}; 656 | 657 | $c->db->delete('schedules' => { id => $id }); 658 | return $c->redirect('/'); 659 | }; 660 | ``` 661 | 662 | ### 動作確認 663 | 664 | それでは, 動作確認をしてみましょう. 665 | 666 | ![](/amon2/img/2-7.png) 667 | 668 | ここから, 「テスト2」の削除ボタンを押すと... 669 | 670 | ![](/amon2/img/2-8.png) 671 | 672 | 「テスト2」が正しく消えましたね! 673 | 674 | ## 表示順序の変更 675 | 676 | さて, これでアプリケーション概要で定めた全ての機能を実装することが出来ました. 677 | ...が, 複数のスケジュールを登録してみた時, 違和感を感じませんか? 678 | 679 | ![](/amon2/img/2-9.png) 680 | 681 | 日時の順番がバラバラになっていますね. 682 | これを, 「最新のスケジュールが上になる」ように変更して, Amon2チュートリアルを終わりにしたいと思います. 683 | 684 | ### Tengの`search`メソッドのオプション 685 | 686 | Tengの`search`メソッドは, 第3引数にオプションを指定できると説明しました. 687 | このオプションで, 取得するデータを並び替える`ORDER BY`を, `date`カラムについて適用するようにします. 688 | 689 | ```perl:lib/Scheduler/Web/Dispatcher.pm(抜粋) 690 | get '/' => sub { 691 | my ($c) = @_; 692 | 693 | my @schedules = $c->db->search('schedules', {}, { order_by => 'date DESC'}); 694 | return $c->render('index.tx', { schedules => \@schedules }); 695 | }; 696 | ``` 697 | 698 | 第2引数, 検索条件はなし(全て取得する)なので, `{}`のように空のハッシュリファレンスを渡しています. 699 | そして第3引数で, `{ order_by => 'date DESC' }`を指定しています. 700 | Tengは, これらの条件やオプションなどを元に, 自動的にSQLのクエリを組み立て, SQLiteやMySQLに対して実行してくれるのです. 701 | 702 | さて, このように変更すると, `'/'`にアクセスした際の結果はどうなるでしょうか? 703 | 704 | ![](/amon2/img/2-10.png) 705 | 706 | ...新しいスケジュールが上に表示されるようになりました! 707 | 708 | ## 練習問題 709 | 710 | ### 1-1. スケジュールの逆順表示 711 | 712 | GETメソッドで`'/'`にアクセスした時はこれまで通り「新しいスケジュールが上」になるように表示しつつ, GETメソッドで[クエリパラメータ(クエリ文字列)](http://e-words.jp/w/%E3%82%AF%E3%82%A8%E3%83%AA%E6%96%87%E5%AD%97%E5%88%97.html)を使って`'/?order=reverse'`でアクセスした時は, 「新しいスケジュールが下」になるように書き換えてみよう. 713 | 714 | #### ヒント 715 | 716 | Controllerにおいて, GETメソッドで`'/?order=reverse'`にアクセスした際の`order=reverse`のようなパラメータは, 次のようにして取得することが出来ます. 717 | 718 | ``` 719 | get '/' => sub { 720 | my ($c) = @_; 721 | 722 | my $order = $c->req->parameters->{order}; 723 | 724 | ... 725 | }; 726 | ``` 727 | 728 | ### 1-2. 「今日」のスケジュールの強調 729 | 730 | スケジュールの日付が今日だった場合, そのスケジュールのタイトルを赤色に変更するようにしてみよう. 731 | 今日の日付については, `Time::Piece`をuseしている場合, `localtime`というサブルーチンで今現在の時間のTime::Pieceオブジェクトを取得することができるので, ここから導くと良いでしょう(`strftime`メソッドで, 適切なフォーマットで文字列を生成しましょう). 732 | 733 | ### 1-3. deflate 734 | 735 | スケジュールを投稿する際, ControllerでTime::Pieceのオブジェクトからepoch秒を生成しています. 736 | これを, Tengのdeflateの機能を使って実装するように書き換えてみましょう. 737 | 738 | うまく実装できれば, Controllerのスケジュール投稿に関するコードは, 次のように書き換えても正しく動くはずです. 739 | 740 | ```perl:lib/Scheduler/Web/Dispatcher.pm(抜粋) 741 | post '/post' => sub { 742 | my ($c) = @_; 743 | 744 | $c->db->insert(schedules => { 745 | title => $c->req->parameters->{title}, 746 | date => $c->req->parameters->{date}, 747 | }); 748 | 749 | return $c->redirect('/'); 750 | }; 751 | ``` 752 | 753 | ### 2. チャレンジ課題 754 | 755 | チャレンジ課題は**必須ではありません**. 756 | 少し難易度が高いので「チャレンジ問題」にしていますが, 時間があれば是非挑戦していただきたいです! 757 | 758 | チャレンジ課題は, トップページに表示されている各スケジュールに「編集」ボタンを設置して, 任意のスケジュールのタイトルや日付を変更できる機能の追加です. 759 | 760 | スケジュール編集をするためのテンプレートを作成するなど, 作業量は少なくありませんが, この問題をクリア出来れば「Amon2で作成されたWebアプリケーションに, 自力で機能を1つ追加できた」という経験が出来ます! 761 | -------------------------------------------------------------------------------- /amon2/img/1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/amon2/img/1-1.png -------------------------------------------------------------------------------- /amon2/img/2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/amon2/img/2-1.png -------------------------------------------------------------------------------- /amon2/img/2-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/amon2/img/2-10.png -------------------------------------------------------------------------------- /amon2/img/2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/amon2/img/2-2.png -------------------------------------------------------------------------------- /amon2/img/2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/amon2/img/2-3.png -------------------------------------------------------------------------------- /amon2/img/2-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/amon2/img/2-4.png -------------------------------------------------------------------------------- /amon2/img/2-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/amon2/img/2-5.png -------------------------------------------------------------------------------- /amon2/img/2-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/amon2/img/2-6.png -------------------------------------------------------------------------------- /amon2/img/2-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/amon2/img/2-7.png -------------------------------------------------------------------------------- /amon2/img/2-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/amon2/img/2-8.png -------------------------------------------------------------------------------- /amon2/img/2-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/amon2/img/2-9.png -------------------------------------------------------------------------------- /author.md: -------------------------------------------------------------------------------- 1 | # Author / Contributor 2 | 3 | - `Author`は, オリジナルの資料を執筆した方のGithub IDです 4 | - `Contributor`は, 資料に対して加筆修正のPull Requestという形で貢献された方のGithub IDです 5 | 6 | ## [Amon2入門 (第1部)](/amon2/1.md) 7 | Author: @papix 8 | 9 | ## [Amon2入門 (第2部)](/amon2/2.md) 10 | Author: @papix 11 | 12 | ## [Homebrewによる環境構築](/homebrew.md) 13 | Author: @papix 14 | 15 | Contributor: @risou 16 | 17 | ## Infrastructure as Code 18 | ### [Vagrant](/infrastructure-as-code/vagrant.md) 19 | Author: @papix 20 | 21 | ### [Ansible](/infrastructure-as-code/ansible.md) 22 | Author: @papix 23 | 24 | Contributor: @azumakuniyuki 25 | 26 | ### [Serverspec](/infrastructure-as-code/serverspec.md) 27 | Author: @papix 28 | 29 | # [Git入門](/git.md) 30 | Author: @papix 31 | 32 | Contributor: @htk291 33 | 34 | # [OOP入門](/oop.md) 35 | Author: @papix 36 | 37 | # [Perlテスト入門](/test.md) 38 | Author: @papix 39 | 40 | # [ORM入門(Teng)](/orm.md) 41 | Author: @papix 42 | 43 | Contributor: @yuyashiraki 44 | 45 | # [CI入門(Wercker)](/ci/wercker.md) 46 | Author: @papix 47 | 48 | # [例外基礎](exception.md) 49 | Author: @papix 50 | 51 | # [WebAppテスト入門](webapp-test.md) 52 | Author: @papix 53 | 54 | # [React.js入門](reactjs.md) 55 | Author: @miniturbo 56 | -------------------------------------------------------------------------------- /ci/wercker.md: -------------------------------------------------------------------------------- 1 | # はじめに 2 | 3 | 自動テストを用意しておくことで, プロダクトをリファクタリングしたり, 或いは新しい機能を追加した時に, その変更にバグがないか(その変更によって, 既存の機能に何らかの不整合が出ていないか)を簡単に確認することができます. 4 | 5 | しかし, せっかく自動テストを書いたとしても, そのテストが実行されなければ何の意味もありません. 6 | このような問題に対して, コードをリポジトリにプッシュしたタイミングで自動的にテストを実行するような仕組みを作り, 継続的にテストを実行するようにしていく, というアプローチがあります. 7 | このような取り組みを, 継続的インテグレーション(Continuous Integration, CI)と呼びます. 8 | 9 | この資料では, Bitbucketのリポジトリで管理しているPerlモジュールに対して, CIのSaaSであるWerckerを利用してテストを継続的に実行する環境を作る方法について解説します. 10 | 11 | # CI概要 12 | 13 | 正確に言えば, CIは「テストの継続的な実行」のみを指すのではなく, そこから自動的にデプロイやリリースを行うことで, 「デプロイやリリースの継続的な実行」もCIに含まれます. 14 | 但し今回は簡単のため, 前述の通り「テストの継続的な実行」のみを扱うこととします. 15 | 16 | さて, CIを実現する為には, そのためのツールを利用するのが手っ取り早いです. 17 | かつてはJenkinsを使う例がありましたが, Jenkinsは利用者がサーバにデプロイしなければならない為, 環境構築が多少面倒です. 18 | そこで近年は, CIを提供するSaaSが多く提供されるようになってきました. 19 | 20 | CIを提供するSaaSとしては, Githubで公開されているライブラリからの利用例が多い[Travis CI](https://travis-ci.org/)の他, [CircleCI](https://circleci.com/), [drone.io](https://drone.io/), そして今回利用する[Wercker](http://wercker.com/)などが存在します. 21 | 今回紹介するWerckerは, Bitbucketに対応していること, そして現在Werckerそのものがベータ版の為, プライベートリポジトリであっても無料で出来ることが特徴です. 22 | 23 | それでは早速, Werckerを利用した継続的なテスト環境の構築に取り組んでいきましょう. 24 | 25 | # Werckerのサインアップ 26 | 27 | まず始めに, Werckerのアカウントを作成しましょう. 28 | こちらの[Sing up](https://app.wercker.com/users/new/)ページに必要な情報を入力し, 「SIGN UP NOW」ボタンをクリックします. 29 | 30 | ![](/ci/wercker/1.png) 31 | 32 | 入力したメールアドレスに認証用のメールアドレスが届きますので, メール 33 | 中の「ACTIVATE YOUR ACCOUNT NOW」をクリックします. 34 | 35 | ![](/ci/wercker/2.png) 36 | 37 | 次のような画面が表示されていれば, サインアップは完了です. 38 | 39 | ![](/ci/wercker/3.png) 40 | 41 | # CI対象となるリポジトリの用意 42 | 43 | WerckerでCI環境を構築する前に, まずは今回CIの対象となるリポジトリをBitbucketに用意します. 44 | 今回はテストが動作することを確認できれば良いので, `minil new SampleModule`で生成したモジュールの雛形を, 自分のBitbucketの`SampleModule`リポジトリに登録して利用しましょう. 45 | 46 | まず, Bitbucketでリポジトリを作成します. 47 | リポジトリは, Bitbucketの上メニューから「Create」ボタンを押し, 「Create repository」を選択します. 48 | 49 | ![](/ci/wercker/4.png) 50 | 51 | 「name」にリポジトリ名である「SampleModule」を入力し, 「Create repository」をクリックします. 52 | このとき, もし, 「Access Level」の「This is a private repository」にチェックが入っていない場合, **チェックを入れておいて下さい**. 53 | 54 | ![](/ci/wercker/5.png) 55 | 56 | リポジトリの作成が完了しました. 57 | 58 | ![](/ci/wercker/6.png) 59 | 60 | 既存のリポジトリをこのリポジトリに登録するための方法については, 「I have an existing project」をクリックすると確認することができます. 61 | 62 | ![](/ci/wercker/7.png) 63 | 64 | それでは, `minil`コマンドを利用してモジュールを作成し, Bitbucketにプッシュしましょう. 65 | 66 | ``` 67 | $ minil new SampleModule 68 | Writing lib/SampleModule.pm 69 | Writing Changes 70 | Writing t/00_compile.t 71 | Writing .travis.yml 72 | Writing .gitignore 73 | Writing LICENSE 74 | Writing cpanfile 75 | Initializing git SampleModule 76 | [SampleModule] $ git init 77 | Initialized empty Git repository in /Users/username/SampleModule/.git/ 78 | Retrieving meta data from lib/SampleModule.pm. 79 | Name: SampleModule 80 | Abstract: It's new $module 81 | Version: 0.01 82 | fatal: bad default revision 'HEAD' 83 | [SampleModule] $ git add . 84 | Finished to create SampleModule 85 | 86 | $ cd SampleModule 87 | $ git commit -m "initial commit" 88 | [master (root-commit) 484e95e] initial commit 89 | 11 files changed, 569 insertions(+) 90 | create mode 100644 .gitignore 91 | create mode 100644 .travis.yml 92 | create mode 100644 Build.PL 93 | create mode 100644 Changes 94 | create mode 100644 LICENSE 95 | create mode 100644 META.json 96 | create mode 100644 README.md 97 | create mode 100644 cpanfile 98 | create mode 100644 lib/SampleModule.pm 99 | create mode 100644 minil.toml 100 | create mode 100644 t/00_compile.t 101 | 102 | $ git remote add origin git@bitbucket.org:papix/samplemodule.git 103 | $ git push -u origin --all 104 | Counting objects: 15, done. 105 | Delta compression using up to 4 threads. 106 | Compressing objects: 100% (13/13), done. 107 | Writing objects: 100% (15/15), 8.65 KiB | 0 bytes/s, done. 108 | Total 15 (delta 1), reused 0 (delta 0) 109 | To git@bitbucket.org:papix/samplemodule.git 110 | * [new branch] master -> master 111 | Branch master set up to track remote branch master from origin. 112 | ``` 113 | 114 | 問題なくプッシュが終了すると, リポジトリをBitbucketした際の表示が次のようになります. 115 | 116 | ![](/ci/wercker/8.png) 117 | 118 | これでリポジトリの準備は完了です. 119 | 120 | # WerckerとBitbucketの連携 121 | 122 | 次に, WerckerからBitbucketのリポジトリを確認できるよう, WerckerとBitbucketの連携を行います. 123 | メニュー右上の歯車アイコンをクリックし, [「Profile Settings」](https://app.wercker.com/#profile/personal)ページへ移動します. 124 | 125 | ![](/ci/wercker/9.png) 126 | 127 | 左のメニューから, 「Git connections」を選択します. 128 | 129 | ![](/ci/wercker/10.png) 130 | 131 | Bitbucketのロゴの右にある, 「Connect」ボタンをクリックします. 132 | 133 | ![](/ci/wercker/11.png) 134 | 135 | Bitbucketのセッションが残っている場合, Bitbucketのロゴの下の文字が「Connected」に変化します. 136 | これでWerckerとBitbucketとの連携は完了です. 137 | 138 | ![](/ci/wercker/12.png) 139 | 140 | # CI環境の構築 141 | 142 | それではいよいよ, Werckerを使ったCI環境を構築していきます. 143 | 上部メニューから「Create」をクリックし, 「Application」を選択します. 144 | 145 | ![](/ci/wercker/13.png) 146 | 147 | すると次のような画面になります. 148 | まずは, CIが対象とするリポジトリを選択します. 149 | 先程, WerckerとBitbucketの連携を済ませたので, 「Use Bitbucket」というボタンが存在するはずです. これをクリックします. 150 | 151 | ![](/ci/wercker/14.png) 152 | 153 | 少し時間がかかった後, 次のような画面が表示されます. 154 | テキストボックスにリポジトリ名を入力して検索し, 該当するリポジトリをクリックしてから, 「Use selected repo」をクリックします. 155 | 156 | ![](/ci/wercker/15.png) 157 | 158 | 次に, Werckerがリポジトリにアクセスする為の手段を設定します. 159 | 今回は, プライベートリポジトリを対象としているので, 一番下の「Manually add the deploy key.」を選択します. 160 | 161 | 「key」の部分に表示されたSSH公開鍵は, 一旦コピーして保存しておきましょう(後でBitbucketで登録します). 162 | 問題なければ, 「Next step」をクリックします. 163 | 164 | ![](/ci/wercker/16.png) 165 | 166 | そのまま「Next step」をクリックします. 167 | 168 | ![](/ci/wercker/17.png) 169 | 170 | これでWerckerの設定は完了です. 171 | 「Make my app public」は, 今回はプライベートリポジトリを対象としているのでチェックせず, そのまま「Finish」をクリックします. 172 | 173 | ![](/ci/wercker/18.png) 174 | 175 | このような画面が表示されていれば, OKです. 176 | 177 | ![](/ci/wercker/19.png) 178 | 179 | # SSH公開鍵の登録 180 | 181 | 最後に, Bitbucketで管理しているリポジトリに, Wercker用のSSH公開鍵を登録します. 182 | まず, Bitbucketのリポジトリのページの左側にある, 歯車をクリックします. 183 | 184 | ![](/ci/wercker/20.png) 185 | 186 | 「GENERAL」の「Deployment keys」をクリックします. 187 | 188 | ![](/ci/wercker/21.png) 189 | 190 | 「Add key」ボタンを押し, SSH公開鍵の入力画面を開きます. 191 | 192 | ![](/ci/wercker/22.png) 193 | 194 | 「Label」に鍵の名前(今回の場合, 「Wercker」等で良いでしょう), 「Key」に先程Werckerで表示されていたSSH公開鍵を入力し, 右下の「Add key」ボタンをクリックします. 195 | 196 | ![](/ci/wercker/23.png) 197 | 198 | このような表示になっていれば, SSH公開鍵の登録は完了です. 199 | 200 | ![](/ci/wercker/24.png) 201 | 202 | # `wercker.yml`の用意 203 | 204 | Werckerは, リポジトリのルートディレクトリに設置された`wercker.yml`というファイルに従ってCIを実施します. 205 | 今回は, 次のような設定を利用することとします. 206 | 207 | ```yml:wercker.yml 208 | box: papix/perl5.18@0.0.1 209 | build: 210 | steps: 211 | - script: 212 | name: Install modules 213 | code: cpanm -l local --installdeps . 214 | - script: 215 | name: Run test 216 | code: prove -l -Ilocal t 217 | ``` 218 | 219 | ## `wercker.yml`の書き方 220 | 221 | ### `box` 222 | 223 | ```yaml:wercker.yml(抜粋) 224 | box: papix/perl5.18@0.0.1 225 | ``` 226 | 227 | `wercker.yml`では, 必ずBoxを指定しなければなりません. 228 | Boxは, Werckerがデフォルトで提供しているものを利用することができます. 229 | ただ, WerckerでCIを行う際, 事前に依存となるライブラリ等のインストールを実施している場合, それらを既にインストールしたBoxを予め作成しておけば, インストールにかかる時間を短縮することができます. 230 | 231 | ここでは, WerckerのBoxを作成する方法については省略します. 232 | なお, Werckerがデフォルトで提供しているものを含め, Werckerから利用できるBoxについては, [こちらのページ](https://app.wercker.com/#explore/boxes)から検索することが可能です. 233 | 234 | このファイルでは, Perl 5.18及びApp::cpanminusとCartonをインストール済みの[papix/perl5.18](https://app.wercker.com/#applications/542ec5cf4d7a367e23000257/tab/details)を利用しています. 235 | 236 | ### `services` 237 | 238 | ```yaml:wercker.yml(抜粋) 239 | services: 240 | - wercker/mysql 241 | - wercker/redis 242 | ``` 243 | 244 | Werckerでは, MySQLやRedisは自由に起動出来ず, `service`という概念を利用して起動しなければなりません. 245 | これらの指定を行うのが`services`です. 246 | 247 | Werckerで利用できる`service`やその詳細については, [こちらのページ](http://old-devcenter.wercker.com/articles/services/)を確認して下さい. 248 | 249 | ### `build` / `deploy` 250 | 251 | `build`はビルドの処理を, `deploy`はその後のデプロイ処理を書くフェイズです. 252 | 今回は`deploy`は実施しないので, テストの実行のみを`build`を利用して実施します. 253 | 254 | `build`及び`deploy`は, 「step」と呼ばれるユーザが定義したひとまとまりの処理を利用することが可能です. 255 | 例えば, [moltar/carton-install](https://app.wercker.com/#applications/545e63cffa3ae0c709291ff7/tab/details)というstepを利用することで, Cartonを利用したライブラリのインストールを次のように書くことが可能です. 256 | 257 | ```yaml:wercker.yml(抜粋) 258 | build: 259 | steps: 260 | - moltar/carton-install 261 | ``` 262 | 263 | なお, 提供されている`step`については, [こちらのページ](https://app.wercker.com/#explore/steps/search)から検索することが可能です. 264 | 265 | また, `script`を利用して, 任意のスクリプトを実行することも可能です. 266 | 267 | ```yaml:wercker.yml(抜粋) 268 | build: 269 | steps: 270 | - script: 271 | name: echo 1 272 | code: |- 273 | echo "hoge" 274 | - script: 275 | name: echo 2 276 | code: |- 277 | echo "fuga" 278 | ``` 279 | 280 | 今回は, `script`を利用して, `cpanm`を利用したモジュールのインストールと, `prove`を利用したテストの実行を行っています. 281 | 282 | なお, `build`や`deploy`は, その途中でstepの処理に失敗した場合, 或いは`script`で実行したコードが正しく終了しなかった場合, その時点で一連の処理を中断します. 283 | そして, Wercker上ではその一連の処理は失敗したものとして扱われます. 284 | 285 | ## `after-steps` 286 | 287 | `build`及び`deploy`では, `after-step`を利用するこが出来ます. 288 | `after-step`に記述した処理は, `build`及び`deploy`の処理が途中で失敗した場合であっても, 最後に必ず実行されます. 289 | 290 | ```yaml:wercker.yml(抜粋) 291 | build: 292 | steps: 293 | ... 294 | ... 295 | ... 296 | after-steps: 297 | - hipchat-notify: 298 | token: HIPCHAT_ROOM_TOKEN 299 | room-id: room_id 300 | from-name: wercker-bot 301 | ``` 302 | 303 | 例えば, このように[hipchat-notify](https://app.wercker.com/#applications/51f26c380771b3526e000c1c/tab/details)のstepを利用すれば, `build`の`steps`に記載した処理の結果を通知してくれます. 304 | 305 | ※ここで求められる`token`は, HipChatのルーム単位で生成できるトークン(API v2用のトークン)ではなく, HipChatの管理者ユーザが生成できる全部屋共通のトークン(API v1用のトークン)です. 306 | 307 | このように, 必ず最後に実行したい処理があるのであれば, 実行漏れがないように`after-steps`を利用して記述するようにしましょう. 308 | 309 | この他, `wercker.yml`で利用できる機能の詳細については, [こちらのページ](http://old-devcenter.wercker.com/articles/werckeryml/)を確認しましょう. 310 | 311 | # 実行結果の確認 312 | 313 | それでは, この`wercker.yml`をコミットして, Bitbucketにプッシュしてみましょう. 314 | 315 | ``` 316 | $ git add . 317 | $ git commit -m "add wercker.yml" 318 | [master c003585] add wercker.yml 319 | Date: Fri May 8 00:21:01 2015 +0900 320 | 1 file changed, 9 insertions(+) 321 | create mode 100644 wercker.yml 322 | 323 | $ git push 324 | Counting objects: 3, done. 325 | Delta compression using up to 4 threads. 326 | Compressing objects: 100% (3/3), done. 327 | Writing objects: 100% (3/3), 398 bytes | 0 bytes/s, done. 328 | Total 3 (delta 1), reused 0 (delta 0) 329 | To git@bitbucket.org:papix/samplemodule.git 330 | + 608ec62...c003585 master -> master 331 | ``` 332 | 333 | リポジトリをBitbucketにプッシュすると, Werckerはそれを自動的に検知し, `wercker.yml`に従って処理を実行します. 334 | 335 | ![](/ci/wercker/25.png) 336 | 337 | テストの状況については, このようにWerckerに接続した際のトップページのフィードに表示されます. 338 | また, フィード上のテスト結果をクリックすることで, 次のようにその詳細を確認することも可能です. 339 | 340 | ![](/ci/wercker/26.png) 341 | 342 | 万が一ビルドに失敗した場合は, 次のようにメールによる通知が行われます. 343 | 344 | ![](/ci/wercker/27.png) 345 | -------------------------------------------------------------------------------- /ci/wercker/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/1.png -------------------------------------------------------------------------------- /ci/wercker/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/10.png -------------------------------------------------------------------------------- /ci/wercker/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/11.png -------------------------------------------------------------------------------- /ci/wercker/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/12.png -------------------------------------------------------------------------------- /ci/wercker/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/13.png -------------------------------------------------------------------------------- /ci/wercker/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/14.png -------------------------------------------------------------------------------- /ci/wercker/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/15.png -------------------------------------------------------------------------------- /ci/wercker/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/16.png -------------------------------------------------------------------------------- /ci/wercker/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/17.png -------------------------------------------------------------------------------- /ci/wercker/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/18.png -------------------------------------------------------------------------------- /ci/wercker/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/19.png -------------------------------------------------------------------------------- /ci/wercker/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/2.png -------------------------------------------------------------------------------- /ci/wercker/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/20.png -------------------------------------------------------------------------------- /ci/wercker/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/21.png -------------------------------------------------------------------------------- /ci/wercker/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/22.png -------------------------------------------------------------------------------- /ci/wercker/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/23.png -------------------------------------------------------------------------------- /ci/wercker/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/24.png -------------------------------------------------------------------------------- /ci/wercker/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/25.png -------------------------------------------------------------------------------- /ci/wercker/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/26.png -------------------------------------------------------------------------------- /ci/wercker/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/27.png -------------------------------------------------------------------------------- /ci/wercker/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/3.png -------------------------------------------------------------------------------- /ci/wercker/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/4.png -------------------------------------------------------------------------------- /ci/wercker/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/5.png -------------------------------------------------------------------------------- /ci/wercker/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/6.png -------------------------------------------------------------------------------- /ci/wercker/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/7.png -------------------------------------------------------------------------------- /ci/wercker/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/8.png -------------------------------------------------------------------------------- /ci/wercker/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/ci/wercker/9.png -------------------------------------------------------------------------------- /examples/react-todo/README.md: -------------------------------------------------------------------------------- 1 | # React TODO Example (forked from [miniturbo/react-todo](https://github.com/miniturbo/react-todo)) 2 | -------------------------------------------------------------------------------- /examples/react-todo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React TODO 6 | 7 | 8 | 9 | 10 | 11 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /exception.md: -------------------------------------------------------------------------------- 1 | # はじめに 2 | 3 | アプリケーションを開発する中で, 「ある処理」を行った時に異常事態が生じうるような場合は, 多々想定できると思います. 4 | 例えば, アプリケーションからMySQLなどのミドルウェアを操作しようとした時に, ミドルウェアが起動されていない場合, アプリケーションが行おうとしていたミドルウェアの操作は当然ながら実施することが出来ません. 5 | また, アプリケーションからミドルウェアを操作できたとしても, 操作した結果ミドルウェアでエラーが発生するといったパターンもあるでしょう. 6 | 7 | このような異常事態が発生した場合, アプリケーションは期待する挙動を実現出来なくなってしまいます. 8 | そこで大抵のプログラミング言語には, このような異常を表現する「例外」という仕組みが提供されており, 例外をきっかけに現在の処理を中止して, 別の処理(例外処理)を行える仕組みが提供されています. 9 | 10 | 本稿では, まずPerlにおいて「例外」を実現する方法と, それを処理するための方法について解説します. 11 | その後に, 例外をどのように使うべきか, 例外を使う際に気をつけるべき点を, 例外理論として概説します. 12 | 13 | # 例外実装 14 | 15 | ## 例外の発生 16 | 17 | まずはPerlで例外を発生させる方法について解説します. 18 | ここでは例外を処理する方法については解説していない為, 基本的には例外が発生したタイミングでプログラムの処理は終了するようになっています. 19 | 発生した例外を受け取り, 任意の処理を実現する為の方法は, 次の「例外の処理」にて解説します. 20 | 21 | ### `die` 22 | 23 | Perlで例外を発生させる一番基本的な方法は, `die`の利用です. 24 | 25 | ```perl:die.pl 26 | use strict; 27 | use warnings; 28 | 29 | print "before\n"; 30 | 31 | die "die!"; 32 | 33 | print "after\n"; 34 | ``` 35 | 36 | このスクリプトを実行すると, 実行結果は次のようになります. 37 | 38 | ``` 39 | $ perl die.pl 40 | before 41 | die! at die.pl line 6. 42 | ``` 43 | 44 | コードで`die`が実行されたタイミング例外が発生し, `die`の第1引数をエラーメッセージを出力して処理が中断していることがわかります. 45 | 46 | ### `Carp` 47 | 48 | Carpモジュールは例外を発生させるモジュールの1つであり, 例外発生時に`die`よりも多くの情報を得ることができます. 49 | 例えば, 次のコードを実行してみましょう. 50 | 51 | ```perl:carp.pl 52 | use strict; 53 | use warnings; 54 | use Carp; 55 | 56 | main(); 57 | 58 | sub main { 59 | print "before\n"; 60 | 61 | Carp::croak "die!"; 62 | 63 | print "after\n"; 64 | } 65 | ``` 66 | 67 | 結果は次の通りです. 68 | 69 | ``` 70 | before 71 | die! at die.pl line 10. 72 | main::main() called at die.pl line 5 73 | ``` 74 | 75 | `die`で例外を発生させた時と同様, 「10行目で例外が発生した」という情報の他に, 「この処理は5行目にある`main()`の呼び出しから発生した」という情報も得ることができました. 76 | Carpモジュールを利用すると, このように例外が発生した処理がどのような経緯で呼びだされたかを知ることが出来ます(このような情報を「スタックトレース」と呼びます). 77 | 78 | このような特徴に加え, CarpモジュールはPerlのコアモジュールでもあるため, CPANモジュールを実装する際の例外発生手段として使われることが多いです. 79 | 80 | ### `Exception::Tiny` 81 | 82 | Exception::Tinyを利用することで, 例外をクラスの形で定義出来るようになります. 83 | 84 | ```perl:exception.pl 85 | use strict; 86 | use warnings; 87 | 88 | print "before\n"; 89 | 90 | Exception::Hoge->throw; 91 | 92 | print "after\n"; 93 | 94 | package Exception::Hoge; 95 | use parent 'Exception::Tiny'; 96 | ``` 97 | 98 | ここでは, とある異常な処理が起きた事を表す`Exception::Hoge`というクラス(パッケージ)を用意し, このクラスの`throw`メソッドを利用して例外を発生させています. 99 | 今回は適当な名前がない為に`Exception::Hoge`としていますが, 実際は`MyApp::Exception::ResourceNotFound`(外部リソースを利用しようとして見つからなかった場合)であったり, `MyApp::DB::Exception::DuplicateException`(DBでUNIQUE制約のカラムが重複してしまった場合)などのように名前を付ける事になるでしょう. 100 | 101 | さて, このコードを実行した場合, 結果は次のようになります. 102 | 103 | ``` 104 | $ perl exception.pl 105 | before 106 | Exception::Hoge at die.pl line 6. 107 | ``` 108 | 109 | `Exception::Hoge`というクラスで用意された例外が発生した, という出力が行われた後に処理が中断します. 110 | 111 | ### 使い分け 112 | 113 | `*.pl`のようなスクリプトを実装する場合,, 大抵は`die`で事足りるでしょう. 114 | 小規模〜中規模なモジュールを実装する場合であれば, `die`ではなくCarpモジュールを利用する方が利用者に多くの情報を提供できるでしょう. 115 | 大規模なモジュールや, Webアプリケーションを開発する場面では様々な種類の例外が想定されるので, Exception::Tinyを利用し, クラスとして細かく例外を表現すると便利です. 116 | 117 | ## 例外の処理 118 | 119 | 例外が発生した際に行われる処理を「例外処理」と呼びます. 120 | ここでは, 例外処理を記述する方法について解説します. 121 | 122 | ### `Try::Tiny` 123 | 124 | `try`と`catch`のような構文を提供するモジュールです. 125 | 126 | ```perl 127 | use Try::Tiny; 128 | 129 | try { 130 | ... 131 | } catch { 132 | ... 133 | }; 134 | ``` 135 | 136 | `try`の後の`{ ... }`と`catch`の後の`{ ... }`は, それぞれコードリファレンス(サブルーチンリファレンス)です. 137 | 1つ目の, `try`の後のサブルーチンメソッドを実行した際, その中で例外が発生した場合のみ, `catch`の次のサブルーチンリファレンスが実行されます. 138 | 139 | Try::Tinyを利用した例外処理は, DBIやORMなどを利用してデータベースを操作する際に, トランザクションとそのロールバックを実装する為に利用することが多いです. 140 | 141 | ```perl 142 | my $teng = MyApp::DB->new( ... ); 143 | 144 | my $txn = $teng->txn_scope; 145 | 146 | try { 147 | 148 | ... DB操作 ... 149 | 150 | $txn->commit; 151 | } catch { 152 | $txn->rollback; 153 | }; 154 | ``` 155 | 156 | この場合, `try`の次のコードリファレンスにある「DB操作」の部分のコードで例外(`$teng`を利用したデータベース操作の失敗)が発生した場合, `catch`の次のコードリファレンスにある`$txn->rollback`が実行されることによって, 「DB操作」で行われたデータベース操作を, 操作する前の段階の内容に巻き戻すことが可能です. 157 | 158 | なお, 発生した例外の情報は, `catch`の次のサブルーチンリファレンスに第1引数として渡されます. 159 | 160 | ```perl:exception.pl 161 | use strict; 162 | use warnings; 163 | use Try::Tiny; 164 | use Data::Dumper; 165 | 166 | try { 167 | MyException::A->throw; 168 | } catch { 169 | my ($e) = @_; 170 | print Dumper $e; 171 | }; 172 | 173 | package MyException::A; 174 | use parent 'Exception::Tiny'; 175 | ``` 176 | 177 | 実行結果は以下の通りです. 178 | 179 | ``` 180 | $ perl exception.pl 181 | $VAR1 = bless( { 182 | 'subroutine' => 'main::try {...} ', 183 | 'package' => 'main', 184 | 'message' => 'MyException::A', 185 | 'line' => 7, 186 | 'file' => 'exception.pl' 187 | }, 'MyException::A' ); 188 | ``` 189 | 190 | このように, `Exception::Tiny`を利用して定義した, `MyExceotion::A`という例外のオブジェクトが渡ってきています. 191 | 192 | ### `Try::Lite` 193 | 194 | さて, Try::Tinyは簡単に例外処理を実装することが可能ですが, 少し複雑な例外処理を実装する為には向いていません. 195 | 例えば, 例外の種類によって例外処理の内容を変更したい場合は, 少し煩雑なコードを書かねばなりません. 196 | このような場合は, Try::Liteを利用することを推奨します. 197 | 198 | 例えば, `Exception::Tiny`を利用して, `MyException::A`と`MyException::B`という2つの例外を用意したとします. 199 | この際, `MyException::A`と`MyException::B`で異なる例外処理を行いたい場合, Try::Liteを利用して次のように実装することが出来ます. 200 | 201 | ```perl 202 | use strict; 203 | use warnings; 204 | use Try::Lite; 205 | 206 | try { 207 | MyException::A->throw; 208 | } ( 209 | 'MyException::A' => sub { 210 | print "MyException::A\n"; 211 | }, 212 | 'MyException::B' => sub { 213 | print "MyException::B\n"; 214 | }, 215 | ); 216 | 217 | package MyException::A; 218 | use parent 'Exception::Tiny'; 219 | 220 | package MyException::B; 221 | use parent 'Exception::Tiny'; 222 | ``` 223 | 224 | Try::Liteでは, `try`の次のサブルーチンリファレンスを実行した際, 例外が発生した場合はその次の`( ... )`で指定された例外処理を実行します. 225 | `( ... )`は例外の名前(パッケージ名)とサブルーチンリファレンスのハッシュ形式になっており, 発生した例外がハッシュのキーとなっている例外と一致した場合, これに対応するサブルーチンリファレンスが実行されます. 226 | 227 | ここでは, `try`の次のサブルーチンで`MyException::A`の例外が発生しているので, `MyException::A`に対応したサブルーチンリファレンスが実行され, 「MyException::A」が表示されます. 228 | `MyException::A`ではなく`MyException::B`の例外が発生した場合は, 同様に`MyExceotion::B`に対応したサブルーチンリファレンスが実行され, 「MyException::B」が表示されます. 229 | 230 | Try::Liteを利用すれば, 例えば「同時にMySQLとRedisを操作する」という場合に, 「MySQLの操作で例外が発生した場合」と「Redisの操作で例外が発生した場合」によって, 例外処理の内容を変更することが可能です. 231 | 232 | なお, Try::Liteでは, 発生した例外を受け取る方法がTry::Liteとは異なります. 233 | Try::Liteでは, 発生した例外は`$@`という変数を通じて受け取ります. 234 | 235 | ```perl 236 | try { 237 | MyException::A->throw; 238 | } ( 239 | 'MyException::A' => sub { 240 | print "MyException::A\n"; 241 | $@->rethrow; # 例外処理をした後, 更に例外を投げる 242 | }, 243 | 'MyException::B' => sub { 244 | print "MyException::B\n"; 245 | }, 246 | ); 247 | ``` 248 | 249 | また, Try::Liteでは全ての例外に対する例外処理は, 次のように書く事が可能です. 250 | 251 | ```perl 252 | try { 253 | ... 254 | } ( 255 | '*' => sub { 256 | ... 257 | }, 258 | ); 259 | ``` 260 | 261 | 但しこの場合であれば, Try::Liteを使わずともTry::Tinyで実装可能です. 262 | Try::Liteが有用なのは, 次のように「Exception::Hoge」と「それ以外」の例外で, 処理を切り替えることが出来るという点です. 263 | 264 | ```perl 265 | try { 266 | ... 267 | } ( 268 | 'MyException::A' => sub { 269 | ... # MyException::Aの例外が起きた時のみ実行 270 | }, 271 | '*' => sub { 272 | ... # MyException::A以外の例外が起きた時のみ実行 273 | }, 274 | ); 275 | ``` 276 | 277 | ### 例外の再送 278 | 279 | Try::Tinyでは`catch`の次に記述する, 例外処理のためのサブルーチンリファレンスの第1引数として, Try::Liteでは`$@`を利用して, 発生した例外の詳細を取得することができました. 280 | 281 | 例外を`Exception::Tiny`を利用してクラスとして定義した場合, 発生した例外の詳細として, その例外のオブジェクトが渡ってきます. 282 | このように取得した例外のオブジェクトからは, `rethrow`メソッドが利用できるようになっています. 283 | このメソッドを利用することで, 例外を再度発生することが可能です(更に上に例外を渡す). 284 | 285 | ```perl:exception.pl 286 | use strict; 287 | use warnings; 288 | use Try::Tiny; 289 | use Data::Dumper; 290 | 291 | try { 292 | MyException::A->throw; 293 | } catch { 294 | my ($e) = @_; # `$e`には, 発生した`MyExceotion::A`のオブジェクトが渡ってくる 295 | $e->rethrow; # 再度例外(この場合は`MyExceotion::A`)が発生! 296 | }; 297 | 298 | package MyException::A; 299 | use parent 'Exception::Tiny'; 300 | ``` 301 | 302 | 実行結果は以下の通りです. 303 | 304 | ``` 305 | $ perl exception.pl 306 | MyException::A at exception.pl line 7. 307 | ``` 308 | 309 | また, `$e->rethrow`の代わりに, `die $e`や`Carp::croak $e`と書いても, 同じような結果を得ることが可能です. 310 | 基本的に, 例外を再度発生させる場合, `die`や`Carp::croak $e`を利用して再送すると良いでしょう. 311 | 312 | 例えば, 次のように`die`を利用して例外を発生させた場合, 例外の詳細は文字列として渡ってきます. 313 | 314 | ```perl:exception.pl 315 | use strict; 316 | use warnings; 317 | use Try::Tiny; 318 | use Data::Dumper; 319 | 320 | try { 321 | die "Exception!"; 322 | } catch { 323 | my ($e) = @_; 324 | print Dumper $e; 325 | }; 326 | ``` 327 | 328 | 実行結果は次の通りです. 329 | 330 | ``` 331 | $ perl exception.pl 332 | $VAR1 = 'Exception! at exp.pl line 7. 333 | '; 334 | ``` 335 | 336 | このように, `$e`は例外のオブジェクトではなく文字列になるため, `$e->rethrow`で例外を再発生することは出来ません. 337 | そのため, `die`や`Carp::croak`を利用すれば, `$e`が例外のオブジェクトであっても, 文字列であっても, 同様に例外を発生させることが可能です. 338 | 339 | ### モジュールにおける例外処理 340 | 341 | ここまで, Try::TinyやTry::Liteを利用した例外処理の実装方法について解説しました. 342 | 実は, ここまで学んできたPerlのモジュールの中には, 標準で例外処理の仕組みを持ったものも存在します. 343 | ここでは, それらの利用方法について解説を行います. 344 | 345 | #### `Teng` 346 | 347 | ORMとしてTengを利用する場合, 次のように`handle_error`メソッドを用意することで, データベースを操作する際などに例外が発生したときの例外処理を書くことが可能です. 348 | 349 | 例えば, TengでSQL文を直接入力して検索できる`search_by_sql`において, 350 | 351 | ```perl 352 | $teng->search_by_sql('SELECT * FOR user WHERE id = ?', [ 1 ]); 353 | ``` 354 | 355 | のように誤ったSQL(`FROM`が`FOR`になっている)を実行した場合, Tengは次のようなエラーメッセージを出力します. 356 | 357 | ``` 358 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 359 | @@@@@ Teng 's Exception @@@@@ 360 | Reason : DBD::SQLite::db prepare failed: near "FOR": syntax error at ... 361 | 362 | SQL : SELECT * FOR user WHERE id = ? 363 | BIND : $VAR1 = [ 364 | 1 365 | ]; 366 | 367 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 368 | at ... 369 | ``` 370 | 371 | これは, Tengの`handle_error`メソッドで出力されているので, `MyApp::DB`に`handle_error`メソッドを用意することでオーバーライドすることが可能です. 372 | 373 | ```perl:lib/MyApp/DB.pm 374 | package MyApp:DB; 375 | use utf8; 376 | use warnings; 377 | use parent 'Teng'; 378 | use Carp; 379 | 380 | ... 381 | 382 | sub handle_error { 383 | my ($self, $stmt, $bind, $reason) = @_; 384 | 385 | Carp::croak $reason; 386 | } 387 | 388 | 1; 389 | ``` 390 | 391 | 例えば, このように書き換えた場合は次のような出力になります. 392 | 393 | ``` 394 | DBD::SQLite::db prepare failed: near "FOR": syntax error at ... 395 | at ... 396 | ``` 397 | 398 | `handle_error`の利用例として, 例えば次のようなコードを用意すれば, UNIQUE制約を満たさなかった場合と, それ以外のエラーでTengから発生する例外を使い分けることが出来ます. 399 | 400 | ```perl:lib/MyApp/DB.pm 401 | package MyApp:DB; 402 | use utf8; 403 | use warnings; 404 | use parent 'Teng'; 405 | use Carp; 406 | 407 | ... 408 | 409 | sub handle_error { 410 | my ($self, $stmt, $bind, $reason) = @_; 411 | 412 | if ($reason =~ /DBD::mysql::st execute failed: Duplicate entry/) { 413 | MyApp::DB::Exception::DuplicateException->throw; 414 | } else { 415 | MyApp::DB::Exception->throw; 416 | } 417 | } 418 | 419 | package MyApp::DB::Exception; 420 | use parent 'Exception::Tiny'; 421 | 422 | package MyApp::DB::Exception::DuplicateException; 423 | use parent -norequire 'MyApp::DB::Exception'; 424 | 425 | 1; 426 | ``` 427 | 428 | #### `Amon2` 429 | 430 | Amon2では, ディスパッチャ(Basic Flavorの場合, `MyApp::Web::Dispatcher`)に`handle_exception`メソッドを定義することで, ディスパッチャのレイヤーで例外操作を行うことが可能です. 431 | 432 | ```perl:lib/MyApp/Dispatcher.pm 433 | package MyApp::Web::Dispatcher; 434 | use strict; 435 | use warnings; 436 | use utf8; 437 | use Amon2::Web::Dispatcher::RouterBoom; 438 | 439 | any '/' => sub { 440 | ... 441 | }; 442 | 443 | sub handle_exception { 444 | my ($class, $c, $e) = @_; 445 | 446 | ... 447 | } 448 | 449 | 1; 450 | ``` 451 | 452 | 例えば, `MyApp::Exception::ValidationError`が発生した場合は400を, それ以外の場合500を返したい場合であれば, 次のように実装することが可能です. 453 | 454 | ```perl:lib/MyApp/Dispatcher.pm 455 | package MyApp::Web::Dispatcher; 456 | use strict; 457 | use warnings; 458 | use utf8; 459 | use Amon2::Web::Dispatcher::RouterBoom; 460 | 461 | any '/' => sub { 462 | ... 463 | }; 464 | 465 | sub handle_exception { 466 | my ($class, $c, $e) = @_; 467 | 468 | if (UNIVERSAL::isa($e, 'MyApp::Exception::ValidationError')) { 469 | return $c->create_simple_status_page(400, 'BAD REQUEST'); 470 | } else { 471 | return $c->res_500; 472 | } 473 | } 474 | 475 | 1; 476 | ``` 477 | 478 | 第2引数はAmon2のコンテキスト, そして第3引数に発生した例外の詳細が渡ってくるので, これらを利用して任意の処理を記述することが出来ます. 479 | 上記の例にもあるように, バリデーションエラーは発生したタイミングですぐキャッチしてリカバリせず, この`handle_exception`を利用して処理するようにすれば, 例外処理のためのコードを`handle_exception`に集約することが可能です. 480 | 481 | ### 例外のテスト 482 | 483 | 「例外が起きたかどうか」をテストするためには, Test::Exceptionの利用が楽です. 484 | 485 | #### `dies_ok` 486 | 487 | `dies_ok`は, 任意のコードで例外が発生するかをテストすることが可能です. 488 | 489 | ```perl:exception.t 490 | use Test::More; 491 | use Test::Exception; 492 | 493 | dies_ok { die } 'Exception'; 494 | 495 | done_testing; 496 | ``` 497 | 498 | `dies_ok`は, 1つのサブルーチンリファレンスを受け取り, その中の処理で例外が発生していればテストが通ります. 499 | `dies_ok`では, 例外であればどのような例外が発生してもテストが通りますので, その詳細を確認したい場合, 後述の`throws_ok`を利用しましょう. 500 | 501 | #### `throws_ok` 502 | 503 | `throws_ok`は, `dies_ok`と同じく1つのサブルーチンリファレンスを受け取り, その中で例外が発生するかをテストすることが出来ます. 504 | `dies_ok`と異なり, 第2引数にどのような例外が発生したかを指定することが可能です(第1引数であるサブルーチンリファレンスと第2引数の間に`,`を入れてはいけません). 505 | 506 | 正規表現リテラル`qr//`を与えた場合, 正規表現リテラルが例外のメッセージと一致するかどうかを確認することが出来ます. 507 | また, 文字列で例外クラスのクラス名を与えることができ, 発生した例外がそのクラスのものかどうかを確認することが出来ます. 508 | 509 | ```perl:exception.t 510 | use Test::More; 511 | use Test::Exception; 512 | 513 | throws_ok { die "Exception!" } qr/Exception/; 514 | throws_ok { die "Exception!" } qr/xxxxxxxxx/; 515 | 516 | throws_ok { MyException::A->throw } 'MyException::A'; 517 | throws_ok { MyException::A->throw } 'MyException::B'; 518 | 519 | done_testing; 520 | 521 | package MyException::A; 522 | use parent 'Exception::Tiny'; 523 | ``` 524 | 525 | 実行結果は次の通りです. 526 | 527 | ``` 528 | $ perl exception.t 529 | ok 1 - threw Regexp ((?^:Exception)) 530 | not ok 2 - threw Regexp ((?^:xxxxxxxxx)) 531 | # Failed test 'threw Regexp ((?^:xxxxxxxxx))' 532 | # at 2.t line 5. 533 | # expecting: Regexp ((?^:xxxxxxxxx)) 534 | # found: Exception! at 2.t line 5. 535 | ok 3 - threw MyException::A 536 | not ok 4 - threw MyException::B 537 | # Failed test 'threw MyException::B' 538 | # at 2.t line 8. 539 | # expecting: MyException::B 540 | # found: MyException::A at 2.t line 8. 541 | 1..4 542 | # Looks like you failed 2 tests of 4. 543 | ``` 544 | 545 | # 例外の考え方 546 | 547 | 最後に, 例外を利用する際に意識しておくと良いポイントを簡単にまとめておきます. 548 | 549 | まず, 「例外が発生した」ということは, 処理中に何らかの異常が発生したということです. 550 | これをそのまま無視するのはプロダクトの品質に影響を及ぼす可能性があります. 551 | 例えば, データベースを操作する際に例外が発生した場合, そのままにしておくと予期せぬデータがデータベース挿入されるなど, データの不整合が発生するかもしれません. 552 | また, 場合によってはサービスそのものやミドルウェアの負荷向上であったり, セキュリティの問題にまで至る可能性もあるでしょう. 553 | 例外が発生した場合, それを無視せず, 極力必要なレイヤーで例外処理を行ってリカバリーするようにしましょう. 554 | 555 | しかしながら, 状況によってはリカバリー出来なかったり, 或いはリカバリーに失敗してしまう場合もあるでしょう. 556 | その場合, 例外を無理に隠蔽してプログラムを動かし続けてはいけません. 557 | 再度例外を投げるなどして, 必ず「処理が中断」する(そして, Webアプリケーションの場合は500を返したり, 異常事態が発生した事をユーザに伝える処理を入れる)ようにしましょう. 558 | 559 | # 参考資料 560 | 561 | - [例外設計における大罪](http://www.slideshare.net/t_wada/exception-design-by-contract) 562 | - [最近のPerl例外厨事情](http://www.songmu.jp/riji/entry/2013-08-05-exception.html) 563 | - Teng Advent Calendar 2011 [error handling](http://perl-users.jp/articles/advent-calendar/2011/teng/17) 564 | -------------------------------------------------------------------------------- /git.md: -------------------------------------------------------------------------------- 1 | # はじめに 2 | 3 | 複数人でプロダクトを開発する場合, 問題となるのは「コードの管理」です. 4 | 5 | 例えば, あるプロダクトのあるファイルを, 同時に2人の開発者が変更を加えたとします. 6 | それぞれがファイルを書き換え, 機能を追加した後, その2つのコードを統合するにはどうすればいいでしょう? 7 | 変更するファイルが1つであればまだ手動で頑張ることもできるでしょう. 8 | しかし統合するために書き換える必要があるファイルが10ファイル, 20ファイルと増えると, 人力で処理するのは現実的ではありません. 9 | 10 | 或いは, 過去のコードを残しておきたい, という場合もあると思います. 11 | よく使われる(使われていた)手として, 「2015-04-30」, 「2015-05-01」... のように日付ごとにディレクトリを切って, その中にその日の段階のプロダクトの全てのコードをコピーしておく(!?)というものがありますが... 12 | 言う前でもなく, その日変更がなかったファイルも含めて全てまるっと複製するのでディスク容量の負担になりますし, 間違えて過去のディレクトリのファイルを書き換えたり, 或いは削除してしまった場合, それはもはや「過去のコードを残す」という目的を果たしていません. 13 | 14 | このような問題を解決するために開発されたのが, バージョン管理システム(Version Control System)です. 15 | バージョン管理システムは, 独自のアルゴリズムに基づいて, 分岐したコードを1つのコードに結合(マージ)する手助けをしてくれます. 16 | また, コードに対する変更(コミット)の履歴を保持し, いつでも任意の状態に戻れるようにしてくれます. 17 | その際, コミットごとに全てのファイルを保存するのではなく, 前後の差分を保存するようになっているので, ディスク容量もさほど消費しません. 18 | 19 | 現代, 高速なプロダクト開発が求められている中で, 「複数人が同時にコードに変更を加える」という場面はもはや日常茶飯事となっています. 20 | そのため, プロダクトのコードを効率よく管理する「バージョン管理システム」に関する知識は, もはやエンジニアとしての必須の知識, 一般常識であると言うことができるでしょう. 21 | 22 | 今回は, 代表的なバージョン管理システムの1つ, Gitの使い方について学びます. 23 | 24 | # Git概要 25 | 26 | Gitは, Linuxカーネルの開発者Linus Torvaldsが, Linuxカーネルで利用するために開発したバージョン管理システムです. 27 | もともと, Linuxカーネルの開発にはBitKeeperという商用ツール(本来は有料のところ, BitKeeperを開発していたBitMover社の好意でLinuxカーネルの開発については無料で使えるようになっていた)が使われていました. 28 | これに対し, `rsync`などの開発者でもあるAndrew TridgellがBitKeeperのプロトコルをリバースエンジニアリング(といっても, 内容としてはBitKeeperのサーバの適切なportに対して, `help`とタイプするだけの簡単なものだったそうです)したところ, この行為をBitMover社はライセンス違反として扱い, Linuxカーネルに対するBitKeeperの利用を停止することになってしまいました. 29 | 当時, BitKeeper以外にも様々なバージョン管理システムがありましたが, Linuxカーネルという巨大なプロダクトに(特に速度面で)耐えうるバージョン管理システムは存在しなかった為, Linus氏はそれを自らの手で作ることにしました. 30 | これが, 現在Linuxカーネルだけでなく, Githubなどのホスティングサービスを軸に様々な企業で使われるようになったバージョン管理システム, 「Git」です. 31 | 32 | バージョン管理システムとしては, Gitの他にMercurialやBasaarなどが存在しますが, ここ最近はGitがバージョン管理システムの世界におけるデファクトスタンダードとなりつつあります(一部, Subversionが使われている所もあるでしょう). 33 | OSSの多くはGit(GitHub)でコードを管理していますし, 個人プロダクトの為にGitを使う場合もあれば, GitHubやBitBucketのOrganizationの機能を利用して, 企業のプロダクト開発のためにも, Gitが使われるようになっています. 34 | 35 | # リポジトリの作成と取得 36 | 37 | バージョン管理システムでは, ファイルの各バージョンの情報(= コミットの情報)を独自の仕組みで保持/管理しており, このデータを「リポジトリ」と呼びます. 38 | まずは, バージョン管理の軸となる, リポジトリを用意する方法について解説します. 39 | 40 | 既存のコードを新たにGitでバージョン管理したい場合, `git init`コマンドでリポジトリを作成することができます. 41 | 42 | ``` 43 | $ git init 44 | Initialized empty Git repository in /Users/username/git-tutorial/.git/ 45 | ``` 46 | 47 | `git init`コマンドを実行すると, カレントディレクトリに`.git`というディレクトリが作成され, ここにリポジトリの各情報が保存されるようになります(その為, `.git`ディレクトリは削除したり, 書き換えたりしないようにしなければいけません). 48 | これで, Gitのリポジトリの準備は完了です. 49 | 50 | 一方, 既存のリポジトリから新たに自分用のリポジトリを作成する場合, `git clone`というコマンドを利用します. 51 | 例えば, Githubにある[perl-entrance-org/Perl-Entrance-Textbook](https://github.com/perl-entrance-org/Perl-Entrance-Textbook)を取得する場合, 次のような手順が必要になります. 52 | 53 | ## リポジトリのURLの確認 54 | GitHubやBitBucketの場合, それぞれリポジトリのページにURLが掲載されています 55 | 56 | ### Githubの場合 57 | ![](/git/2.png) 58 | ### Bitbucketの場合 59 | ![](/git/1.png) 60 | 61 | 矢印の部分がそうです. 今回は, `git@github.com:perl-entrance-org/Perl-Entrance-Textbook.git`のようなURLが記載されています. 62 | 63 | なお, 64 | 65 | - Githubの場合, URLの下側の`HTTPS`や`SSH`などをクリック 66 | - Bitbucketの場合, URLの左側の`SSH`をクリック 67 | 68 | 69 | すると, リポジトリを取得する際に利用するプロトコルを変更することができます. 70 | 基本的には`SSH`を利用するようにしましょう. 71 | 72 | ## リポジトリの取得 73 | 74 | リポジトリのURLがわかれば, 後はリポジトリを取得するだけです. 75 | 76 | ``` 77 | $ git clone git@github.com:perl-entrance-org/Perl-Entrance-Textbook.git 78 | Cloning into 'Perl-Entrance-Textbook'... 79 | remote: Counting objects: 1459, done. 80 | remote: Compressing objects: 100% (1092/1092), done. 81 | remote: Total 1459 (delta 100), reused 1340 (delta 91) 82 | Receiving objects: 100% (1459/1459), 5.34 MiB | 2.23 MiB/s, done. 83 | Resolving deltas: 100% (100/100), done. 84 | Checking connectivity... done. 85 | 86 | $ ls 87 | 2015rookies 88 | ``` 89 | 90 | なお, 任意のリポジトリ名で`clone`したい場合, リポジトリのURLの後に希望するディレクトリ名を指定することができます. 91 | 92 | ``` 93 | $ git clone git@github.com:perl-entrance-org/Perl-Entrance-Textbook.git textbook 94 | Cloning into 'Perl-Entrance-Textbook'... 95 | remote: Counting objects: 1459, done. 96 | remote: Compressing objects: 100% (1092/1092), done. 97 | remote: Total 1459 (delta 100), reused 1340 (delta 91) 98 | Receiving objects: 100% (1459/1459), 5.34 MiB | 2.23 MiB/s, done. 99 | Resolving deltas: 100% (100/100), done. 100 | Checking connectivity... done. 101 | 102 | $ ls 103 | textbook 104 | ``` 105 | 106 | `git clone`した場合も`git init`した場合と同じく, `.git`ディレクトリでリポジトリを管理していますので, 書き換えや削除をしないように気をつけましょう. 107 | 108 | ## コラム: 分散型と集中型 109 | 110 | Gitは, バージョン管理システムの中でも「分散型」と呼ばれる仕組みに基づいています(対になるのが「集中型」, Subversionなどが該当します). 111 | 「分散型」と「集中型」はこのリポジトリの扱いが異なっていて, 「分散型」ではそれぞれのユーザ(開発者)がリポジトリを持っているのに対し, 「集中型」では1つのリポジトリを複数人で利用する, という仕組みになっています. 112 | 113 | 「集中型」の場合, コードに対する変更の履歴(コミット)は全て単一のリポジトリに保存されます. 114 | そのため, 過去のコードの履歴などを取得する場合, 必ずリポジトリから取得しなければなりません. 115 | また, リポジトリが設置されているサーバにネットワーク経由でアクセスできない場合, コミットを含む諸々の操作が出来なくなってしまいます. 116 | 更に, もしリポジトリが破損してしまった場合, 復元することは難しいので, 定期的にバックアップを取るなどの工夫も必要になってくるでしょう. 117 | 118 | 一方, 「分散型」の場合, リポジトリはそれぞれのユーザが保持しています. 119 | 分散型では, それぞれのユーザがリポジトリを持っているので, 集中型と異なり, ネットワークに繋がっていない状態でも(手元のリポジトリに対しては)作業を行うことができます. 120 | また, 「手元のリポジトリにはあるが, 他人のリポジトリやコアとなるリポジトリにはデータがない」状態になるので, 心理的に試行錯誤もやりやすいです(集中型の場合, コミットすれば常にコードがリポジトリに保存されてしまうので, 他の開発者がそのコードを容易に見ることが出来てしまいます). 121 | 更に, 分散型の場合, リポジトリがユーザの数だけ存在しているので, もしもコアとなるリポジトリが破損してしまったとしても, 開発者が持っているリポジトリから新しくコアとなるリポジトリを用意する, ということもできます. 122 | 難点としては, リポジトリが複数存在するため, 「自分のリポジトリに対する操作」と「他のリポジトリに対する操作」が別々に用意されており, その分コマンド体系が集中型に比べて複雑になってしまっている, という点が挙げられるでしょう. 123 | 124 | # コミット 125 | 126 | バージョン管理システムでは, コードの変更を「コミット」という単位で保存します. 127 | まずは, Gitでコードをコミットする方法について学びます. 128 | 129 | `git init`をしたディレクトリで, 適当なファイル(例えば, `README.md`)を作成してから, `git commit`でコミットしてみます. 130 | 131 | ``` 132 | $ git init 133 | $ touch README.md 134 | $ git commit 135 | On branch master 136 | 137 | Initial commit 138 | 139 | Untracked files: 140 | README.md 141 | 142 | nothing added to commit but untracked files present 143 | ``` 144 | 145 | Gitでは, コードをコミットする前に, 変更したファイルを「ステージ」という領域に追加する必要があります. 146 | 147 | ステージを使いこなすことで, 例えば「AとBというファイルを更新したとき, 先にAを(ステージに追加して)コミットしてから, Bを(ステージに追加して)コミットする」といった操作が出来るようになります. 148 | 一度に大量の変更を行う「大きなコミット」は, 後で見返した時に変更点が多く, コードを理解しにくくなります. 149 | ステージを利用して, 極力「1コミット = 1作業(変更)」にするよう, 心がけましょう. 150 | 151 | さて, 変更したファイルをステージに追加するためのコマンドは, `git add`です. 152 | 153 | ``` 154 | $ git add README.md 155 | $ git status 156 | On branch master 157 | 158 | Initial commit 159 | 160 | Changes to be committed: 161 | (use "git rm --cached ..." to unstage) 162 | 163 | new file: README.md 164 | ``` 165 | 166 | `git add`した後, `git status`でリポジトリの状態を確認しています. 167 | `git status`コマンドを利用すれば, 現在どのファイルが変更されていて, そのうちどのファイルがステージに追加されているかを確認することができます. 168 | 169 | さて, `git add`コマンドで, `README.md`をステージに追加することができました. 170 | なお, 複数のファイルを同時にステージに追加する場合, `git add -A`でリポジトリ内部の変更が行われた全ファイルをステージに追加することもできます. 171 | 172 | ファイルをステージに追加した後は, `git commit`でステージに追加したファイルのみをコミットすることができます. 173 | 174 | ``` 175 | $ git commit -m 'commit' 176 | [master (root-commit) 348094e] commit 177 | 1 file changed, 1 insertion(+) 178 | create mode 100644 README.md 179 | ``` 180 | 181 | ここでは, `-m`オプションでコミットログ`commit`を指定した上でコミットを実行しています. 182 | 183 | なお, `-m`オプションを指定していない場合, 自動的にエディタが開き, エディタを利用してコミットログを入力することが出来ます. 184 | コミットログを記入してからエディタを閉じると, 自動的に入力したテキストをコミットログにして, コミットを実行してくれます. 185 | 186 | # ログ 187 | 188 | コミットのログは, `git log`コマンドで確認することができます. 189 | 190 | ``` 191 | commit 348094e4b518b8f6513d454c9971541bab647750 192 | Author: papix 193 | Date: Thu Apr 30 22:54:42 2015 +0900 194 | 195 | commit 196 | ``` 197 | 198 | `commit:`の後にある, `348094e4b518b8f6513d454c9971541bab647750`という文字列は「ハッシュ」と呼ばれる文字列で, コミットを一意に特定するためのユニークな文字列です. 199 | 例えば, Gitには, 任意のコミットの詳細を確認する`git show`コマンドがあります. 200 | このハッシュは, `git show`コマンドで閲覧したいコミットを指定する為に利用することができます. 201 | 202 | ``` 203 | $ git show 348094e4b518b8f6513d454c9971541bab647750 204 | ``` 205 | 206 | なお, ハッシュは40桁ありますが, 前方一致で重なるものがなければ, それ以降は省略することができます. 207 | 大抵の場合, あるリポジトリに格納されたコミットについて, ハッシュの文字列の前方が4桁以上重複することは珍しいそうです. 208 | そのため, 209 | 210 | ``` 211 | $ git show 3480 212 | ``` 213 | 214 | のような入力でも, 概ね問題なく実行することができます. 215 | 216 | また, `git log`コマンドでは, グラフ形式でログを出力することも可能です. 217 | 218 | ``` 219 | $ git log --graph --date=short --decorate=short --pretty=format:'%Cgreen%h %Creset%cd %Cblue%cn %Cred%d %Creset%s' 220 | ... 前略 ... 221 | * d0ae127 2015-07-15 nqounet setup時は--deploymentを付けたほうが一人の環境に合わせ 222 | やすい(master) 223 | * 59c11a1 2015-07-08 Takuya Tsuchida riji publish 224 | * daab264 2015-07-08 Takuya Tsuchida Merge pull request #11 from tomcha/master 225 | |\ 226 | | * 9569293 2015-07-08 tomcha in福岡 住所修正 227 | | * 049c5ac 2015-07-08 tomcha in福岡の告知を更新 228 | | * 970ab43 2015-07-08 tomcha Update index.md 229 | |/ 230 | * 34e67e6 2015-07-04 papix すごく指摘されたので修正した 231 | * db29913 2015-07-04 papix oooooooooooooooops 232 | ... 後略 ... 233 | ``` 234 | 235 | これはPerl入学式の公式サイトのコードを管理しているリポジトリ内での実行例ですが, 後述するブランチの関係がグラフで可視化されており, 非常に確認しやすくなっています. 236 | 237 | # ブランチ 238 | 239 | Gitを含むバージョン管理システムには, 「ブランチ」と呼ばれる仕組みが存在します. 240 | ブランチを利用することで, リポジトリの中で任意のコミットを枝分かれさせ, 併存することが出来るようになります. 241 | 242 | Gitでは, デフォルトのブランチは`master`になっています. 243 | 現在のブランチとブランチの一覧は, `git branch`で確認することができます. 244 | 245 | ``` 246 | $ git branch 247 | * master 248 | ``` 249 | 250 | ## ブランチの使い方 251 | 252 | 例えば, `a`, `b`というコミットをした後に, 新しい機能を追加した`c`というコミットを`master`ブランチに作ったとしましょう. 253 | 254 | ``` 255 | master a ----> b ----> c 256 | ``` 257 | 258 | この後, `b`のコミットにバグが存在し, 修正することになったとしましょう. 259 | そのまま`master`ブランチでコミットしてしまうと, `b`の機能修正と`c`という機能追加のコミット(`d`)が混ざってしまいます. 260 | 261 | ``` 262 | | 新しい機能の追加 263 | v 264 | master a ----> b ----> c ----> d 265 | ^ 266 | | `b`の修正コミット 267 | ``` 268 | 269 | `b`の修正が`d`というコミットだけで終わればいいですが, この後`master`ブランチで機能追加のコミットと`b`の修正コミットを交互に繰り返してしまうと, 後で「新しく機能追加した部分だけ確認したい!」或いは「`b`の修正コミットだけ確かめたい!」となった際, 関係ないコミットを除外するのが非常に面倒になります. 270 | 271 | そこで役立つのが, ブランチです. 272 | 273 | Gitでは, 新しいブランチは`git branch `で作成することが出来ます. 274 | 275 | ``` 276 | $ git branch new-branch 277 | $ git branch 278 | * master 279 | new-branch 280 | ``` 281 | 282 | 更に, ブランチを作成する際, `git branch `のように指定することで, 任意のブランチやコミット(``)からブランチを作成することもできます. 283 | 今回の場合, `b`というコミットから, `b`のバグを修正する`fix-b`ブランチを生成するようにしてみましょう. 284 | 285 | 実はGitでは, 現在の先頭(最新)のコミットを`HEAD`で表せるようになっています. 286 | そして, その1つ親のブランチを`HEAD^`, 更に1つ親のブランチを`HEAD^^`のように指定することができます. 287 | 現在の`master`ブランチの最新のコミットは`c`ですので, `b`はその1つ親のコミットに相当します. 288 | そのため, `b`というコミットを指定するためには, (`git log`で`b`コミットのハッシュ値を確認して, ハッシュ値で指定する他に)`HEAD^`で指定することもできます. 289 | 290 | ``` 291 | $ git branch fix-b HEAD^ 292 | $ git branch 293 | fix-b 294 | * master 295 | ``` 296 | 297 | これで, `b`というコミットから`fix-b`というブランチを作ることができました. 298 | 299 | ``` 300 | | fix-bブランチ 301 | v 302 | master a ----> b ----> c 303 | ``` 304 | 305 | ブランチ間の移動については, `git checkout `で実現できます. 306 | 307 | ``` 308 | $ git checkout fix-b 309 | $ git branch 310 | * fix-b 311 | master 312 | ``` 313 | 314 | 現在, `fix-b`ブランチの最新のコミットは`b`であり, `c`で行った機能追加に関するコードは含まれていません. 315 | ここから, `fix-b`ブランチで`b`ブランチに存在したバグを修正し, コミットをすると, `fix-b`ブランチと`master`ブランチに関するコミットは次のような形になります. 316 | 317 | ``` 318 | fix-b +-----> d 319 | | 320 | master a ----> b ----> c 321 | ``` 322 | 323 | このように, トピック(例えば, 「A機能の追加」とか, 「B機能のバグの修正」とか...)単位でブランチを作る(「ブランチを切る」, とも言います)ことで, 同時に複数の作業を行っても, それらのコードが混ざることなく管理することが出来るようになります. 324 | 325 | ## ブランチに関連する操作 326 | 327 | ブランチの操作を学ぶにあたって, 実際にGitのリポジトリを作成して操作を体験してみるのも良いですが, 今回は[Learn Git Branching](http://k.swd.cc/learnGitBranching-ja/)を利用して学ぶこととします. 328 | 329 | このサービスは, Gitの次のコマンドについて, 実際に操作しながら学ぶことができます. 330 | 331 | - commit 332 | - branch 333 | - checkout 334 | - cherry-pick 335 | - reset 336 | - revert 337 | - rebase 338 | - merge 339 | 340 | ### 練習問題 341 | 342 | Learn Git Branchingが提供する課題のうち, 少なくとも以下のカリキュラムについては挑戦しておくことを推奨します. 343 | 344 | - 「まずはここから」の'1' 〜 '4' 345 | - 「次のレベルに進もう」の'1' 〜 '4' 346 | - 「Rebaseをモノにする」の'1' 〜 '2' 347 | 348 | なお, 「様々なTips」及び「応用編」については, 余裕があればチャレンジしてみるとよいでしょう. 349 | 350 | #### ヒント 351 | 352 | - `levels`でレベル選択画面へ遷移することができます 353 | - `reset`で作業を初期化することができます 354 | - `clear`でコマンドのログを消去することができます(これまでの作業内容は消えません) 355 | 356 | # 複数人でGitを使う 357 | 358 | 次に, 複数人でGitを使う際に必要となる, 次のコマンドについて解説を行います. 359 | 360 | - fetch 361 | - pull 362 | - push 363 | 364 | ## `origin`なリポジトリ 365 | 366 | `git clone`でリポジトリを取得した場合, その取得元であるリポジトリ(例えば, 先程`clone`で取得した「2015rookies」リポジトリの場合, BitBucketでホスティングされているGitリポジトリ)が`origin`として登録されます. 367 | 368 | 分散型バージョン管理システムであるGitでは, BitBucketがホスティングしているリポジトリだけでなく, 他の開発者が持っているリポジトリともやり取りすることが出来ます. 369 | しかしながら, 基本的にはリポジトリ間の操作は, `origin`として指定されている"コアとなるリポジトリ"(= GitHubやBitBucketといったリポジトリホスティングサービスや, Gitサーバに設置されているリポジトリ)とのやり取りだけで事足ります. 370 | 例えば, 他の開発者が自分の開発したコミットを取得したい場合, 直接自分のリポジトリから取得するのではなく, まず自分が`origin`で指定されたリポジトリにコミットを適用し, 他の開発者は`origin`のリポジトリから自分のコミットを取得すれば良いからです. 371 | 372 | そのため, Gitではコアとなるリポジトリを`origin`として登録することができ, `fetch`や`pull`, `push`といったリポジトリ間の操作を行うコマンドについて, 「デフォルトの操作対象」として使うことができます. 373 | 今回は, コマンドによる操作対象のリポジトリ(これを「リモートリポジトリ」と呼びます. 一方, 手元にあるリポジトリは「ローカルリポジトリ」と呼びます)は, 基本的には`origin`で指定されているリポジトリであるとして, 解説を進めていきます. 374 | 375 | ## `fetch` 376 | 377 | リモートリポジトリの変更点を取得するコマンドです. 378 | 例えば, ローカルリポジトリのmasterブランチより1つ進んだコミットがリモートリポジトリにある場合, `git fetch`コマンドを実行すると, 次のような出力が得られます. 379 | 380 | ``` 381 | $ git fetch 382 | remote: Counting objects: 3, done. 383 | remote: Total 3 (delta 0), reused 0 (delta 0) 384 | Unpacking objects: 100% (3/3), done. 385 | From github.com:your-name/your-repository.git 386 | d15cddc..d840590 master -> origin/master 387 | ``` 388 | 389 | これによって, リモートリポジトリの`master`ブランチの内容が, ローカルリポジトリの`origin/master`というブランチに適用されました. 390 | このブランチの内容は, 391 | 392 | ``` 393 | $ git checkout origin/master 394 | ``` 395 | 396 | でブランチを切り替え, 確認することができます. 397 | 398 | ここからもし, `origin/master`ブランチを, ローカルリポジトリの`master`ブランチにマージしたい場合, 399 | 400 | ``` 401 | $ git checkout master 402 | $ git merge origin/master 403 | ``` 404 | 405 | で, マージすることができます. 406 | 407 | ## `pull` 408 | 409 | 一言で言えば, `fetch`と`merge`を同時に行うコマンドです. 410 | 411 | 例えば, ローカルリポジトリの`master`ブランチで`git pull`を実行した場合, リモートリポジトリの`master`ブランチと差分がない場合は, 412 | 413 | ``` 414 | $ git pull 415 | Already up-to-date. 416 | ``` 417 | 418 | と表示されます. 一方, 差分がある場合は 419 | 420 | ``` 421 | remote: Counting objects: 3, done. 422 | remote: Total 3 (delta 0), reused 0 (delta 0) 423 | Unpacking objects: 100% (3/3), done. 424 | From github.com:your-name/your-repository.git 425 | d840590..e14eed7 master -> origin/master 426 | Updating d840590..e14eed7 427 | Fast-forward 428 | README.md | 2 +- 429 | 1 file changed, 1 insertion(+), 1 deletion(-) 430 | ``` 431 | 432 | このように, 自動的にリモートリポジトリの`master`ブランチの内容を, ローカルリポジトリにマージしてくれます. 433 | 434 | なお, この際`--rebase`オプションを指定すると, `merge`ではなく`rebase`を使ってマージすることもできます. 435 | 必要に応じて使い分けるようにしましょう. 436 | 437 | ### `pull`の際のコンフリクトとその解消 438 | 439 | リモートリポジトリから`pull`をする場合, 場合によってはコンフリクト(衝突)が発生する場合があります. 440 | 例えばこのように, リモートリポジトリの`master`から1つ進んだコミットをローカルリポジトリで作ったとしましょう(矢印がコミットの親子関係を示していて, `a`や`b`といったアルファベットが1つのコミットだと思ってください. 更に, `m`がローカルの`master`ブランチの最新のコミット, `M`がリモートの`master`ブランチの最新のコミットであるとします). 441 | 442 | ``` 443 | m 444 | v 445 | +-----> c 446 | | 447 | a ----> b 448 | ^ 449 | M 450 | ``` 451 | 452 | しかし, その作業をしている間, 別のユーザがリモートリポジトリの`master`から別のコミットを作って, リモートリポジトリに`push`していたとします(下図における`x`のコミット). 453 | 454 | ``` 455 | m 456 | v 457 | +-----> c 458 | | 459 | a ----> b ----> x 460 | ^ 461 | M <- 別のユーザがcommitして, pushした! 462 | ``` 463 | 464 | この状態で`git pull`をした場合, Gitは可能な限り`c`と`x`のコミットをマージしようとします. 465 | しかし, 自動的なマージに失敗した場合, 次のような出力を表示します. 466 | 467 | ``` 468 | $ git pull 469 | Auto-merging README.md 470 | CONFLICT (content): Merge conflict in README.md 471 | Automatic merge failed; fix conflicts and then commit the result. 472 | ``` 473 | 474 | この場合, ユーザが手動でコンフリクトを解消しなければなりません. 475 | 出力には, `README.md`でコンフリクトが発生している, と表示されているので, 確認してみます. 476 | 477 | ``` 478 | <<<<<<< HEAD 479 | # こんにちはGit!!! 480 | 481 | こんにちはこんにちは! 482 | ======= 483 | # こんにちはGit!!!!!!!! 484 | >>>>>>> 5dd36fe276784f9d18287279d91dfc19000c01fd 485 | ``` 486 | 487 | `<<<<<<< HEAD`から`=======`で囲まれた部分がローカルリポジトリの内容で, `=======`から`>>>>>>> 5dd36fe276784f9d18287279d91dfc19000c01fd`で囲まれた部分がリモートリポジトリの内容を指しています. 488 | Gitは, この部分についてどちらの内容を採択すればいいか判断できなかった, ということです. 489 | 490 | コンフリクトは1つのファイルで複数生じる場合もありますし, もしリポジトリに複数のファイルが存在するのであれば, 同時に複数のファイルで生じることもあります. 491 | ただ, コンフリクトが起きているファイルについては, `git pull`をした時に全て確認することが出来ますし, `git status`コマンドで確認することもできます. 492 | 493 | ``` 494 | $ git status 495 | On branch master 496 | Your branch and 'origin/master' have diverged, 497 | and have 1 and 1 different commit each, respectively. 498 | (use "git pull" to merge the remote branch into yours) 499 | You have unmerged paths. 500 | (fix conflicts and run "git commit") 501 | 502 | Unmerged paths: 503 | (use "git add ..." to mark resolution) 504 | 505 | both modified: README.md 506 | 507 | no changes added to commit (use "git add" and/or "git commit -a") 508 | ``` 509 | 510 | それでは, `README.md`のコンフリクトを解消しましょう. 511 | 512 | ``` 513 | # こんにちはGit!!!!!!!! 514 | 515 | こんにちはこんにちは! 516 | ``` 517 | 518 | `README.md`を正しい(期待する)内容に書き換え, `git add .`してから`git commit`します. 519 | 520 | ``` 521 | $ git add . 522 | $ git commit 523 | ``` 524 | 525 | すると, 次のような文面が入力された状態でエディタが開くはずです. 526 | 527 | ``` 528 | Merge branch 'master' of github.com:your-name/your-repository.git 529 | 530 | # Conflicts: 531 | # README.md 532 | # 533 | # It looks like you may be committing a merge. 534 | # If this is not correct, please remove the file 535 | # .git/MERGE_HEAD 536 | # and try again. 537 | 538 | 539 | # Please enter the commit message for your changes. Lines starting 540 | # with '#' will be ignored, and an empty message aborts the commit. 541 | # On branch master 542 | # Your branch and 'origin/master' have diverged, 543 | # and have 1 and 1 different commit each, respectively. 544 | # (use "git pull" to merge the remote branch into yours) 545 | # 546 | # All conflicts fixed but you are still merging. 547 | # 548 | # Changes to be committed: 549 | # modified: README.md 550 | # 551 | ``` 552 | 553 | 基本的に, このままの文面でコミットして構いません. 554 | このコミットは, 異なるリポジトリ間のコミットをマージしているので, 「マージコミット」と呼ぶこともあります. 555 | 556 | ``` 557 | $ git commit 558 | [master ea69b8e] Merge branch 'master' of github.com:your-name/your-repository.git 559 | ``` 560 | 561 | このとき, ローカル/リモートの`master`ブランチの最新コミットは, 次のようになります(`d`が, ローカルリポジトリの`c`とリモートリポジトリの`x`の内容をマージした, マージコミットです). 562 | 563 | ``` 564 | +-----> c ------| 565 | | v 566 | a ----> b ----> x ----> d 567 | ^ ^ 568 | M m 569 | ``` 570 | 571 | なお, ここで紹介したコンフリクトの解決方法は, ローカルリポジトリにあるブランチ同士の`merge`でコンフリクトが発生した場合と同じです(前述の通り, `git pull`は`git fetch`と`git merge`を同時に実行するコマンドだからです). 572 | もし, ローカルリポジトリにあるブランチをマージしてコンフリクトした場合も, `git pull`でコンフリクトが生じた時と同じように, 慌てず対処していきましょう. 573 | 574 | ## `push` 575 | 576 | `fetch`や`pull`はリモートリポジトリの内容を取得するコマンドでしたが, `push`は逆にリモートリポジトリへ更新を送り込むコマンドです. 577 | 例えばローカルリポジトリの`master`ブランチで`git push`を実行したとします. 578 | もし, ローカルリポジトリの`master`ブランチとリモートリポジトリの`master`ブランチに差分がないのであれば, 579 | 580 | ``` 581 | $ git push 582 | Everything up-to-date 583 | ``` 584 | 585 | このような出力になります. 586 | 587 | 一方, ローカルリポジトリの`master`ブランチがリモートリポジトリの`master`ブランチより進んでいる場合, 588 | 589 | ``` 590 | $ git push 591 | Counting objects: 3, done. 592 | Writing objects: 100% (3/3), 239 bytes | 0 bytes/s, done. 593 | Total 3 (delta 0), reused 0 (delta 0) 594 | To github.com:your-name/your-repository.git 595 | e14eed7..5dd36fe master -> master 596 | ``` 597 | 598 | このように, ローカルリポジトリの`master`ブランチをリモートリポジトリの`master`ブランチに適用することができます. 599 | 600 | ### `push`の際のコンフリクトとその解消 601 | 602 | `git pull`と同様, `git push`の際もコンフリクトが起こる場合があります. 603 | 604 | ``` 605 | m 606 | v 607 | +-----> c 608 | | 609 | a ----> b ----> x 610 | ^ 611 | M <- 別のユーザがcommitして, pushした! 612 | ``` 613 | 614 | このような状態で, ローカルの`master`ブランチをリモートに`push`しようとした場合, コンフリクトが発生し, 次のようなエラーが表示されます. 615 | 616 | ``` 617 | $ git push 618 | To github.com:your-name/your-repository.git 619 | ! [rejected] master -> master (fetch first) 620 | error: failed to push some refs to 'github.com:your-name/your-repository.git' 621 | hint: Updates were rejected because the remote contains work that you do 622 | hint: not have locally. This is usually caused by another repository pushing 623 | hint: to the same ref. You may want to first integrate the remote changes 624 | hint: (e.g., 'git pull ...') before pushing again. 625 | hint: See the 'Note about fast-forwards' in 'git push --help' for details. 626 | ``` 627 | 628 | 解決するためには, エラー文にも書かれている通り, `git pull`でリモートリポジトリの更新を取り込んでから`push`してやればOKです. 629 | ここから先の`git pull`の手順については, 前述の「`pull`の際のコンフリクトとその解消」と同様ですので省略します. 630 | 631 | `git pull`でリモートリポジトリの内容をマージすることが出来れば, 次のように`git push`が無事成功します. 632 | 633 | ``` 634 | $ git push 635 | Counting objects: 6, done. 636 | Delta compression using up to 4 threads. 637 | Compressing objects: 100% (3/3), done. 638 | Writing objects: 100% (6/6), 541 bytes | 0 bytes/s, done. 639 | Total 6 (delta 0), reused 0 (delta 0) 640 | To github.com:your-name/your-repository.git 641 | 5dd36fe..ea69b8e master -> master 642 | ``` 643 | 644 | ローカル/リモートの`master`ブランチの最新コミットは, 次のようになっているはずです. 645 | 646 | ``` 647 | +-----> c ------| 648 | | v 649 | a ----> b ----> x ----> d 650 | ^ 651 | m/M 652 | ``` 653 | 654 | ### コラム: `push -f`の恐怖 655 | 656 | `push`する際, コンフリクトを無視して無理やり`push`を行う`-f`オプション(`force`の`f`)がありますが, 特にチームで開発を行う際には必ず使わないようにしましょう. 657 | もし`push -f`で無理やり`push`してしまうと, ローカル/リモートリポジトリ間のコミットの関係が崩壊し, 解消が非常に面倒なことになります. 658 | 659 | このため, 一部界隈では`push -f`は「核爆弾」と呼ばれ, 恐れられて(嫌われて)いたりします. 660 | 661 | # 練習問題 662 | 663 | BitBucketの個人アカウントで`git-training`というリポジトリを作成し, 手元のマシンへ`clone`して, ここまで学んできたGitの使い方を振り返ってみよう. 664 | 665 | - コミット 666 | - ブランチの作成 667 | - ブランチのマージ 668 | - コードの`push` 669 | - コードの`pull` 670 | - コンフリクトが生じる状態で`pull`をして, コンフリクトを解消してから`push` 671 | 672 | なお, それぞれの課題を達成するために実行したコマンドの履歴については, 課題提出用リポジトリの指定されたディレクトリに`git-training.md`というファイルを用意して, そこに書くようにしよう. 673 | また気がついた事や苦戦したこと, メモしておきたい事があれば, これもファイル内に記載しておくようにしよう. 674 | 675 | ## ヒント 676 | 677 | ``` 678 | $ git clone github.com:your-name/your-repository.git user1 679 | $ git clone github.com:your-name/your-repository.git user2 680 | ``` 681 | 682 | のように, 別々のディレクトリでリポジトリを`clone`すれば, コンフリクト状態の再現が簡単にできます(`user1`ディレクトリでコミットし, リモートリポジトリに`push`した後, `user2`ディレクトリでコミットし, リモートリポジトリに`push`すれば, 「`pull`の際のコンフリクトとその解消」で紹介したような状況を作り出すことができます). 683 | -------------------------------------------------------------------------------- /git/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/git/1.png -------------------------------------------------------------------------------- /git/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/git/2.png -------------------------------------------------------------------------------- /homebrew.md: -------------------------------------------------------------------------------- 1 | # 諸注意 2 | 3 | HomebrewはMac OS X用のアプリケーションです. 予めご了承下さい. 4 | 5 | # はじめに 6 | 7 | プログラムを開発する場面では, 様々なツールやライブラリが必要になります. 8 | プログラミング言語そのものもそうですし, 例えばデータベースを使うのであればMySQLやSQLite, KVSが必要であればRedisやMemcached, VMで開発環境を用意するのであればVagrantが必要ですし, デプロイやサーバの環境構築にはCapistranoやAnsibleなどが必要になるでしょう. 9 | 10 | 通常, このようなツールやライブラリを導入してMac上に開発環境を整える際は, ツールやライブラリをそれぞれ1つずつ, Applicationディレクトリに配置したり, インストーラを使ったりしてインストールしていくと思います. 11 | もちろんそれでも良いのですが, 例えば依存となるツールやライブラリがある場合は別途導入する必要がありますし, ツールやライブラリそのものの更新も, 1つずつ対応しなければなりません. 12 | 13 | このような問題を解決するのが, Mac OS X向けのパッケージ管理ツール, [Homebrew](http://brew.sh/index_ja.html)です. 14 | Homebrewを利用すれば, CentOSの`yum`やUbuntuの`apt-get`のように, 例えばテキストエディタのvimであれば`brew install vim`でインストールすることができますし, その際依存となるライブラリやツールがあればそのインストールも自動的に実施してくれます. 15 | また, Homebrewで導入済みのライブラリやツールについては, `brew upgrade`で全て(Homebrewが対応している)最新版に更新することができます. 16 | 17 | まだHomebrewを利用していないのであれば, **(研修カリキュラムの一部はHomebrewの導入が前提となっているので)**この機会に是非Homebrewを基盤にした開発環境を構築しましょう. 18 | 19 | # Homebrewの導入 20 | 21 | Homebrewを導入する前に, XcodeのCommand Line Toolsを導入しておく必要があります. 22 | まだ導入していない場合, 以下のコマンドを入力しておきましょう. 23 | 24 | ``` 25 | $ xcode-select --install 26 | ``` 27 | 28 | XcodeのCommand Line Toolsが導入できていれば, 後は[Homebrew](http://brew.sh/index_ja.html)の公式サイトに書かれている通り, 以下のコマンドで導入することができます. 29 | 30 | ``` 31 | $ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 32 | ``` 33 | 34 | ## コラム: Homebrew Cask 35 | 36 | Homebrewが対応しているのは, プログラムの開発に利用するツールやライブラリが主で, 例えばHipChatのクライアントやChromeといったブラウザ(GUIアプリケーション)には対応していません. 37 | このようなアプリケーションをHomebrewで管理するための拡張として, [Homebrew Cask](http://caskroom.io/)が開発されています. 38 | 39 | 別途, [Homebrew-file](https://github.com/rcmdnk/homebrew-file)などの仕組みを利用すれば, 新しいMacを用意した後, まずHomebrewを入れてから, Homebrew経由で各種アプリケーションやツール, ライブラリを導入すれば, 開発環境がほぼ完成する, といった環境を準備することができます(自分の場合, Homebrew-fileなどを利用せず, `brew install ...`を連ねて書いたシェルスクリプトを各PCで共有して管理しています). 40 | 41 | なお, 本研修では, Homebrew CaskやHomebrew-fileの導入方法や使い方について詳しく解説しません. 42 | 興味がある方は, 次の記事を参考に挑戦してみて下さい. 43 | 44 | - [homebrew cask](http://qiita.com/kon/items/8c5396d8de42a4c34818) 45 | - [homebrew-caskを使って簡単にMacの環境構築をしよう!](http://nanapi.co.jp/blog/2014/03/05/homebrew-cask/) 46 | - [Homebrew-fileでhomebrewでインストールしたパッケージの管理をする](http://www.task-notes.com/entry/20150316/1426474800) 47 | 48 | # Homebrewの使い方 49 | 50 | ここからは, Homebrewの使い方を見て行きましょう. 51 | なお, Homebrewのバージョンは2015年3月時点の最新版の0.9.5とします. 52 | 53 | ``` 54 | $ brew 55 | Example usage: 56 | brew [info | home | options ] [FORMULA...] 57 | brew install FORMULA... 58 | brew uninstall FORMULA... 59 | brew search [foo] 60 | brew list [FORMULA...] 61 | brew update 62 | brew upgrade [FORMULA...] 63 | brew pin/unpin [FORMULA...] 64 | 65 | Troubleshooting: 66 | brew doctor 67 | brew install -vd FORMULA 68 | brew [--env | config] 69 | 70 | Brewing: 71 | brew create [URL [--no-fetch]] 72 | brew edit [FORMULA...] 73 | open https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/Formula-Cookbook.md 74 | 75 | Further help: 76 | man brew 77 | brew home 78 | ``` 79 | 80 | なお, Homebrewにおいては, ライブラリやコマンドは「Formula」というビルド手順書単位で管理することになります. 81 | 例えばPerlをインストールするのであれば, 後述の`install`コマンドに対して, Perlのビルド手順書が書かれた`perl`というFormulaを指定することで実現することができます. 82 | 83 | Formulaでは, ライブラリやツールをビルドする手順に加えて, 依存となるライブラリやツールの指定もできますので, 導入したいライブラリやコマンドのインストールを指定するだけで, その依存となるライブラリやツールも自動的にインストールすることができます. 84 | Homebrewで利用できるFormulaについては, 後述の`search`コマンドで検索したり, `info`コマンドで詳細を確認することができます. 85 | 86 | ## `install` 87 | 88 | Homebrewを利用して, Formulaからツールをインストールします. 89 | 例えばemacsをインストールするのであれば, 「emacs」というFormulaを利用して, `brew install emacs`でOKです. 90 | 91 | ``` 92 | $ brew install emacs 93 | ==> Downloading https://homebrew.bintray.com/bottles/emacs-24.4.mavericks.bottle.3.tar.gz 94 | ####################################################################### 100.0% 95 | ==> Pouring emacs-24.4.mavericks.bottle.3.tar.gz 96 | ==> Caveats 97 | To have launchd start emacs at login: 98 | ln -sfv /usr/local/opt/emacs/*.plist ~/Library/LaunchAgents 99 | Then to load emacs now: 100 | launchctl load ~/Library/LaunchAgents/homebrew.mxcl.emacs.plist 101 | 102 | WARNING: launchctl will fail when run under tmux. 103 | ==> Summary 104 | 🍺 /usr/local/Cellar/emacs/24.4: 3914 files, 104M 105 | ``` 106 | 107 | `which`で`emacs`コマンドの位置を確認すると, 次のように表示されるはずです. 108 | 109 | ``` 110 | $ which emacs 111 | /usr/local/bin/emacs 112 | ``` 113 | 114 | Homebrewは, `/usr/local/Cellar`以下にFormula単位でライブラリやコマンドをインストールし, これらの`bin`ディレクトリに配置されているファイルのエイリアス(シンボリックリンク)をパスが通っている`/usr/local/bin`以下に配置することで, Formulaを利用してインストールしたライブラリやコマンドの管理を実現しています. 115 | 116 | ## `info` 117 | 118 | Homebrewが扱えるFormulaの詳細を確認できます. 119 | 120 | ``` 121 | $ brew info emacs 122 | emacs: stable 24.4 (bottled), devel 24.4-dev, HEAD 123 | https://www.gnu.org/software/emacs/ 124 | /usr/local/Cellar/emacs/24.4 (3914 files, 104M) * 125 | Poured from bottle 126 | From: https://github.com/Homebrew/homebrew/blob/master/Library/Formula/emacs.rb 127 | ==> Dependencies 128 | Build: xz ✔, pkg-config ✔ 129 | Optional: d-bus ✘, gnutls ✘, librsvg ✘, imagemagick ✘, mailutils ✘, glib ✔ 130 | ==> Options 131 | --cocoa 132 | Build a Cocoa version of emacs 133 | --keep-ctags 134 | Don't remove the ctags executable that emacs provides 135 | --with-d-bus 136 | Build with d-bus support 137 | --with-glib 138 | Build with glib support 139 | --with-gnutls 140 | Build with gnutls support 141 | --with-imagemagick 142 | Build with imagemagick support 143 | --with-librsvg 144 | Build with librsvg support 145 | --with-mailutils 146 | Build with mailutils support 147 | --with-x11 148 | Build with x11 support 149 | --devel 150 | Install development version 24.4-dev 151 | --HEAD 152 | Install HEAD version 153 | ==> Caveats 154 | To have launchd start emacs at login: 155 | ln -sfv /usr/local/opt/emacs/*.plist ~/Library/LaunchAgents 156 | Then to load emacs now: 157 | launchctl load ~/Library/LaunchAgents/homebrew.mxcl.emacs.plist 158 | 159 | WARNING: launchctl will fail when run under tmux. 160 | ``` 161 | 162 | インストール時に表示する`Caveats`も表示されるので, 見逃してしまった場合は`info`コマンドで確認しましょう. 163 | 164 | なお, `info`コマンドは, Homebrewで管理できるが, まだインストールしていないコマンドに対しても実行することができます. 165 | 166 | ``` 167 | ruby: stable 2.2.1 (bottled), HEAD 168 | https://www.ruby-lang.org/ 169 | Not installed 170 | From: https://github.com/Homebrew/homebrew/blob/master/Library/Formula/ruby.rb 171 | ==> Dependencies 172 | Build: pkg-config ✔ 173 | Required: libyaml ✔, openssl ✔ 174 | Recommended: readline ✔ 175 | Optional: gdbm ✔, gmp ✘, libffi ✔ 176 | ==> Options 177 | --universal 178 | Build a universal binary 179 | --with-doc 180 | Install documentation 181 | --with-gdbm 182 | Build with gdbm support 183 | --with-gmp 184 | Build with gmp support 185 | --with-libffi 186 | Build with libffi support 187 | --with-suffix 188 | Suffix commands with '22' 189 | --with-tcltk 190 | Install with Tcl/Tk support 191 | --without-readline 192 | Build without readline support 193 | --HEAD 194 | Install HEAD version 195 | ``` 196 | 197 | 未インストールの場合, このように3行目がインストール先ではなく「Not installed」と表示されます. 198 | 199 | ### `search` 200 | 201 | Homebrewで扱えるFormulaを検索します. 202 | `brew search perl`のように引数を与えた場合, その引数で指定した文字列に関係のあるFormulaが表示されます. 203 | 204 | ``` 205 | $ brew search perl 206 | perl perl-build perlmagick 207 | ``` 208 | 209 | また, `brew search`のみで実行した場合, Homebrewで管理できるツールやライブラリの一覧を表示します. 210 | 211 | ``` 212 | $ brew search 213 | a2ps apachetop autojump bibtex2html cadaver 214 | a52dec ape automake bibtexconv cadubi 215 | aacgain apg automoc4 bibutils cairo 216 | aalib apgdiff automysqlbackup bigdata cairomm 217 | aamath apib autopano-sift-c bigloo calabash 218 | ... 後略 ... 219 | ``` 220 | 221 | ### `list` 222 | 223 | 現在Homebrewで管理しているツールやライブラリの一覧を表示します. 224 | 225 | ``` 226 | $ brew list 227 | ansible giflib libyaml ricty 228 | autoconf git lua ruby-build 229 | automake glib luajit scons 230 | bison go lv selenium-server-standalone 231 | brew-cask gobject-introspection mercurial sqlite 232 | ... 後略 ... 233 | ``` 234 | 235 | ### `update` 236 | 237 | Homebrew本体や, Formulaの更新を行うコマンドです. 238 | 239 | ``` 240 | $ brew update 241 | Updated Homebrew from 01463d23 to 49d0e7a6. 242 | ==> New Formulae 243 | git-plus 244 | ==> Updated Formulae 245 | curl encfs jenkins markdown peco/peco/peco vegeta 246 | ecj git libxmp multimarkdown syncthing 247 | elasticsearch heroku-toolbelt lnav openconnect trafficserver 248 | ``` 249 | 250 | ### `upgrade` 251 | 252 | Homebrewで管理しているツールやライブラリを更新します. 253 | 254 | ``` 255 | $ brew upgrade 256 | ==> Upgrading 1 outdated package, with result: 257 | git 2.3.4 258 | ==> Upgrading git 259 | ==> Downloading https://homebrew.bintray.com/bottles/git-2.3.4.yosemite.bottle.tar.gz 260 | ####################################################################### 100.0% 261 | ==> Pouring git-2.3.4.yosemite.bottle.tar.gz 262 | ==> Caveats 263 | The OS X keychain credential helper has been installed to: 264 | /usr/local/bin/git-credential-osxkeychain 265 | 266 | The "contrib" directory has been installed to: 267 | /usr/local/share/git-core/contrib 268 | 269 | Bash completion has been installed to: 270 | /usr/local/etc/bash_completion.d 271 | 272 | zsh completion has been installed to: 273 | /usr/local/share/zsh/site-functions 274 | ==> Summary 275 | 🍺 /usr/local/Cellar/git/2.3.4: 1362 files, 31M 276 | ``` 277 | 278 | ### `uninstall` 279 | 280 | Homebrewでインストールしたツールやライブラリを削除します. 281 | 282 | ``` 283 | $ brew uninstall emacs 284 | Uninstalling /usr/local/Cellar/emacs/24.4... 285 | ``` 286 | -------------------------------------------------------------------------------- /infrastructure-as-code/ansible.md: -------------------------------------------------------------------------------- 1 | # Ansible 2 | 3 | 先程, Vagrantの練習問題で簡単なWebアプリケーションの環境構築をVagrant上に「手動で」行いました. 4 | 練習問題では, 構築する台数が1台だったので何とかなりましたが, もし同じ環境を10台作って欲しい, と言われたらどう思いますか? 5 | 手動で行えば, 10倍の時間がかかりますし, その間にコマンドの入力ミス(オペレーションミス, オペミス)など起こしてしまう可能性も十分あるでしょう. 6 | 7 | こんな時に役立つのが, 環境構築を自動的に行ってくれる「プロビジョニングツール」と呼ばれるツール類です(「構成管理ツール」と呼ぶこともあります). 8 | プロビジョニングツールを使えば, 構築手順(構築手順を記述する方法は, プロビジョニングツールによって異なっています)をコードとして書いてしまいさえすえば, それを複数のサーバに適用することが出来るようになります. 9 | エンジニアは, プロビジョニングツールに対して環境構築を行うように命令するだけで済むので, 手動で構築する際に起こりえるオペレーションミスも防ぎやすいです. 10 | 11 | 近年, サービスのインフラストラクチャをコードで表現/実現する「Infrastructure as Code」が流行していますが, プロビジョニングツールはその中の「環境構築」という位置領域を担うツールと言うことができます. 12 | 13 | 後述しますが, 「Infrastructure as Code」の流行によって, 近年様々なプロビジョニングツールが産まれています. 14 | そのため, 各社/各チームにとって適切なプロビジョニングツールを選択していく必要性があるでしょう. 15 | 今回は, Python製のプロビジョニングツールであるAnsible(アンシブル)を使って, プロビジョニングツールとInfrastructure as Codeを体験していきます. 16 | 17 | ## 代表的なプロビジョニングツール 18 | 19 | |名前 |特徴 | 20 | |:--------|:------------------------------------------------------------------------------------------------------------------| 21 | | Ansible | 今回紹介するプロビジョニングツール. Python製. 構築手順はYAMLで記述する. 他のツールと比べると利用までの障壁は低い. | 22 | | Chef | Ruby製. 構築手順は「レシピ」と呼ばれ, Rubyで記述する. プロビジョニングツールとしては多機能. | 23 | | Fabric | Python製. 構築手順はPythonで記述する. Ansibleよりもプログラマブルに構築手順を記述できる. Chefよりはシンプル. | 24 | | Itamae | Ruby製. Rubyで構築手順を記述でき, Chefよりもシンプルなプロビジョニングツール. | 25 | 26 | この他, プロビジョニングツールで構築した環境に, アプリケーションをデプロイするためのデプロイツールと呼ばれるツールもあります. 27 | 例えば, Capistrano(Ruby)やCinnamon(Perl)などが有名ですが, これらのデプロイツールとプロビジョニングツールの境界はかなり曖昧です. 28 | 実際に, プロビジョニングツールのAnsibleでプロビジョニングだけでなくデプロイも行う, という事例もあります. 29 | 30 | # Ansibleのインストール 31 | 32 | Homebrewからインストールすることができます. 33 | 34 | ``` 35 | $ brew install ansible 36 | ``` 37 | 38 | Python製のツールなので, `easy_install`や`pip`でもインストールできます. 39 | ただ, Mac OS XであればHomebrewでインストールするのが一番手っ取り早いですし, 安心でしょう. 40 | 41 | # Ansible入門 42 | 43 | ## 仮想マシンの準備 44 | 45 | それでは, まずAnsibleで環境構築を行う対象となる仮想マシンを作っていきましょう. 46 | 47 | ``` 48 | $ vagrant init ubuntu/trusty64 49 | $ vagrant up 50 | ``` 51 | 52 | これまで学んできたように, これで仮想マシンの準備は完了です. 53 | 54 | Vagrantを利用するのであれば, `ssh`コマンド経由でVagrantに接続出来るようにしておくと楽になります. 55 | `ssh-config`で, この仮想マシンに接続するために必要な設定を出力し, `.ssh/config`に追記しましょう. 56 | 57 | ``` 58 | $ vagrant ssh-config --host vagrant >> ~/.ssh/config 59 | ``` 60 | 61 | 接続出来るか確認してみましょう. 62 | 63 | ``` 64 | $ ssh vagrant 65 | Welcome to Ubuntu 14.04.4 LTS (GNU/Linux 3.13.0-85-generic x86_64) 66 | ... 中略 ... 67 | vagrant@vagrant-ubuntu-trusty-64:~$ 68 | ``` 69 | 70 | 問題ないですね. 71 | 72 | ## `hosts`とPlaybookの準備 73 | 74 | Ansibleで環境構築を行うためには, 予めいくつかの準備が必要となります. 75 | 76 | まずは`hosts`ファイルです. 77 | このファイルには, 環境構築を行う対象となるホストを, IPないしホスト名で記述していきます. 78 | ファイルはini形式で記載することができ, ホストのロール(役割)に応じてホストの集合をグループとして定義することもできます. 79 | 80 | 例えば, アプリケーションを動かすサーバが3台(`app1`〜`app3`), データベース用のサーバが2台(`db1`〜`db2`)があるのであれば, 次のように書くと良いでしょう. 81 | 82 | ``` 83 | [app] 84 | app1 85 | app2 86 | app3 87 | 88 | [db] 89 | db1 90 | db2 91 | ``` 92 | 93 | 今回は, Amon2::Liteで作成した簡単なアプリケーションを動かすサーバが1台だけなので, 次のように設定しておきます. 94 | 95 | ``` 96 | [app] 97 | vagrant 98 | ``` 99 | 100 | ### Playbook 101 | 102 | Playbookは, Ansibleにおける「構築手順書」そのものです. 103 | 先程設定した`hosts`に記載したホストやグループに, Playbookを適用することで, Ansibleは環境構築を行います. 104 | 105 | Ansibleでは, PlaybookはYAML形式で記述します. 106 | まずは試しに, 仮想マシンの`/tmp`ディレクトリに, 「Hello, Ansible!」と書かれた「ansible.txt」というファイルを設置するようなPlaybookを書いてみることとします. 107 | 108 | Playbookの書き方についての詳細は後述します. 109 | まずはとりあえず, このPlaybookをVagrantで作った仮想マシンに適用するところまでをやってみましょう. 110 | 111 | ```yaml 112 | - hosts: app 113 | sudo: yes 114 | tasks: 115 | - name: Hello, Ansible! 116 | shell: echo 'Hello, Ansible!' > /tmp/ansible.txt 117 | ``` 118 | 119 | これを, `sample-playbook.yml`というファイル名で保存しておきます. 120 | `hosts`ファイルとPlaybookファイルの設置が終わっていれば, カレントディレクトリは次のようになっているでしょう. 121 | 122 | ``` 123 | $ tree 124 | . 125 | ├── Vagrantfile 126 | ├── hosts 127 | └── sample-playbook.yml 128 | 129 | 0 directories, 3 files 130 | ``` 131 | 132 | ## Playbookの実行 133 | 134 | 設定した`hosts`ファイルを使って, Playbook(`sample-playbook.yml`)を仮想マシンに適用してみましょう. 135 | 136 | ``` 137 | $ ansible-playbook -i hosts sample-playbook.yml 138 | ``` 139 | 140 | Playbookの適用は, `ansible-playbook`コマンドを利用します. 141 | `-i`で`hosts`ファイルを指定し, その後にPlaybookのファイル名を指定します. 142 | 143 | ``` 144 | $ ansible-playbook -i hosts sample-playbook.yml 145 | 146 | PLAY [app] ******************************************************************** 147 | 148 | GATHERING FACTS *************************************************************** 149 | ok: [vagrant] 150 | 151 | TASK: [Hello, Ansible!] ******************************************************* 152 | changed: [vagrant] 153 | 154 | PLAY RECAP ******************************************************************** 155 | vagrant : ok=2 changed=1 unreachable=0 failed=0 156 | ``` 157 | 158 | 最後に, 実行結果が出力されています. 159 | 160 | ``` 161 | PLAY RECAP ******************************************************************** 162 | vagrant : ok=2 changed=1 unreachable=0 failed=0 163 | ``` 164 | 165 | タスクを処理するごとに, Playbookの処理が成功したら`ok`, 変更を伴う処理が成功すれば`changed`, ホストに接続できなければ`unreachable`, そして失敗した場合は`failed`が, それぞれ1ずつ増えていきます. 166 | そのため, `unreachable`と`failed`が0であれば, Playbookの適用は成功した, ということになります. 167 | 168 | それでは, 本当に仮想マシン上にファイルが生成されたのか, 仮想マシンに接続して確認してみましょう. 169 | 170 | ``` 171 | $ ssh vagrant 172 | Welcome to Ubuntu 14.04.2 LTS (GNU/Linux 3.13.0-48-generic x86_64) 173 | ... 中略 ... 174 | vagrant@vagrant-ubuntu-trusty-64:~$ cat /tmp/ansible.txt 175 | Hello, Ansible! 176 | ``` 177 | 178 | ...確かに, 生成されていますね! 179 | 180 | ここまで, 簡単なPlaybookを通して, Ansibleを使った環境構築の自動化を体験してきました. 181 | 今回は, ただファイルを設置する簡単なPlaybookだったので, そこまで大きいメリットを感じなかったかもしれません. 182 | ですが, 実際にアプリケーションを動かすための環境を構築するためには, これまで体験してきたように様々なツールやミドルウェアをホストに導入し, その設定ファイルをホスト内に適切に設置していかなければなりません. 183 | 184 | Ansibleのようなプロビジョニングツールが真価を発揮するのは, まさにこのような場面です. 185 | そしてこのような場面は, 私達がエンジニアとして仕事をしている中では, もはや日常茶飯事と言うことが出来るでしょう. 186 | そういった意味で, Ansibleのようなプロビジョニングツールやデプロイツールの知識は, 恐らくこれからのエンジニアにとって必須の知識になってくると思います. 187 | なので, まずはこのカリキュラムで, しっかりAnsibleに慣れてしまいましょう. 188 | 189 | ...それでは次に, Ansibleの「キモ」となる, Playbookの書き方について, さらに細かく見ていきます. 190 | 191 | ## Playbookの書き方 192 | 193 | 先程述べたように, AnsibleのPlaybookは次のようなYAML形式で記述します. 194 | 195 | ```yaml 196 | - hosts: app 197 | sudo: yes 198 | tasks: 199 | - name: Hello, Ansible! 200 | shell: echo 'Hello, Ansible!' > /tmp/ansible.txt 201 | ``` 202 | 203 | Playbookで設定出来る項目のうち, いくつかを紹介します. 204 | 205 | ### hosts 206 | 207 | `ansible-playbook`コマンドに対し`-i`オプションで指定したホストファイルのうち, このPlaybookを適用する(実行する)ホストやグループを指定します. 208 | カンマ区切りやYAMLのリスト指定で, 同時に複数のホストやグループを指定できます. 209 | 210 | ### sudo 211 | 212 | `sudo`が`yes`の場合, このPlaybookはホスト側では`sudo`を使って実行されます. 213 | デフォルトでは`root`として実行しますが, `sudo_user`を指定すれば任意のユーザとして実行することもできます. 214 | 215 | ### gather_facts 216 | 217 | `yes`の場合, あるいは無指定の場合, Playbookを実行するを実行する前に対象となるホストの情報を取得します(取得できる情報については, 「Ansible Tutorial」の[対象ホストの情報を取得(GATHERING FACTS)](http://yteraoka.github.io/ansible-tutorial/#gather-facts)などを参考にしてください). 218 | 取得した情報は, Playbook中で参照することも出来るので, 例えば「OSや, そのバージョンに応じて, 実行する処理を変更する」などといった事も可能です. 219 | 220 | ホスト情報の取得には少し時間がかかりますので, ホスト情報を利用しない場合は`gather_facts`を`no`にすることで, Playbookを実行する時間を少し減らすことが出来ます. 221 | 222 | ### tasks 223 | 224 | ホストで実行したい処理を記述することができます. 225 | 226 | ```yaml 227 | tasks: 228 | - name: Hello, Ansible! 229 | shell: echo 'Hello, Ansible!' > /tmp/ansible1.txt 230 | ``` 231 | 232 | タスクは, Ansibleが提供する「モジュール」を使って記述します(英語ですが, モジュールの一覧は[こちら](http://docs.ansible.com/list_of_all_modules.html)にあります). 233 | ここでは, ホスト側で任意のコマンドを実行する[shell](http://docs.ansible.com/shell_module.html)というモジュールを実行しています. 234 | 235 | タスクを指定する際は, 別途`name`でその解説を記入することが出来ます. 236 | `name`を指定した場合, `ansible-playbook`でPlaybookを実行した際に, 次のように表示されます. 237 | 238 | ``` 239 | TASK: [Hello, Ansible!] ******************************************************* 240 | ``` 241 | 242 | 実行結果の可読性が向上するので, 是非指定するようにしておきましょう. 243 | 244 | なお, Playbookで利用できるモジュールのうち, 代表的なものについては, この後でいくつか紹介します. 245 | 246 | ### vars_files 247 | 248 | Playbook中で利用できる変数を指定できます. 249 | 例えば, `vars.yml`というファイルに, 250 | 251 | ``` 252 | word: 'Hello, Ansible!' 253 | ``` 254 | 255 | のように指定しておけば, Playbookの中から`Hello, Ansible!`という文字列を, `{{ word }}`として参照することができます. 256 | 257 | ```yaml 258 | - hosts: app 259 | sudo: yes 260 | vars_files: 261 | - vars.yml 262 | tasks: 263 | - name: Hello, Ansible! 264 | shell: echo '{{ word }}' > /tmp/ansible.txt 265 | ``` 266 | 267 | 例えばこのようなPlaybookを実行した場合, ホスト側の`/tmp/ansible.txt`には, 「{{ word }}」ではなく「word」を`vars.yml`で展開した「Hello, Ansible!」が書き込まれます. 268 | 変数は, ここで紹介している`vars_files`で指定したものの他に, デフォルトで利用できる[マジック変数](http://qiita.com/h2suzuki/items/15609e0de4a2402803e9)というものもあります. 269 | 270 | ### コラム: vars_filesの暗号化 271 | 272 | Playbookで`vars_files`で指定する, 変数が書かれたファイルは暗号化することもできます. 273 | 例えば, `vars.yml`というファイルを暗号化したいのであれば, `ansible-vault encrypt vars.yml`というコマンドを実行します. 274 | 275 | ``` 276 | $ ansible-vault encrypt vars.yml 277 | Vault password: [復号用パスワードを入力] 278 | Confirm Vault password: [もう一度同じ復号用パスワードを入力] 279 | Encryption successful 280 | ``` 281 | 282 | すると, 次のように`vars.yml`の中身が暗号化されます. 283 | 284 | ``` 285 | cat vars.yml 286 | $ANSIBLE_VAULT;1.1;AES256 287 | 39333631313465373036653437346363633330396232313564303239656564653038346466353165 288 | 3161633864666163303131346438643734653861393265330a383635353131346538353366346435 289 | 66666536663034306336656433633833323336313466383061376264333463373562633333303337 290 | 3437643166663233340a653333336637316433613036646537623539646635646464393137633933 291 | 30396166376635386136336661383136333037373933643364383435326235366232 292 | ``` 293 | 294 | このファイルを利用してPlaybookを実行する場合, `ansible-playbook`コマンドに対して`--ask-vault-pass`オプションを与え, 復号用パスワードを入力しなければなりません. 295 | 296 | ``` 297 | $ ansible-playbook --ask-vault-pass -i hosts sample-playbook.yml 298 | Vault password: [復号用パスワードを入力] 299 | 300 | PLAY [app] ******************************************************************** 301 | ... 後略 ... 302 | ``` 303 | 304 | もし, 復号用パスワードが異なっている場合, 次のようなエラーが出てPlaybookの実行は終了します. 305 | 306 | ``` 307 | $ ansible-playbook --ask-vault-pass -i hosts sample-playbook.yml 308 | Vault password: [復号用パスワードを入力] 309 | ERROR: Decryption failed 310 | ``` 311 | 312 | 暗号化したファイルは, `ansible-vault decrypt`で復号することが出来ます. 313 | 314 | ``` 315 | $ ansible-vault decrypt vars.yml 316 | Vault password: [復号用パスワードを入力] 317 | Decryption successful 318 | ``` 319 | 320 | しかし, ファイルの中身を変更する度に`decrypt`と`encrypt`を繰り返すのは手間ですし, `encrypt`を忘れて平文のままcommit/pushしてしまうと, このファイルを暗号化する意味がなくなってしまいます. 321 | 322 | そこで, 一度暗号化した後は, `ansible-vault edit`コマンドを使う事をおすすめします. 323 | `ansible-vault edit vars.yml`のように実行すると, 復号用パスワードを入力でき, パスワードが正しい場合はエディタ(環境変数`$EDITOR`で指定しているエディタ)が自動的に開いて対象ファイル(今回の場合, `vars.yml`)を復号した状態で編集できるようになります. 324 | 325 | エディタを閉じると, 自動的に同じパスワードで暗号化してくれるので, 編集後の暗号化を忘れる心配がなくなります. 326 | 327 | ## 代表的なモジュール 328 | 329 | ここでは, Ansibleのモジュールのうち, Playbookを用意する際に頻繁に利用するモジュールを紹介します. 330 | 前述していますが, Ansibleで利用できるモジュールの一覧は[こちら](http://docs.ansible.com/list_of_all_modules.html)にあります. 331 | ここで紹介しているモジュールの他にもたくさんのモジュールが用意されているので, 一度目を通してみることをおすすめします. 332 | 333 | ### [yum](http://docs.ansible.com/yum_module.html) / [apt](http://docs.ansible.com/apt_module.html) 334 | 335 | ```yaml 336 | - apt: name=foo 337 | - yum: name=foo 338 | ``` 339 | 340 | `yum`コマンドや`apt-get`コマンドを使って, `name`で指定したパッケージをインストールします. 341 | 342 | ### [shell](http://docs.ansible.com/shell_module.html) 343 | 344 | ```yaml 345 | - shell: echo 'sample' >> log.txt 346 | ``` 347 | 348 | 指定したコマンドを, ホスト側で実行します. 349 | 350 | ### [file](http://docs.ansible.com/file_module.html) 351 | 352 | ```yaml 353 | - file: path=/tmp/sample state=directory 354 | - file: path=/tmp/sample/log state=touch 355 | ``` 356 | 357 | ファイルやディレクトリの操作をします. `state`が「directory」の場合, `path`で指定したディレクトリを作成し, `state`が「touch」の場合は空のファイルを作成することができます. 358 | 359 | ### [service](http://docs.ansible.com/service_module.html) 360 | 361 | ```yaml 362 | - service: name=nginx state=started 363 | - service: name=nginx state=started enabled=yes 364 | ``` 365 | 366 | `name`で指定したパッケージを操作することができます. 367 | `state`で, パッケージの動作状況を変更することができ, `started`で起動, `stopped`で停止, `restarted`で再起動することができます. 368 | 369 | また, `enabled`でブート時に自動起動するかを設定することができます(`enabled=yes`でブート時に自動的に起動するようになる). 370 | 371 | ### [git](http://docs.ansible.com/git_module.html) 372 | 373 | ```yaml 374 | - git: repo=https://github.com/gotandaGM/text-dev-env.git dest=/tmp/dev-env 375 | ``` 376 | 377 | `repo`で指定したリポジトリを, ホストの`dest`で指定したディレクトリにチェックアウトします. 378 | 379 | ### [copy](http://docs.ansible.com/copy_module.html) 380 | 381 | ```yaml 382 | - copy: src=file.txt dest=/tmp/file.txt 383 | ``` 384 | 385 | ローカルマシンにある, `src`で指定したファイルを, ホストの`dest`で指定したパスにコピーすることができます. 386 | ライブラリやアプリケーションの設定ファイルの設置などに利用することができます. 387 | 388 | ### [template](http://docs.ansible.com/template_module.html) 389 | 390 | ```yaml 391 | - template: src=file.txt dest=/tmp/file.txt 392 | ``` 393 | 394 | ローカルマシンにある, `src`で指定したテンプレートを元にファイルを生成し, ホストの`dest`で指定したパスにコピーすることができます. 395 | `copy`モジュールとの違いは, `src`で指定したファイルをテンプレートとして, 動的にファイルを生成できる点です. 396 | 397 | 例えば, `vars_files`で`type`という変数で`Ansible`を指定している場合, `file.txt`の中身が 398 | 399 | ``` 400 | Hello, {{ type }}! 401 | ``` 402 | 403 | このようになっているのであれば, `dest`で指定した`/tmp/file.txt`の中身は, 次のようになります. 404 | 405 | ``` 406 | Hello, Ansible! 407 | ``` 408 | 409 | templateモジュールはPythonのJinja2というテンプレートエンジン(日本語マニュアルは[こちら](http://ymotongpoo.appspot.com/jinja2_ja/index.html))を利用しているので, Ninja2の構文に従って条件分岐や繰り返しなども実現することができます. 410 | 411 | ## コラム: Playbookのシンタックスチェック 412 | 413 | AnsibleのPlaybookのシンタックスチェックは, `ansible-playbook`に対して`--syntax-check`オプションを指定すればOKです. 414 | シンタックスエラーがなければ, 次のような出力になります. 415 | 416 | ``` 417 | $ ansible-playbook -i hosts sample-playbook.yml --syntax-check 418 | 419 | playbook: sample-playbook.yml 420 | ``` 421 | 422 | 一方, シンタックスエラーが発生した場合はその詳細が出力されます. 423 | 例えば, Playbookの内容が次のようになっている場合... 424 | 425 | ```yaml 426 | - hosts: app 427 | sudo: yes 428 | tasks: 429 | - name: Hello, Ansible! 430 | shell: echo 'Hello, Ansible!' > /tmp/ansible.txt 431 | ``` 432 | 433 | シンタックスチェックの結果は, 次の通りになります. 434 | 435 | ``` 436 | $ ansible-playbook -i hosts sample-playbook.yml --syntax-check 437 | ERROR: Syntax Error while loading YAML script, sample-playbook.yml 438 | Note: The error may actually appear before this position: line 5, column 5 439 | 440 | - name: Hello, Ansible! 441 | shell: echo 'Hello, Ansible!' > /tmp/ansible.txt 442 | ^ 443 | ``` 444 | 445 | `name`と`shell`の行頭を揃えていないのでシンタックスエラーが生じていることがわかります. 446 | 447 | ## 練習問題 448 | 449 | Vagrantの「練習問題」において手動で行った環境構築を, Ansibleを利用して自動化してみましょう(Amon2::Liteアプリケーションの準備や, Vagrantfileの用意と起動などは手動で行ってください). 450 | `build.yml`というファイルを用意し, ここにPlaybookを書いていきましょう. 451 | 452 | ```yaml 453 | - hosts: app 454 | sudo: yes 455 | gather_facts: no 456 | tasks: 457 | - name: Install git 458 | apt: name=git 459 | - name: Clone xbuild 460 | git: repo=https://github.com/tagomoris/xbuild.git dest=/tmp/xbuild 461 | - name: Install Perl (and App::cpanminus / Carton) 462 | shell: /tmp/xbuild/perl-install 5.20.1 /opt/perl-5.20 463 | ``` 464 | 465 | ヒントとして, xbuildを利用してPerl 5.20.1をインストールするまでのPlaybookを用意しておきました. 466 | このPlaybookをベースに, `carton install`の実行やNginx, Supervisorのインストールと初期設定などをAnsibleで自動的に実行できるようにしてみましょう. 467 | 468 | ## 複雑なPlaybook 469 | 470 | 先程の練習問題では, 環境構築に必要なタスクを全て`build.yml`に記述しました. 471 | しかし, 1つのファイルに全てのタスクを書いてしまうと, 1ファイルにおける記述量が大量になってしまいます. 472 | そのため, Ansibleではタスクを「ロール」という単位で分割することが出来るようになっています. 473 | 474 | ここでは, 先程の練習問題で書いたPlaybookのタスクのうち, Perlのインストールに関連する部分を, `perl`というロールに切り分けてみます. 475 | まずは, `build.yml`側の変更です. 476 | 477 | ```yaml 478 | - hosts: app 479 | sudo: yes 480 | gather_facts: no 481 | roles: 482 | - perl 483 | ``` 484 | 485 | `tasks`の変わりに`roles`という設定が入り, これから作る`perl`というロールを指定しています. 486 | 487 | 一方, 具体的なタスクについては, `/roles/[role名]/tasks/main.yml`に記載することになります. 488 | 今回の場合, ロールの名前は`perl`なので, `roles/perl/tasks/main.yml`に転記しましょう. 489 | 490 | ``` 491 | vi roles/perl/tasks/main.yml 492 | - name: Install git 493 | apt: name=git 494 | - name: Clone xbuild 495 | git: repo=https://github.com/tagomoris/xbuild.git dest=/tmp/xbuild 496 | - name: Install Perl (and App::cpanminus and Carton) 497 | shell: /tmp/xbuild/perl-install 5.20.1 /opt/perl-5.20 498 | ``` 499 | 500 | 今回の場合, ロールへの切り出しはこれで完了です. 501 | `tree`コマンドでディレクトリ構成を見てみると, 次のようになっているはずです. 502 | 503 | ``` 504 | . 505 | ├── Vagrantfile 506 | ├── hosts 507 | ├── roles 508 | │   └── perl 509 | │   └── tasks 510 | │   └── main.yml 511 | └── sample-playbook.yml 512 | ``` 513 | 514 | では, `ansible-playbook`コマンドでPlaybookを(再度)実行してみましょう. 515 | 516 | ``` 517 | $ ansible-playbook -i hosts build.yml 518 | 519 | PLAY [app] ******************************************************************** 520 | 521 | TASK: [perl | Install git] **************************************************** 522 | ok: [vagrant] 523 | 524 | TASK: [perl | Clone xbuild] *************************************************** 525 | ok: [vagrant] 526 | 527 | TASK: [perl | Install Perl (and App::cpanminus and Carton)] ******************* 528 | changed: [vagrant] 529 | 530 | PLAY RECAP ******************************************************************** 531 | vagrant : ok=3 changed=1 unreachable=0 failed=0 532 | ``` 533 | 534 | ロールに切りだす前と同じ処理が, 問題なく実行することができましたね. 535 | 536 | ### ロールのディレクトリ構成 537 | 538 | 先程, ロールではタスクを`/roles/[role名]/tasks/main.yml`に配置する, と説明しました. 539 | Ansibleのロールでは, このようなディレクトリやファイル構成が重要で, `tasks`以外にも`vars`や`meta`, `templates`, `files`などを指定することが出来ます. 540 | これらの設定によって, 複雑な環境構築手順もシンプルに記述することが出来るようになります. 541 | 542 | #### `/roles/[role名]/tasks/main.yml` 543 | 544 | 前述のように, ロールとして実行するタスクを記載するファイルです. 545 | 546 | #### `/roles/[role名]/vars/main.yml` 547 | 548 | `/roles/[role名]/tasks/main.yml`で利用する変数を定義出来るファイルです. 549 | 変数の定義方法については, `vars_files`で指定する場合と同様です. 550 | 551 | #### `/roles/[role名]/meta/main.yml` 552 | 553 | ロールの依存関係を定義出来ます. 554 | 例えば, 以下のような記述をした場合, このロールを実行する前に依存関係のある`foobar`というロールを実行してから, 現在のロールを実行します. 555 | 556 | Nginxの設定ファイルを配置するロールにおいて, Nginxをインストールするロールを依存として指定しておく, といった場合に利用できます. 557 | 558 | ```yaml 559 | dependencies: 560 | - { role: foobar } 561 | ``` 562 | 563 | #### `/roles/[role名]/files/main.yml` 564 | 565 | `/roles/[role名]/tasks/main.yml`内で`copy`モジュールを利用する際に使えるファイルを配置するディレクトリです. 566 | 例えば, このディレクトリ内に`foobar.txt`というファイルがあったとすると, そのファイルは`copy`モジュールで`src=foobar.txt`と指定することで利用することができます. 567 | 568 | #### `/roles/[role名]/templates/main.yml` 569 | 570 | `/roles/[role名]/tasks/main.yml`内で`template`モジュールを利用する際に使えるテンプレートを配置するディレクトリです. 571 | 例えば, このディレクトリ内に`template.txt`というファイルがあったとすると, そのテンプレートは`template`モジュールで`src=template.txt`と指定することで利用することができます. 572 | 573 | ## コラム: 階層的なロール名 574 | 575 | `/roles/[role名]/tasks/main.yml`について, `[ロール名]`の部分は階層的にすることが出来ます. 576 | 例えば, `carton install`をするロールを, `/roles/perl/carton/tasks/main.yml`などの形で設置することも出来ます. 577 | 578 | 但し, これまで見てきたように, ロールを構成するディレクトリ名として`vars`や`meta`, `templates`, `files`などは既に利用されているので, これらの名前は避けなければなりません. 579 | 580 | ## 練習問題 581 | 582 | 前の練習問題で作ったPlaybook中のタスクを, ロールを使って整理してみよう. 583 | 時間があれば, 仮想マシンを初期化した上で, もう一度最初からPlaybookを適用してみましょう. 584 | 585 | ## コラム: Vagrantとの連携 586 | 587 | Vagrantと連携し, 仮想マシンを起動する際に自動的にPlaybookを適用することも可能です. 588 | `Vagrantfile`と同じ階層に, Playbookを記載した`playbook.yml`というファイルがあるのであれば, `Vagrantfile`に次のように記載することで, 仮想マシン起動後にPlaybookを適用することができます. 589 | 590 | ```rb 591 | Vagrant.configure("2") do |config| 592 | config.vm.provision "ansible" do |ansible| 593 | ansible.playbook = "playbook.yml" 594 | end 595 | end 596 | ``` 597 | -------------------------------------------------------------------------------- /infrastructure-as-code/img/vagrant-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/infrastructure-as-code/img/vagrant-1.png -------------------------------------------------------------------------------- /infrastructure-as-code/img/vagrant-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/infrastructure-as-code/img/vagrant-2.png -------------------------------------------------------------------------------- /infrastructure-as-code/img/vagrant-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/infrastructure-as-code/img/vagrant-3.png -------------------------------------------------------------------------------- /infrastructure-as-code/img/vagrant-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/infrastructure-as-code/img/vagrant-4.png -------------------------------------------------------------------------------- /infrastructure-as-code/img/vagrant-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/infrastructure-as-code/img/vagrant-5.png -------------------------------------------------------------------------------- /infrastructure-as-code/img/vagrant-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/infrastructure-as-code/img/vagrant-6.png -------------------------------------------------------------------------------- /infrastructure-as-code/img/vagrant-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/infrastructure-as-code/img/vagrant-7.png -------------------------------------------------------------------------------- /infrastructure-as-code/img/vagrant-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/infrastructure-as-code/img/vagrant-8.png -------------------------------------------------------------------------------- /infrastructure-as-code/serverspec.md: -------------------------------------------------------------------------------- 1 | # Serverspec 2 | 3 | 先程, プロビジョニングツールの1つ, Ansibleを使って, Vagrantで構築した仮想マシン上に簡単なWebアプリケーションが動作する環境を構築しました. 4 | 5 | Ansibleを利用することで, 複数台のサーバに対するプロビジョニングを, 並列化しながら自動的に実行することが出来るようになりました. 6 | 次の課題は, プロビジョニングが期待通りに完了しているかどうかの確認です. 7 | もし, プロビジョニング対象のサーバが1台であれば手動で確認することも可能ですが, 複数台のサーバが対象であれば, プロビジョニングと同じくプロビジョニング後の確認についても, 手動で行うのは困難になります. 8 | 9 | Ansibleの場合, `unreachable`や`failed`が0であれば, Playbookの適用は成功しているはずです. 10 | ですが, Playbook適用後の環境が本当に正しい環境になっているかどうかまでは, これだけで保証することは出来ません. 11 | これを確認する為の手段の1つが, 「サーバのテストツール」, Serverspecです. 12 | 13 | Serverspecは, 宮下剛輔さん([@gousukenator](https://twitter.com/gosukenator), 通称mizzyさん)が開発された, Ruby製のツールです. 14 | Rubyのテストフレームワーク, RSpecを活用して実装されており, 「リソース」とそれに対応する「マッチャー」を組み合わせることで, サーバの状態を簡単にテストすることが出来ます. 15 | 16 | ここでは, Serverpsecの導入及び使い方を紹介した後, Playbookに対応するテストを, Serverspecを使って構築していきます. 17 | 18 | ## Serverspecのインストール 19 | 20 | Rubyがインストール済みで, `gem`コマンドが利用可能であれば, 以下のコマンドでインストールできます. 21 | 22 | ``` 23 | $ gem install serverspec 24 | ``` 25 | 26 | もし, Rubyのインストールが出来ていないのであれば, 次の項目を参考に, `rbenv`を利用してRubyの環境を構築しましょう. 27 | 28 | ### Rubyのインストール 29 | 30 | [rbenv](https://github.com/sstephenson/rbenv)は, 複数の異なるバージョンのRubyをインストールし, 状況に応じて切り替えながら利用出来るようにするツールです. 31 | Perlの[plenv](https://github.com/tokuhirom/plenv)の元になったツールでもあります. 32 | 33 | ``` 34 | $ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv 35 | $ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build 36 | ``` 37 | 38 | まず, rbenv本体をGitHubから`clone`します. 39 | 同時に, Rubyをインストールする為の`ruby-build`というツールも`clone`しておきます. 40 | 41 | ``` 42 | $ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile 43 | $ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile 44 | ``` 45 | 46 | 続いて, `~/.bash_profile`にrbenvに必要な設定を書き込みます. 47 | `~/.bash_profile`の部分は, 環境に応じて適宜変更して下さい(例えば, zshユーザであれば`.zshrc`に変更する, 等). 48 | 49 | ``` 50 | $ exec $SHELL -l 51 | $ rbenv --version 52 | rbenv 0.4.0-153-g3b6faa8 53 | ``` 54 | 55 | これで, `rbenv`コマンドが利用出来るようになっています. 56 | それでは, `rbenv intall`コマンドで, 現時点での最新版であるRuby 2.2.2をインストールします. 57 | 58 | ``` 59 | $ rbenv install 2.2.2 60 | Downloading ruby-2.2.2.tar.gz... 61 | -> https://dqw8nmjcqpjn7.cloudfront.net/5ffc0f317e429e6b29d4a98ac521c3ce65481bfd22a8cf845fa02a7b113d9b44 62 | Installing ruby-2.2.2... 63 | Installed ruby-2.2.2 to /Users/username/.rbenv/versions/2.2.2 64 | ``` 65 | 66 | 問題なくインストールが完了していれば, `which ruby`と`ruby -v`の出力が, 次のようになっているはずです. 67 | 68 | ``` 69 | $ which ruby 70 | /Users/username/.rbenv/shims/ruby 71 | $ ruby -v 72 | ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14] 73 | ``` 74 | 75 | Rubyのインストールと同時に, `gem`コマンドも利用出来るようになっているので, 後は冒頭の記述の通り, `gem install serverspec`でServerspecをインストールしましょう. 76 | 77 | ### コラム: anyenv 78 | 79 | ここまで紹介してきた`rbenv`や, Perl用の`plenv`は, ある言語について複数のバージョン(の環境)をインストールし, 必要に応じて切り替えられるツールと説明してきました. 80 | なぜこのようなツールが必要になるのでしょうか? 81 | それは, 複数のプロダクトを開発している場合, そのプロダクトで適した(= 本番環境で利用している)言語のバージョンを利用しなければならないからです(もし, 異なるバージョンを使っていた場合, バージョン間の誤差によって, 正しいコードが動かなかったり, 正しくないコードが動いてしまったり, といった事が起こりえます). 82 | 83 | そのため, Perl用の`plenv`やRuby用の`rbenv`の他に, Node用の`ndenv`, Python用の`pyenv`など, 様々な言語のバージョンを管理する`**env`が開発されています. 84 | これらの`**env`をそれぞれインストールするのは手間なので, これらを統一的に扱う仕組みが開発されています. 85 | それが, [`anyenv`](https://github.com/riywo/anyenv)です. 86 | 87 | 既に`plenv`や`rbenv`を導入済みであれば, 別途インストールしなければならない為二度手間となってしまいますが, 1つのツールで複数の言語の環境を管理出来る, というのはとても便利です. 88 | 機会があれば, 既に導入済みの`plenv`や`rbenv`を, `anyenv`で管理するように手を加えてみることをおすすめします. 89 | 90 | ## Serverspecの初期設定 91 | 92 | Vagrantで立ち上げた仮想マシンは, 先程Ansibleで環境構築を行った仮想マシンをそのまま利用します. 93 | `ssh vagrant`で仮想マシンに接続出来るか, 予め確認しておきましょう. 94 | 95 | Serverspecの初期設定は, `serverspec-init`コマンドで行います. 96 | 97 | ``` 98 | $ serverspec-init 99 | Select OS type: 100 | 101 | 1) UN*X 102 | 2) Windows 103 | 104 | Select number: 1 105 | 106 | Select a backend type: 107 | 108 | 1) SSH 109 | 2) Exec (local) 110 | 111 | Select number: 1 112 | 113 | Vagrant instance y/n: n 114 | Input target host name: vagrant 115 | + spec/ 116 | + spec/vagrant/ 117 | + spec/vagrant/sample_spec.rb 118 | + spec/spec_helper.rb 119 | + Rakefile 120 | ``` 121 | 122 | `serverspec-init`コマンドでは, 以下のとおり設定します. 123 | 124 | - OSの種類(`Select OS type: `) 125 | - Windows以外を利用しているのであれば`1`を入力し, リターンキーを押します. 126 | - バックエンドの種類(`Select a backend type: `) 127 | - テスト対象となるホストにどのように接続するか選択します. 128 | - SSHで接続するのであれば`1`, ローカル環境に対してテストを実行するのであれば`2`を選択し, リターンキーを押します. 129 | - ここでは, Vagrantで立ち上げた仮想マシンを対象とするので, `1`を選択します. 130 | - Vagrantのインスタンスか否か(`Vagrant instance y/n: `) 131 | - 本来であれば`y`ですが, ここは`n`を入力し, リターンキーを押します. 132 | - テスト対象のホスト名(`Input target host name: `) 133 | - Vagrantで建てた仮想マシンには, `vagrant`で接続できるよう設定しているので, `vagrant`と入力し, リターンキーを押します. 134 | 135 | これで, Serverspecの初期設定を実施することが出来ます. 136 | この際, 入力した設定に応じて, Serverspecのためのファイルが生成されています. 137 | 138 | ## Serverspecのファイル構成 139 | 140 | `serverspec-init`は, 以下のファイルを生成します. 141 | これらは, Serverspecによるテストを実現する為の大切なファイルです. 142 | 143 | ### `Rakefile` 144 | 145 | Serverspecを利用したテストのタスクが記述されているファイルです. 146 | このファイルによって, 後述するように`rake spec`でテストを実行出来るようになります. 147 | 148 | ### `spec/spec_helper.rb` 149 | 150 | 各テストのヘルパークラスです. 151 | SSHを利用して, テスト対象となるホストへ接続する為のコードが記載されています. 152 | 153 | ### `spec/vagrant/sample_spec.rb` 154 | 155 | 実際にテストを記述する部分です. 156 | デフォルトで, 次のようなテストが記述されています. 157 | 158 | ```rb 159 | require 'spec_helper' 160 | 161 | describe package('httpd'), :if => os[:family] == 'redhat' do 162 | it { should be_installed } 163 | end 164 | 165 | describe package('apache2'), :if => os[:family] == 'ubuntu' do 166 | it { should be_installed } 167 | end 168 | 169 | describe service('httpd'), :if => os[:family] == 'redhat' do 170 | it { should be_enabled } 171 | it { should be_running } 172 | end 173 | 174 | describe service('apache2'), :if => os[:family] == 'ubuntu' do 175 | it { should be_enabled } 176 | it { should be_running } 177 | end 178 | 179 | describe service('org.apache.httpd'), :if => os[:family] == 'darwin' do 180 | it { should be_enabled } 181 | it { should be_running } 182 | end 183 | 184 | describe port(80) do 185 | it { should be_listening } 186 | end 187 | ``` 188 | 189 | ## テストの実装 190 | 191 | Serverspecのテストは, Rubyのコードとして実装します. 192 | といっても, Serverspecのテストを書く際にRubyの知識はほとんど求められません(但し, `Rakefile`や`spec_helper.rb`を書き換える場合は, その限りではありません). 193 | 基本的に, Serverspecの「リソース」を利用しながら, 下記のフォーマットに従ってサーバの仕様(Spec)を書いていけばOKです. 194 | 195 | ```rb 196 | describe [リソースタイプ]([テスト対象]) do 197 | [テスト条件] 198 | end 199 | ``` 200 | 201 | 例として, テスト対象のホストに`nginx`のパッケージが導入されているかを確認するテストを書いてみます. 202 | この場合, 各OSのパッケージマネージャについてテストすることができる[`package`](http://serverspec.org/resource_types.html#package)というリソースを利用すると良いでしょう. 203 | 204 | ```rb 205 | describe package('nginx') do 206 | [テスト条件] 207 | end 208 | ``` 209 | 210 | `package`リソースは, 例えば`package('nginx')`のように書くと, 対象ホストの`nginx`パッケージについてテストをすることが出来ます. 211 | 212 | 一方「テスト条件」については, [packageリソースのドキュメント](http://serverspec.org/resource_types.html#package)を見ると, インストール済みかどうかを検証する`be_installed`と呼ばれるマッチャー(matcher)が利用できる, と書かれています. 213 | 214 | ```rb 215 | require 'spec_helper' 216 | 217 | describe package('nginx') do 218 | it { should be_installed } 219 | end 220 | ``` 221 | 222 | このように, `package`というリソースと, それに対応する`be_installed`というマッチャーを組み合わせ, `spec/vagrant/sample_spec.rb`をこのように書いておけば, 「対象ホストに, nginxというパッケージがインストール済み(installed)か?」をテストすることができます. 223 | 224 | ### コラム: ServerespecとSpecinfra 225 | 226 | 先程紹介した`package`というリソースは, テスト対象となるホストのOSを問わず実行することができます. 227 | これはつまり, `package`というリソースであるパッケージについてテストする際, テスト対象のホストのOSがCentOSであれば`yum`を, Ubuntuであれば`apt`を利用し, テストを実行するということです(但し, 例えばCentOSとUbuntuでパッケージ名が異なる場合はその限りではありません). 228 | 229 | これは, Serverspecが利用している[Specinfra](https://github.com/serverspec/specinfra)が, OS間のコマンドの差異を吸収し, 適切なコマンドを発行しているためです. 230 | 元々SpecinfraはServerspecの一部でしたが, 新しいインフラのテストツールやプロビジョニングツールの基盤として利用されることを狙って, Serverspecから切りだされました. 231 | 実際に, Cookpadが「軽いChef」を目指して開発した[Itamae](https://github.com/itamae-kitchen/itamae)というプロビジョニングツールも, その実装にSpecinfraを利用し, OS間の差異を吸収できるようにしています. 232 | 233 | ## テストの実行 234 | 235 | それでは, このテストをVagrantで建てた仮想マシンに対して実行してみましょう. 236 | 既にAnsibleのPlaybookを適用済みであれば, `nginx`パッケージは導入済みのはずなので, テストは全て通るはずです. 237 | 238 | ``` 239 | $ rake spec 240 | /Users/username/.rbenv/versions/2.2.2/bin/ruby -I/Users/username/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/rspec-support-3.4.1/lib:/Users/username/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/rspec-core-3.4.1/lib /Users/username/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/rspec-core-3.4.1/exe/rspec --pattern spec/vagrant/\*_spec.rb 241 | 242 | Package "nginx" 243 | should be installed 244 | 245 | Finished in 0.45598 seconds (files took 0.62706 seconds to load) 246 | 1 example, 0 failures 247 | ``` 248 | 249 | Serverspecのテストは, `Rakefile`が設置されているディレクトリで`rake spec`コマンドを実行することで実施できます. 250 | 実行結果としては最後に`1 example, 0 failures`が出力されていて, 1つのテストを実行し0の失敗, つまり全て成功という結果になりました. 251 | 252 | では次に, 失敗時のパターンを確認しておきます. 253 | `spec/vagrant/sample_spec.rb`を, 次のように書き換えましょう. 254 | 255 | ```rb 256 | require 'spec_helper' 257 | 258 | describe package('nginx') do 259 | it { should be_installed } 260 | end 261 | 262 | describe package('apache2') do 263 | it { should be_installed } 264 | end 265 | ``` 266 | 267 | `apache2`は仮想マシン上で動いているUbuntu Serverにインストールしていないはずなので, 268 | 269 | ``` 270 | $ rake spec 271 | /Users/username/.rbenv/versions/2.1.4/bin/ruby -I/Users/username/.rbenv/versions/2.1.4/lib/ruby/gems/2.1.0/gems/rspec-support-3.2.2/lib:/Users/username/.rbenv/versions/2.1.4/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.1/lib /Users/username/.rbenv/versions/2.1.4/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.1/exe/rspec --pattern spec/vagrant/\*_spec.rb 272 | 273 | Package "nginx" 274 | should be installed 275 | 276 | Package "apache2" 277 | should be installed (FAILED - 1) 278 | 279 | Failures: 280 | 281 | 1) Package "apache2" should be installed 282 | On host `vagrant' 283 | Failure/Error: it { should be_installed } 284 | expected Package "apache2" to be installed 285 | sudo -p 'Password: ' /bin/sh -c dpkg-query\ -f\ \'\$\{Status\}\'\ -W\ apache2\ \|\ grep\ -E\ \'\^\(install\|hold\)\ ok\ installed\$\' 286 | 287 | # ./spec/vagrant/sample_spec.rb:8:in `block (2 levels) in ' 288 | 289 | Finished in 0.58318 seconds (files took 0.58938 seconds to load) 290 | 2 examples, 1 failure 291 | 292 | Failed examples: 293 | 294 | rspec ./spec/vagrant/sample_spec.rb:8 # Package "apache2" should be installed 295 | 296 | /Users/username/.rbenv/versions/2.1.4/bin/ruby -I/Users/username/.rbenv/versions/2.1.4/lib/ruby/gems/2.1.0/gems/rspec-support-3.2.2/lib:/Users/username/.rbenv/versions/2.1.4/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.1/lib /Users/username/.rbenv/versions/2.1.4/lib/ruby/gems/2.1.0/gems/rspec-core-3.2.1/exe/rspec --pattern spec/vagrant/\*_spec.rb failed 297 | ``` 298 | 299 | 失敗した場合, このような出力が得られます. 300 | 301 | ``` 302 | Package "apache2" 303 | should be installed (FAILED - 1) 304 | ``` 305 | 306 | この部分を見れば, `apache2`に関して, `should be installed`が`FAILED`になっていることがわかります. 307 | 308 | ## 代表的なリソース 309 | 310 | ここでは, Serverspecで利用できる代表的なリソースと, そこで利用できるマッチャーについて紹介します. 311 | Serverspecで利用できるリソースの一覧は, Serverspecの公式ウェブサイトの「[Resource Types](http://serverspec.org/resource_types.html)」というページで確認することができます. 312 | 313 | ### [`package`](http://serverspec.org/resource_types.html#package)リソース 314 | 315 | 任意のパッケージについてテストすることができます. 316 | マッチャーとしては, 指定したパッケージがインストール済みかどうかをテストする, `be_installed`が用意されています. 317 | 318 | ```rb 319 | describe package('nginx') do 320 | it { should be_installed } 321 | end 322 | ``` 323 | 324 | ### [`service`](http://serverspec.org/resource_types.html#service)リソース 325 | 326 | 任意のサービスについてテストすることができます. 327 | マッチャーとしては, OS起動時に自動的に起動するよう設定されているかを確認する`be_enabled`, サービスが稼働しているかを確認する`be_running`などを利用することができます. 328 | 329 | ```rb 330 | describe service('nginx') do 331 | it { should be_enabled } 332 | it { should be_running } 333 | end 334 | ``` 335 | 336 | ### [`port`](http://serverspec.org/resource_types.html#port)リソース 337 | 338 | 任意のポートについてテストすることが出来ます. 339 | マッチャーとしては, そのポートがlisten(待ち受け)しているかを確認する`be_listening`を利用できます. 340 | 341 | ```rb 342 | describe port(80) do 343 | it { should be_listening } 344 | end 345 | ``` 346 | 347 | `be_listening`マッチャーについては, `with`を利用することで, どのプロトコルでlistenしているかをテストすることもできます(`tcp`, `udp`, `tcp6`, `udp6`を指定可能). 348 | 349 | ```rb 350 | describe port(80) do 351 | it { should be_listening.with('tcp') } 352 | end 353 | ``` 354 | 355 | この場合, 80番ポートが`tcp`でlistenされているかを確認することができます. 356 | 357 | ### [`process`](http://serverspec.org/resource_types.html#process)リソース 358 | 359 | 任意のプロセスについてテストする事ができます. 360 | マッチャーとしては, そのプロセスが動いているかどうかを確認する`be_running`を利用することができます. 361 | 362 | ```rb 363 | describe process('nginx') do 364 | it { should be_running } 365 | end 366 | ``` 367 | 368 | また, `ps`コマンドで確認できる, `user`や`group`, `args`といったプロセスのパラメータについても, 次のような記述でテストすることができます. 369 | 370 | ```rb 371 | describe process('memcached') do 372 | its(:user) { should eq 'memcached' } 373 | its(:args) { should match /-c 32000\b/ } 374 | end 375 | ``` 376 | 377 | `its(:user) { should eq 'memcached' }`で, このプロセスが`memcached`ユーザによって動作していることを, `its(:args) { should match /-c 32000\b/ }`で, このプロセスが`-c 32000`というオプション付きで起動されていることをテストしています. 378 | 379 | ### [`file`](http://serverspec.org/resource_types.html#file)リソース 380 | 381 | 任意のファイルやディレクトリについてテストすることができます. 382 | マッチャーとしては, まずファイルの種類を確認するマッチャーとして, ファイルであることを確認する`be_file`, ディレクトリであることを確認する`be_directory`, ソケットであることを確認する`be_socket`, symlinkであることを確認する`be_symlink`が用意されています. 383 | 384 | ```rb 385 | describe file('/etc/passwd') do 386 | it { should be_file } 387 | end 388 | ``` 389 | 390 | この場合, `/etc/passwd`というファイルがあることをテストしています. 391 | 392 | また, モードを確認する`be_mode`, オーナーを確認する`be_owned_by`, グループの情報を確認する`be_grouped_info`, リンク先を確認する`be_linked_to`, 読み込み可能かを確認する`be_readable`, 書き込み可能か確認する`be_writable`, 実行可能か確認する`be_executable`, ディレクトリがマウントされているかを確認する`be_mounted`というマッチャーも用意されています. 393 | 394 | また, `its(:content)`を使うことで, ファイルに任意の文字列が含まれているかを確認することができます. 395 | 396 | ```rb 397 | describe file('/etc/httpd/conf/httpd.conf') do 398 | its(:content) { should match /ServerName www.example.jp/ } 399 | end 400 | ``` 401 | 402 | この場合, `/etc/httpd/conf/httpd.conf`というファイルに, `ServerName www.example.jp`という記述があるかどうかを確認しています. 403 | 404 | 更に, `its(:md5sum)`や`its(:sha256sum)`を使えば, ファイルのチェックサムが同じかどうかをテストすることもできます. 405 | 406 | ```rb 407 | describe file('/etc/services') do 408 | its(:sha256sum) { should eq 'a861c49e9a76d64d0a756e1c9125ae3aa6b88df3f814a51cecffd3e89cce6210' } 409 | end 410 | ``` 411 | 412 | ### [`command`](http://serverspec.org/resource_types.html#command)リソース 413 | 414 | 指定したコマンドを実行した結果について, テストすることができます. 415 | 416 | ```rb 417 | describe command('ls -al /') do 418 | its(:stdout) { should match /bin/ } 419 | end 420 | ``` 421 | 422 | この場合, ホスト側で`ls -al /`というコマンドを実行してから, その標準出力に`bin`という文字列が含まれているかどうかを確認することができます. 423 | 424 | また, 標準出力について確認する`its(:stdout)`の他に, 標準エラー出力について確認する`its(:stderr)`, 終了時のステータスについて確認する`its(:exit_status)`も利用することができます. 425 | 426 | `its(:stdout)`及び`its(:stderr)`では, `contain`というマッチャーを利用することができます. 427 | これによって, 標準出力及び標準エラー出力について, より詳細に確認をすることができます. 428 | 429 | ```rb 430 | describe command('apachectl -M') do 431 | its(:stdout) { should contain('proxy_module') } 432 | end 433 | ``` 434 | 435 | このように記述した場合, `apachectl -M`を実行した際の標準出力に, `proxy_module`という文字列が含まれていることを確認できます. 436 | 437 | ```rb 438 | describe command('apachectl -V') do 439 | # test 'Prefork' exists between "Server MPM" and "Server compiled". 440 | its(:stdout) { should contain('Prefork').from(/^Server MPM/).to(/^Server compiled/) } 441 | 442 | # test 'conf/httpd.conf' exists after "SERVER_CONFIG_FILE". 443 | its(:stdout) { should contain('conf/httpd.conf').after('SERVER_CONFIG_FILE') } 444 | 445 | # test 'Apache/2.2.29' exists before "Server built". 446 | its(:stdout) { should contain(' Apache/2.2.29').before('Server built') } 447 | end 448 | ``` 449 | 450 | また, このように記述した場合, `apachectl -V`を実行した際の標準出力について, 451 | 452 | - `^Server MPM`という正規表現に一致する文字列から`^Server compiled`という正規表現に一致する文字列の間に, `Prefork`という文字列が含まれていること 453 | - `SERVER_CONFIG_FILE`という文字列の後に, `conf/httpd.conf`という文字列が含まれていること 454 | - `Server build`という文字列の前に, ` Apache/2.2.29`という文字列が含まれていること 455 | 456 | について, 確認することができます. 457 | 458 | ## 練習問題 459 | 460 | Serverspecの[Resource Types](http://serverspec.org/resource_types.html)から適切なリソースを探しながら, 次の要素を満たすように`sample_spec.rb`を書き換えていきましょう. 461 | 462 | ### `SampleApp`のテスト 463 | 464 | Ansible Playbookで自動的に構築出来るようにした`SampleApp`について, Serverspecを利用したテストを書いてみよう. 465 | 466 | ### MySQL 467 | 468 | (実際に`SampleApp`では利用していませんが)Ansible Playbookを変更して, 仮想マシン内にMySQLをインストールしましょう. 469 | また, ServerspecでMySQLについてテストするようにしましょう. 470 | -------------------------------------------------------------------------------- /oop.md: -------------------------------------------------------------------------------- 1 | # はじめに 2 | 3 | この資料では, Perlにおけるオブジェクト指向について説明します. 4 | 「Perlでオブジェクト指向をどのように実現するか」を中心に扱い, オブジェクト指向の基本的な知識(理論)については**扱いません.** 5 | 6 | # Perlのオブジェクト指向 7 | 8 | Perlは良くも悪くも, 後方互換性を非常に重視した言語です. 9 | そのため, 当初オブジェクト指向に関する機能を持っていなかったPerlがオブジェクト指向に対応する際も, 後方互換性を維持しながらオブジェクト指向に対応しました. 10 | 以下解説していきますが, そのような理由により, Perlのオブジェクト指向はJavaやRubyなど, 他のオブジェクト指向な言語に比べて非常に「歪」です. 11 | 他言語使いからすれば非常に理解に苦しむ部分も多いとは思いますが, 「新しい言語に慣れるトレーニング」と思って頑張っていきましょう. 12 | 13 | これ以降は, 簡単なPerlのモジュールを作りながら, Perlのオブジェクト指向プログラミングや, それを支える各種モジュールの使い方を説明していきます. 14 | 15 | # Minilla 16 | 17 | Perlのモジュールを作るにあたって, まずそのひな形を用意します. 18 | かつてはModule::Starterなど様々なツールが提案されていましたが, ここ数年は[@tokuhirom](https://twitter.com/tokuhirom)さんが開発した[Minilla](https://metacpan.org/pod/Minilla)がデファクトスタンダードになっています. 19 | Minillaは, モジュールのひな形を作るだけでなく, モジュール開発の様々な支援(例えばテストの実施, モジュールのインストール, モジュールのファイルをまとめたtarballの作成, そしてモジュールのCPANへのアップロードなど)を行ってくれるので, モジュールを開発するのであれば積極的に利用すると良いでしょう. 20 | 21 | plenvを利用したPerlの環境は構築済みだと思いますので, `cpanm`コマンドでMinillaをインストールします. 22 | 23 | ``` 24 | $ cpanm Minilla 25 | --> Working on Minilla 26 | Fetching http://www.cpan.org/authors/id/T/TO/TOKUHIROM/Minilla-v2.4.1.tar.gz ... OK 27 | Configuring Minilla-v2.4.1 ... OK 28 | ... 中略 ... 29 | Successfully installed Minilla-v2.4.1 (upgraded from v2.1.1) 30 | 4 distributions installed 31 | ``` 32 | 33 | Minillaのインストールが完了しました(元々インストール済みだったので, 表示上は`upgraded`になっていますが). 34 | これで, Minillaが提供する`minil`コマンドが利用できるようになっているはずです. 35 | 36 | ``` 37 | $ which minil 38 | /Users/username/.anyenv/envs/plenv/shims/minil 39 | ``` 40 | 41 | それでは早速, `minil`コマンドからモジュールのひな形を作りましょう. 42 | 今回はチュートリアル用なので, `PerlEntrance::OOPTutorial`というモジュールを作ることとします. 43 | 44 | ``` 45 | $ minil new PerlEntrance::OOPTutorial 46 | Writing lib/PerlEntrance/OOPTutorial.pm 47 | Writing Changes 48 | Writing t/00_compile.t 49 | Writing .travis.yml 50 | Writing .gitignore 51 | Writing LICENSE 52 | Writing cpanfile 53 | Initializing git PerlEntrance::OOPTutorial 54 | [PerlEntrance-OOPTutorial] $ git init 55 | Initialized empty Git repository in /Users/username/current_directory/PerlEntrance-OOPTutorial/.git/ 56 | Retrieving meta data from lib/PerlEntrance/OOPTutorial.pm. 57 | Name: PerlEntrance::OOPTutorial 58 | Abstract: It's new $module 59 | Version: 0.01 60 | fatal: bad default revision 'HEAD' 61 | [PerlEntrance-OOPTutorial] $ git add . 62 | Finished to create PerlEntrance::OOPTutorial 63 | ``` 64 | 65 | カレントディレクトリに, `PerlEntrance-OOPTutorial`というディレクトリが出来ているはずです. 66 | 67 | ``` 68 | $ ls 69 | PerlEntrance-OOPTutorial 70 | ``` 71 | 72 | これで, ひな形の生成は完了しました. 73 | 74 | # ディレクトリ構成 75 | 76 | 次に, ひな形として生成されたファイルを見ながら, Perlのモジュールのディレクトリ構成について解説します. 77 | 生成された`PerlEntrance-OOPTutorial`の中で, `tree`コマンドを実行すると, 次のような出力になるはずです. 78 | 79 | ``` 80 | $ cd PerlEntrance-OOPTutorial 81 | $ tree 82 | . 83 | ├── Build.PL 84 | ├── Changes 85 | ├── LICENSE 86 | ├── META.json 87 | ├── README.md 88 | ├── cpanfile 89 | ├── lib 90 | │   └── PerlEntrance 91 | │   └── OOPTutorial.pm 92 | ├── minil.toml 93 | └── t 94 | └── 00_compile.t 95 | 96 | 3 directories, 9 files 97 | ``` 98 | 99 | ## `Build.PL` 100 | 101 | モジュールをビルドする為のファイルです. 開くと, 102 | 103 | ```:Build.PL(抜粋) 104 | # ========================================================================= 105 | # THIS FILE IS AUTOMATICALLY GENERATED BY MINILLA. 106 | # DO NOT EDIT DIRECTLY. 107 | # ========================================================================= 108 | ``` 109 | 110 | と書いてある通り, 基本的には手動で書き換えることはありません. 111 | 112 | ## `Changes` 113 | 114 | モジュールの更新履歴です. 初期状態では次のようになっています. 115 | 116 | ```:Changes 117 | Revision history for Perl extension PerlEntrance-OOPTutorial 118 | 119 | {{$NEXT}} 120 | 121 | - original version 122 | ``` 123 | 124 | このように書いておくと, モジュールのリリース時に`{{$NEXT}}`の部分が現在の日付に置き換わり, 125 | 126 | ```:Changes 127 | Revision history for Perl extension PerlEntrance-OOPTutorial 128 | 129 | 0.01 2015-05-05T09:00:00Z 130 | 131 | - original version 132 | ``` 133 | 134 | のように書き換わってくれます. 135 | 136 | ## `LICENSE` 137 | 138 | このモジュールのライセンスを表すファイルです. 139 | 初期状態では, PerlのモジュールはPerlと同じ[Artistic License](http://ja.wikipedia.org/wiki/Artistic_License)か[GPL](http://ja.wikipedia.org/wiki/GNU_General_Public_License)を選択できるライセンスになっています. 140 | 141 | よほど特殊な事情ではないかぎり, 書き換える必要はありません. 142 | 143 | ## `META.json` 144 | 145 | モジュールのメタ情報が格納されたファイルです. 146 | こちらも`Build.PL`と同様, Minillaが自動的に生成してくれるので, 手動で書き換える必要はありません. 147 | 148 | ## `cpanfile` 149 | 150 | このモジュールが依存する(必要とする)モジュールを書くファイルです. 151 | 初期状態では, 次のようになっています. 152 | 153 | ```perl:cpanfile 154 | requires 'perl', '5.008001'; 155 | 156 | on 'test' => sub { 157 | requires 'Test::More', '0.98'; 158 | }; 159 | ``` 160 | 161 | `cpanfile`はPerlのDSLとして提供されており, `requires <モジュール名>, <バージョン>;`という記述で, このモジュールが`<モジュール名`に依存しており, その依存モジュールの`<バージョン>`で指定したバージョンが必要である, ということを指定することができます(`<バージョン>`については省略することができます). 162 | 163 | ```perl 164 | requires 'perl', '5.008001'; 165 | ``` 166 | 167 | ここでは, このモジュールがPerlの5.8.1以上を必要としていることを指定しています(基本的に, ここ最近のモジュールはPerl 5.8.1以上で動作するように作ることが一般的です). 168 | 169 | 例えば, このモジュールが`JSON`に依存しているのであれば, 170 | 171 | ```perl:cpanfile 172 | requires 'perl', '5.008001'; 173 | requires 'JSON'; # 追加 174 | 175 | on 'test' => sub { 176 | requires 'Test::More', '0.98'; 177 | } 178 | ``` 179 | 180 | のように記述すると良いでしょう. 181 | 182 | なお, `on 'test' => sub { ... };`の部分は, モジュールの動作に必要はないが, テストの為に必要なモジュールを指定する部分です. 183 | 後述しますが, Perlにはテストを支援する様々なモジュールが提供されています. 184 | それらを利用するのであれば, このエリアに記述するようにしましょう. 185 | 186 | ## `minil.toml` 187 | 188 | Minillaの設定ファイルです. 189 | 基本的には書き換える必要はありません. 190 | 191 | ## `lib` 192 | 193 | モジュール本体のコードを格納するディレクトリです. 194 | 詳しくは後述します. 195 | 196 | ## `t` 197 | 198 | モジュールのテストコードを格納するディレクトリです. 199 | こちらも, 詳しくは後述します. 200 | 201 | # 名前空間 202 | 203 | Perlには, 「名前空間」という概念があります. 204 | 名前空間は, `package`という構文で指定することができます. 205 | 206 | ```perl:namespace.pl 207 | use strict; 208 | use warnings; 209 | use utf8; 210 | 211 | hoge(); 212 | 213 | package Hoge; 214 | 215 | sub hoge { 216 | print 'hoge'; 217 | } 218 | ``` 219 | 220 | このコードを例に, Perlの名前空間を解説していきます. 221 | 222 | まず5行目では, `hoge();`で`hoge`というサブルーチンを呼び出しています. 223 | 一方7行目以降では, `package Hoge;`で`Hoge`という名前空間を用意しており, その中で`hoge`というサブルーチンを定義しています. 224 | 225 | この状態でスクリプトを実行すると, どうなるでしょうか? 226 | 227 | ``` 228 | $ perl namespace.pl 229 | Undefined subroutine &main::hoge called at namespace.pl line 5. 230 | ``` 231 | 232 | `hoge`というサブルーチンが定義されていない, というエラーが出ました. 233 | ここで注目して欲しいのは, `&main::hoge`の部分です. 234 | 実は, Perlでは名前空間が指定されていない場合, 自動的に`main`という名前空間に属している, と処理されるのです. 235 | そして, 特に指定がない場合, Perlは現在の名前空間(この場合, `main`)から変数やサブルーチンを探し出し, 処理しようとします. 236 | そのため, このエラーは`main`という名前空間に`hoge`という関数が定義されていないので, エラーになったという意味になります. 237 | 238 | 前述の通り, `hoge`というサブルーチンは`Hoge`という名前空間で定義されているので, `main`という名前空間からそのまま利用することができません. 239 | `main`から, `Hoge`という名前空間のサブルーチンを呼ぶ為には, `hoge`というサブルーチンを呼び出す際に, その名前空間も指定しなければなりません. 240 | 241 | ```perl:namespace.pl 242 | use strict; 243 | use warnings; 244 | use utf8; 245 | 246 | Hoge::hoge(); 247 | 248 | package Hoge; 249 | 250 | sub hoge { 251 | print 'hoge'; 252 | } 253 | ``` 254 | 255 | `hoge();`を`Hoge::hoge();`に書き換えました. 256 | スクリプトを実行すると... 257 | 258 | ``` 259 | $ perl namespace.pl 260 | hoge 261 | ``` 262 | 263 | `main`名前空間から, `Hoge`名前空間の`hoge`というサブルーチンを呼び出すことができました. 264 | ここまでの内容をまとめると, 次のようになると思います. 265 | 266 | - Perlのサブルーチンや変数は, 必ず何らかの名前空間に属している 267 | - 名前空間の指定がない場合, `main`という名前空間に属すことになる 268 | - サブルーチンや変数を呼び出す際, 名前空間を省略すると, 現在の名前空間から探しだそうとする 269 | - `Hoge::`のように, プレフィックスに名前空間を付けると, 他の名前空間のサブルーチンを呼び出すことができる 270 | 271 | なお, 余談ではありますが, 現在の名前空間は`__PACKAGE__`で確認することができます. 272 | 273 | ```perl:namespace.pl 274 | use strict; 275 | use warnings; 276 | use utf8; 277 | 278 | print __PACKAGE__ . "\n"; # => main 279 | Hoge::hoge(); 280 | 281 | package Hoge; 282 | 283 | sub hoge { 284 | print __PACKAGE__ . "\n"; # => Hoge 285 | } 286 | ``` 287 | 288 | もし1つの名前空間しかない場合, プログラマは全ての変数名とサブルーチン名が既存のものと重複しないことを確認しながらコーディングをしなければなりません. 289 | しかし, 名前空間を適切に利用すれば, 変数やサブルーチンといったコードの影響範囲を, 名前空間の内部だけに抑えることが出来ます. 290 | 更に, 適切な名前空間を与えることで, そのコードの役割(例えば, `MyApp::Model::User`の場合, この名前空間に存在するコードは, `MyApp`というアプリケーションの`User`に関する`Model`についてのコードである, など)を伝えることもできるでしょう. 291 | 292 | ## 名前空間とファイルの配置 293 | 294 | Perlの名前空間とライブラリのファイル配置には, 密接な関係があります. 295 | 例えば, あるスクリプトで`Foo::Bar::Baz`という名前空間のコードを利用するために, `use Foo::Bar::Baz;`と記載したとします. 296 | このとき, Perlは`lib`ディレクトリ以下にある`Foo/Bar/Baz.pm`というファイルを探索し, 見つければそのファイルを`Foo::Bar::Baz`のコードとして処理します. 297 | 逆に言えば, `Foo::Bar::Baz`という名前空間のコードは, `lib`ディレクトリ以下の`Foo/Bar/Baz.pm`というファイルに記載しなければ, Perlはうまくコードを見つけ出すことが出来ず, 期待する動作をしてくれません. 298 | 299 | それではもう一度, `PerlEntrance::OOPTutorial`の内部で`tree`コマンドを実行した時の出力を見てみます. 300 | 301 | ``` 302 | . 303 | ├── Build.PL 304 | ├── Changes 305 | ├── LICENSE 306 | ├── META.json 307 | ├── README.md 308 | ├── cpanfile 309 | ├── lib 310 | │   └── PerlEntrance 311 | │   └── OOPTutorial.pm 312 | ├── minil.toml 313 | └── t 314 | └── 00_compile.t 315 | 316 | 3 directories, 9 files 317 | ``` 318 | 319 | `PerlEntrance::OOPTutorial`という名前空間に属するコードは, `lib`ディレクトリ以下の`PerlEntrance`ディレクトリにある, `OOPTutorial.pm`というファイルに記載するようになっています. 320 | Perlでコードを書く時は, 必ずコードの名前空間とファイル名, ディレクトリ構成を同じにするようにしましょう(このどちらかに誤りがあり, 期待通りにコードが動かない... というのは, Perlでモジュールを作ったり, オブジェクト指向プログラミングをする時によくあるミスです). 321 | 322 | # Perlのオブジェクト指向プログラミング 323 | 324 | それでは早速, Perlのオブジェクト指向プログラミングについて体験していきます. 325 | 誤解を恐れずに一言で言えば, Perlはオブジェクト指向を, 「名前空間でデータを包む」ことによって実現している, と言うことができます. 326 | 327 | ```perl:oop.pl 328 | use strict; 329 | use warnings; 330 | use utf8; 331 | 332 | my $papix = Engineer->new( 333 | name => 'papix', 334 | ); 335 | 336 | my $hoto = Engineer->new( 337 | name => 'hoto', 338 | ); 339 | 340 | print $papix->name . "\n"; # => papix 341 | print $hoto->name . "\n"; # => hoto 342 | 343 | package Engineer; 344 | 345 | sub new { 346 | my ($class, %params) = @_; 347 | 348 | bless { 349 | name => $params{name}, 350 | }, $class; 351 | } 352 | 353 | sub name { 354 | my ($self) = @_; 355 | return $self->{name}; 356 | } 357 | ``` 358 | 359 | 実行結果は, 次のようになります. 360 | 361 | ``` 362 | $ perl oop.pl 363 | papix 364 | hoto 365 | ``` 366 | 367 | コードを詳しく見ていきましょう. 368 | まずは, `Engineer`という名前空間のコードから見ていきます. 369 | 370 | ## コンストラクタ 371 | 372 | ```perl:oop.pl(抜粋) 373 | package Engineer; 374 | 375 | sub new { 376 | my ($class, %params) = @_; 377 | 378 | bless { 379 | name => $params{name}, 380 | }, $class; 381 | } 382 | 383 | sub name { 384 | my ($self) = @_; 385 | return $self->{name}; 386 | } 387 | ``` 388 | 389 | `new`というサブルーチンはコンストラクタです. 390 | `new`でなければならない, というルールはありませんが, Perlではコンストラクタは`new`というサブルーチンを使うのが一般的です. 391 | 392 | サブルーチン`new`において, 引数を`my ($class, %params) = @_;`という形で受けています. 393 | 一方, このコンストラクタの呼び出し元を見ると, 次のようなコードになっています. 394 | 395 | ```perl:oop.pl(抜粋) 396 | my $papix = Engineer->new( 397 | name => 'papix', 398 | ); 399 | ``` 400 | 401 | 実はPerlは, `Engineer->new( ... );`のような形, つまり`名前空間->サブルーチン名( ... );`というフォーマットで他の名前空間のサブルーチンを呼び出した場合, 呼び出したサブルーチンの第1引数には呼び出した名前空間(つまりは, `->`の左側)が文字列で渡るようになっています. 402 | 403 | その為, コンストラクタを次のように書き換えると... 404 | 405 | ```perl:oop.pl(抜粋) 406 | sub new { 407 | my ($class, %params) = @_; 408 | 409 | print $class . "\n"; 410 | 411 | bless { 412 | name => $params{name}, 413 | }, $class; 414 | } 415 | ``` 416 | 417 | 実行結果は次のようになります. 418 | 419 | ``` 420 | $ perl oop.pl 421 | Engineer 422 | Engineer 423 | papix 424 | hoto 425 | ``` 426 | 427 | `Engineer->new( ... );`で呼び出しているので, コンストラクタの第1引数には`Engineer`という文字列が渡ってきていることがわかります. 428 | 429 | ...この時点で, 「何だこれ, 気持ち悪い」と思う方も多いのではないでしょうか. 430 | しかし**気持ち悪いのはまだまだこれから**です. 431 | 432 | ### 「祝福」 433 | 434 | ```perl:oop.pl(抜粋) 435 | sub new { 436 | my ($class, %params) = @_; 437 | 438 | print $class . "\n"; 439 | 440 | bless { 441 | name => $params{name}, 442 | }, $class; 443 | } 444 | ``` 445 | 446 | コンストラクタの最後で, `%params`で受けたパラメータを利用してハッシュリファレンスを生成し, それと引数として受け取ったクラス名(今回の場合は, `Engineer`)を, `bless`という関数に渡しています. 447 | 実は, Perlという言語におけるオブジェクトは, このようにして`bless`という関数を使って, リファレンス(大抵の場合, ハッシュリファレンス)と名前空間を「祝福」することによって生成されるのです(!?). 448 | 449 | これで, 第1引数のハッシュリファレンスで定義したデータと, 第2引数で指定した名前空間のコードが結びつき, 今回の場合はハッシュリファレンスから`Engineer`という名前空間に属するサブルーチンをメソッドとして呼び出すことができるようになります. 450 | 451 | ## オブジェクトからのメソッド呼び出し 452 | 453 | というわけで, コンストラクタを使って, 454 | 455 | ``` 456 | my $papix = Engineer->new( 457 | name => 'papix', 458 | ); 459 | ``` 460 | 461 | のようにして, `{ name => 'papix' }`というデータと`Engineer`という名前空間が紐付いたオブジェクトが生成され, 462 | 463 | ``` 464 | my $hoto = Engineer->new( 465 | name => 'hoto', 466 | ); 467 | ``` 468 | 469 | のようにして, `{ name => 'hoto' }`というデータと`Engineer`という名前空間が紐付いたオブジェクトが生成されました. 470 | この2つのオブジェクトを, Data::Dumperを使って確認してみましょう. 471 | 472 | ```perl:oop.pl 473 | use strict; 474 | use warnings; 475 | use utf8; 476 | 477 | use Data::Dumper; 478 | 479 | my $papix = Engineer->new( 480 | name => 'papix', 481 | ); 482 | 483 | my $hoto = Engineer->new( 484 | name => 'hoto', 485 | ); 486 | 487 | print Dumper $papix; 488 | print Dumper $hoto; 489 | 490 | package Engineer; 491 | 492 | sub new { 493 | my ($class, %params) = @_; 494 | 495 | bless { 496 | name => $params{name}, 497 | }, $class; 498 | } 499 | 500 | sub name { 501 | my ($self) = @_; 502 | return $self->{name}; 503 | } 504 | ``` 505 | 506 | 実行結果は, 次の通りになります. 507 | 508 | ``` 509 | $ perl oop.pl 510 | $VAR1 = bless( { 511 | 'name' => 'papix' 512 | }, 'Engineer' ); 513 | $VAR1 = bless( { 514 | 'name' => 'hoto' 515 | }, 'Engineer' ); 516 | ``` 517 | 518 | `$papix`と`$hoto`という変数には, それぞれ別のハッシュリファレンスと名前空間(`Engineer`)を祝福(`bless`)したもの(つまりはオブジェクト)が格納されている, という意味です. 519 | 520 | それでは次に, オブジェクトからメソッドを呼び出してみます. 521 | 522 | ```perl:oop.pl 523 | use strict; 524 | use warnings; 525 | use utf8; 526 | 527 | my $papix = Engineer->new( 528 | name => 'papix', 529 | ); 530 | 531 | my $hoto = Engineer->new( 532 | name => 'hoto', 533 | ); 534 | 535 | print $papix->name . "\n"; 536 | print $hoto->name . "\n"; 537 | 538 | package Engineer; 539 | 540 | sub new { 541 | my ($class, %params) = @_; 542 | 543 | bless { 544 | name => $params{name}, 545 | }, $class; 546 | } 547 | 548 | sub name { 549 | my ($self) = @_; 550 | return $self->{name}; 551 | } 552 | ``` 553 | 554 | 実行結果は次の通りです. 555 | 556 | ``` 557 | $ perl oop.pl 558 | papix 559 | hoto 560 | ``` 561 | 562 | オブジェクトからメソッドを呼び出す場合, `$object->method();`で呼び出すことが出来ます. 563 | このときメソッドは, そのオブジェクトに紐付いた名前空間から検索します. 564 | 例えば, `$papix->name();`のように呼び出した場合, `name`というメソッドは, `$papix`に紐付いた名前空間, すなわち`Engineer`という名前空間から検索するのです. 565 | 566 | 実際, 存在しないメソッドを呼び出した場合... 例えば`$papix->hoge;`のようなコードを書いた場合, Perlは次のようなエラーを出力します. 567 | 568 | ``` 569 | $ perl oop.pl 570 | Can't locate object method "hoge" via package "Engineer" at oop.pl line 15. 571 | ``` 572 | 573 | Perlは, `$papix`に紐付いた`Engineer`という名前空間から`hoge`というメソッドを探したが, 見つからなかった, という意味です. 574 | 575 | ## メソッドの実装 576 | 577 | さて, 呼び出されたメソッドはどのように実装すれば良いのでしょうか. 578 | 再び, `Engineer`名前空間のコードを見てみましょう. 579 | 580 | ```perl:oop.pl 581 | package Engineer; 582 | 583 | sub new { 584 | my ($class, %params) = @_; 585 | 586 | bless { 587 | name => $params{name}, 588 | }, $class; 589 | } 590 | 591 | sub name { 592 | my ($self) = @_; 593 | return $self->{name}; 594 | } 595 | ``` 596 | 597 | `name`というサブルーチンが, `Engineer`という名前空間に紐ついたオブジェクトの`name`メソッドの実装になります. 598 | ここでの重要なポイントは, サブルーチンの引数を`my ($self) = @_;`で受けている点です. 599 | 600 | 先程, コンストラクタを呼び出す場面で, `Engineer->new( ... );`のように呼び出すと, 呼び出した`new`というメソッドの第1引数には`Engineer`という文字列, つまり`->`の左側が渡ってくると説明しました. 601 | これと同様, `$papix->name()`のようにオブジェクトから呼び出した場合, 呼び出したメソッドの第1引数には`->`の左側, つまり`$papix`のオブジェクト自身が渡ってきます. 602 | そのため, メソッドの第1引数は自分自身, つまり`$self`で受けることが一般的です. 603 | 604 | `name`というメソッドの中で, `$self`の中身をData::Dumperでダンプしてみます. 605 | 606 | ``` 607 | use strict; 608 | use warnings; 609 | use utf8; 610 | 611 | my $papix = Engineer->new( 612 | name => 'papix', 613 | ); 614 | 615 | my $hoto = Engineer->new( 616 | name => 'hoto', 617 | ); 618 | 619 | print $papix->name . "\n"; 620 | print $hoto->name . "\n"; 621 | 622 | package Engineer; 623 | use Data::Dumper; # Data::DumperはEngineer名前空間で利用しているので, ここでuseしなければならない 624 | 625 | sub new { 626 | my ($class, %params) = @_; 627 | 628 | bless { 629 | name => $params{name}, 630 | }, $class; 631 | } 632 | 633 | sub name { 634 | my ($self) = @_; 635 | print Dumper $self; 636 | return $self->{name}; 637 | } 638 | ``` 639 | 640 | 実行結果は次のようになります. 641 | 642 | ``` 643 | $ perl oop.pl 644 | $VAR1 = bless( { 645 | 'name' => 'papix' 646 | }, 'Engineer' ); 647 | papix 648 | $VAR1 = bless( { 649 | 'name' => 'hoto' 650 | }, 'Engineer' ); 651 | hoto 652 | ``` 653 | 654 | `$papix`から`name`メソッドを呼び出した時の`$self`の中身と, `$hoto`から`name`メソッドを呼び出した時の`$self`の中身は, 先程`main`名前空間で`$papix`及び`$hoto`をダンプした時と同じであることがわかります. 655 | 656 | 後はメソッドの中で, `$self`で受け取った自分自身のオブジェクトを利用して, メソッドで実現したい操作をしてやれば良いです. 657 | `bless`で祝福されたハッシュリファレンスも, 基本的には普通のハッシュリファレンスのように利用できるので, 例えば`Engineer`名前空間の`name`メソッドのように, 658 | 659 | ``` 660 | sub name { 661 | my ($self) = @_; 662 | return $self->{name}; 663 | } 664 | ``` 665 | 666 | `$self`というハッシュリファレンスの`name`というキーのデータを返すようにすれば, オブジェクト(に格納されたハッシュリファレンス)から`name`というキーのデータを返すことが出来ますし, 667 | 668 | ``` 669 | sub supernize { 670 | my ($self) = @_; 671 | if ($self->{name} eq 'hoto') { 672 | $self->{name} = 'super hoto'; 673 | } 674 | } 675 | ``` 676 | 677 | のようなメソッドを用意すれば... 678 | 679 | ```perl:oop.pl 680 | use strict; 681 | use warnings; 682 | use utf8; 683 | 684 | my $papix = Engineer->new( 685 | name => 'papix', 686 | ); 687 | 688 | my $hoto = Engineer->new( 689 | name => 'hoto', 690 | ); 691 | 692 | print $papix->name . "\n"; 693 | print $hoto->name . "\n"; 694 | print "--------------------\n"; 695 | $papix->supernize; 696 | $hoto->supernize; 697 | print $papix->name . "\n"; 698 | print $hoto->name . "\n"; 699 | 700 | package Engineer; 701 | 702 | sub new { 703 | my ($class, %params) = @_; 704 | 705 | bless { 706 | name => $params{name}, 707 | }, $class; 708 | } 709 | 710 | sub name { 711 | my ($self) = @_; 712 | return $self->{name}; 713 | } 714 | 715 | sub supernize { 716 | my ($self) = @_; 717 | if ($self->{name} eq 'hoto') { 718 | $self->{name} = 'super hoto'; 719 | } 720 | } 721 | ``` 722 | 723 | 実行結果は, 次のようになります. 724 | 725 | ``` 726 | papix 727 | hoto 728 | -------------------- 729 | papix 730 | super hoto 731 | ``` 732 | 733 | オブジェクト(に格納されたハッシュリファレンス)の中身を, メソッドの中で書き換えることも可能です. 734 | 735 | # ファイルの切り出し 736 | 737 | ここまで, `oop.pl`というスクリプトを元にして, Perlにおけるオブジェクト指向プログラミングの実装方法について解説してきました. 738 | このスクリプトでは, `main`名前空間と`Engineer`名前空間を同じファイルに記述していましたが, このように同じスクリプトに記述することは滅多になく, `Engineer`名前空間で書いていたコードは`lib`ディレクトリ以下に切り出すことが大半です. 739 | 740 | 最後に, オブジェクト指向を構成するファイルを別ファイルに切り出し, これを任意のスクリプトから呼び出す方法について解説します. 741 | 今回は, `oop.pl`の`Engineer`名前空間で記述したコードを, 先程作ったひな形の`PerlEntrance::OOPTutorial::Engineer`という名前空間として切り出してみることにします. 742 | 743 | `PerlEntrance::OOPTutorial::Engineer`は, `lib/PerlEntrance/OOPTutorial/Engineer.pm`として配置しなければなりません. 744 | まずは, `lib/PerlEntrance`ディレクトリに移動し, `OOPTutorial`というディレクトリを作成しましょう. 745 | 746 | ``` 747 | $ ls 748 | Build.PL Changes LICENSE META.json README.md cpanfile lib minil.toml t 749 | 750 | $ cd lib/PerlEntrance 751 | $ ls 752 | OOPTutorial.pm 753 | 754 | $ mkdir OOPTutorial 755 | $ ls 756 | OOPTutorial OOPTutorial.pm 757 | ``` 758 | 759 | それでは, `OOPTutorial`ディレクトリの中に, `Engineer.pm`を作成します. 760 | 761 | ``` 762 | $ vi OOPTutorial/Engineer.pm 763 | ``` 764 | 765 | 中身は, 次のようにします. 766 | 767 | ```perl:lib/PerlEntrance/OOPTutorial/Engineer.pm 768 | package PerlEntrance::OOPTutorial::Engineer; 769 | use strict; 770 | use warnings; 771 | use utf8; 772 | 773 | sub new { 774 | my ($class, %params) = @_; 775 | 776 | bless { 777 | name => $params{name}, 778 | }, $class; 779 | } 780 | 781 | sub name { 782 | my ($self) = @_; 783 | return $self->{name}; 784 | } 785 | 786 | 1; 787 | ``` 788 | 789 | 末尾の`1;`ですが, とりあえずここではPerlの「お約束」だと思って下さい. 790 | Perlのモジュールを構成するファイル(`*.pm`のファイル)は, 必ず最後に真値を返さなければなりません. 791 | 792 | それでは, このモジュールを利用したスクリプトを書いてみます. 793 | Perlのモジュールでは, サンプルスクリプトを`eg`というディレクトリに配置することが多いので, `eg`ディレクトリに`oop.pl`というスクリプトを用意することにします. 794 | 795 | ``` 796 | $ ls 797 | Build.PL Changes LICENSE META.json README.md cpanfile lib minil.toml t 798 | 799 | $ mkdir eg 800 | $ cd eg 801 | $ vi oop.pl 802 | ``` 803 | 804 | `oop.pl`の中身は, 次のようにします. 805 | 806 | ```perl:eg/oop.pl 807 | use strict; 808 | use warnings; 809 | use utf8; 810 | 811 | use PerlEntrance::OOPTutorial::Engineer; 812 | 813 | my $papix = PerlEntrance::OOPTutorial::Engineer->new( 814 | name => 'papix', 815 | ); 816 | 817 | my $hoto = PerlEntrance::OOPTutorial::Engineer->new( 818 | name => 'hoto', 819 | ); 820 | 821 | print $papix->name . "\n"; 822 | print $hoto->name . "\n"; 823 | ``` 824 | 825 | 5行目で, 先程作成した`PerlEntrance::OOPTutorial::Engineer`を読み込み, これを利用してオブジェクトを生成し, メソッドを呼び出しています. 826 | 827 | それでは, 早速実行してみましょう. 828 | 829 | ``` 830 | $ cd ../ 831 | $ ls 832 | Build.PL Changes LICENSE META.json README.md cpanfile eg lib minil.toml t 833 | 834 | $ perl eg/oop.pl 835 | Can't locate PerlEntrance/OOPTutorial/Engineer.pm in @INC (you may need to install the PerlEntrance::OOPTutorial::Engineer module) (@INC contains: /Users/username/.anyenv/envs/plenv/versions/5.18/lib/perl5/site_perl/5.18.2/darwin-2level /Users/username/.anyenv/envs/plenv/versions/5.18/lib/perl5/site_perl/5.18.2 /Users/username/.anyenv/envs/plenv/versions/5.18/lib/perl5/5.18.2/darwin-2level /Users/username/.anyenv/envs/plenv/versions/5.18/lib/perl5/5.18.2 .) at eg/oop.pl line 5. 836 | BEGIN failed--compilation aborted at eg/oop.pl line 5. 837 | ``` 838 | 839 | ...実行に失敗しました. 840 | これは, 先程作成した`PerlEntrance::OOPTutorial::Engineer`というモジュールが設置されているディレクトリを, Perlがライブラリを検索する際に検索対象としていないからです. 841 | このモジュールはカレントディレクトリの`lib`以下に設置されているので, このディレクトリを`perl`コマンドの`-I`オプション(検索対象となるディレクトリの追加)で指定すればOKです. 842 | 843 | ``` 844 | $ perl -Ilib eg/oop.pl 845 | papix 846 | hoto 847 | ``` 848 | 849 | # 練習問題 850 | 851 | `minil`コマンドでモジュールのひな形が準備が出来たら, このカリキュラムで用意した`PerlEntrance::OOPTutorial::Engineer`を実装してみよう. 852 | また, `PerlEntrance::OOPTutorial::Engineer`以外のクラスを提供してみよう(例えば, `PerlEntrance::OOPTutorial::Chief`など). 853 | `eg`ディレクトリ以下に, これらのクラスを利用した, 適当なスクリプトを書いてみよう. 854 | 855 | ```perl:例 856 | use strict; 857 | use warnings; 858 | use utf8; 859 | 860 | use PerlEntrance::OOPTutorial::Engineer; 861 | use PerlEntrance::OOPTutorial::Chief; 862 | 863 | my $hoto = PerlEntrance::OOPTutorial::Engineer->new( name => 'hoto' ); 864 | my $higo = PerlEntrance::OOPTutorial::Chief->new( name => 'higo' ); 865 | 866 | print $hoto->job(); # => 'engineer' 867 | print $higo->job(); # => 'chief' 868 | ``` 869 | -------------------------------------------------------------------------------- /orm.md: -------------------------------------------------------------------------------- 1 | # はじめに 2 | 3 | Webアプリケーションを作る際, そのWebアプリケーションが利用するデータを永続的に保存する為にデータストアを利用することが多いでしょう. 4 | データストアには様々な種類がありますが, 一般的にはRDBMS, その中でも特にMySQLを使う機会が多いと思います. 5 | 6 | Perlには, RDBMSのインターフェースとしてDBIというモジュールがあり, これと各種RDBMSと接続するDBD系モジュールを組み合わせることでRDBMSの操作を行います. 7 | 実際にRDBMSと接続するDBD系モジュールと, そのインターフェイスとなるDBIモジュールで分離することで, ユーザは利用しているRDBMSの違いをほぼ気にせずにデータベースを操作することが可能です. 8 | 9 | 一方, ORM(O/Rマッパ, Object-Relational Mapper)は, RDBMSから取得した単一行のデータを, 特定のクラスと紐付けてオブジェクト化してくれる仕組みです. 10 | DBIを利用してRDBMSからデータを取得した場合, RDBMSから取得したデータは, ハッシュリファレンスや配列リファレンスといった形で取得することになります. 11 | これに対してORMを利用した場合はRDBMSからオブジェクト(の集合)として取得することが出来るので, ハッシュリファレンスや配列リファレンスをそのまま利用する場合と比べて, データの取り回しが比較的楽になります. 12 | 13 | ここでは, Perl製の軽量ORMであり, Amon2でデフォルトで利用されているTengというORMについて解説します. 14 | 15 | # Tengのインストールと初期設定 16 | 17 | `cpanm`コマンドからインストールすることができます. 18 | 19 | ``` 20 | $ cpanm Teng 21 | --> Working on Teng 22 | Fetching http://www.cpan.org/authors/id/S/SA/SATOH/Teng-0.28.tar.gz ... OK 23 | Configuring Teng-0.28 ... OK 24 | Building and testing Teng-0.28 ... OK 25 | Successfully installed Teng-0.28 (upgraded from 0.25) 26 | 1 distribution installed 27 | ``` 28 | 29 | ## スキーマ情報の設定 30 | 31 | ORMを使う為には, 予め利用するデータベースのスキーマ情報を提供しなければなりません. 32 | Tengの場合, Teng::Schema::DeclareかTeng::Schema::Loaderを利用することでスキーマ情報を提供することが可能です. 33 | 34 | Teng::Schema::Declareは, このモジュールが提供するDSLを使ってスキーマ情報を定義することが出来ます. 35 | 後述する, `inflate`や`deflate`機能の設定を含む, 細かいスキーマ情報の指定が可能です. 36 | Teng::Schema::Declareを利用したスキーマは, 手で書くことも出来ますが, Teng::Schema::Dumperを利用してデータベースからスキーマを取得して生成することも可能です. 37 | また, データベースのスキーマをDBIx::Schema::DSLを利用して定義している場合, SQL::TranslatorとSQL::Translator::Producer::Tengを利用してTengのスキーマを用意することもできます. 38 | 39 | 一方, Teng::Schema::Loaderは, Tengはデータベースから直接スキーマ情報を取得して利用します. 40 | Teng::Schema::Declareのように, 細かいスキーマ定義は出来ませんが, 非常に簡単にスキーマを用意することが出来るので, 簡単なアプリケーションの作成や, 軽くTengを試してみたい時に利用するとよいでしょう. 41 | 42 | 今回は, 手動でTeng::Schema::Declareを利用したスキーマを用意し, これを利用してTengを使うことにします. 43 | また, RDBMSは簡単のためにSQLiteを利用することとしましょう. 44 | 45 | ```sql 46 | CREATE TABLE IF NOT EXISTS user ( 47 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 48 | name VARCHAR(255), 49 | created_at INTEGER NOT NULL, 50 | updated_at INTEGER NOT NULL 51 | ); 52 | ``` 53 | 54 | 今回利用するテーブルのDDL(Data Definition Language)は上記の通りにします. 55 | `user`というテーブルに, 主キーかつAuto IncrementなINT型の`id`, ユーザの名前を持つVARCHAR(255)の`name`, そしてデータ生成日時/更新日時を保持する`created_at`と`updated_at`を持たせます. 56 | SQLiteには, MySQLにおける(時間を保持するための)DATETIME型のようなデータ型がないので, ここではepoch秒をINTEGER型で持たせることとしましょう. 57 | 58 | さて, 今回の課題では, `teng.pl`というスクリプトからTengを利用し, スキーマファイルは`lib/MyApp/DB/Schema.pm`に配置することにします. 59 | `tree`コマンドでディレクトリ構成を確認すると, 次のようになります. 60 | 61 | ``` 62 | . 63 | ├── lib 64 | │   └── MyApp 65 | │   └── DB 66 | │   └── Schema.pm 67 | └── teng.pl 68 | ``` 69 | 70 | 早速, 先程のDDLにあわせて`Schema.pm`を用意します. 71 | 72 | ```perl:lib/MyApp/DB/Schema.pm 73 | package MyApp::DB::Schema; 74 | use strict; 75 | use warnings; 76 | use utf8; 77 | 78 | use Teng::Schema::Declare; 79 | 80 | base_row_class 'MyApp::DB::Row'; 81 | 82 | table { 83 | name 'user'; 84 | pk 'id'; 85 | columns qw(id name created_at updated_at); 86 | }; 87 | 88 | 1; 89 | ``` 90 | 91 | ## その他の準備 92 | 93 | 更にTengを利用するにあたって, Tengを継承したクラスを用意しなければなりません. 94 | 今回は, `MyApp::DB`という名前空間で提供することにします. 95 | 96 | ```perl:lib/MyApp/DB.pm 97 | package MyApp::DB; 98 | use parent qw/Teng/; 99 | 100 | 1; 101 | ``` 102 | 103 | また, Tengがオブジェクトを作る時のベースクラスとなる, `MyApp::DB::Row`も作っておきます. 104 | 105 | ```perl:lib/MyApp/DB/Row.pm 106 | package MyApp::DB::Row; 107 | use strict; 108 | use warnings; 109 | use utf8; 110 | use parent qw(Teng::Row); 111 | 112 | 1; 113 | ``` 114 | 115 | これでTengの準備は完了です. 116 | 117 | # データベースへの接続 118 | 119 | ## SQLiteの準備 120 | 121 | Tengを使ってデータベースに接続する前に, 接続先となるSQLiteの準備をしておきましょう. 122 | `teng.pl`と同じ階層に`sqlite.sql`を設置し, 先程紹介したDDLを記載しておきます. 123 | 124 | ```sql:sqlite.sql 125 | CREATE TABLE IF NOT EXISTS user ( 126 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 127 | name VARCHAR(255), 128 | created_at INTEGER NOT NULL, 129 | updated_at INTEGER NOT NULL 130 | ); 131 | ``` 132 | 133 | SQLiteはファイルベースのデータベースなので, 今回は`teng.pl`と同じ階層に`sqlite.db`という名前で用意することにします. 134 | 135 | ``` 136 | $ sqlite3 sqlite.db < sqlite.sql 137 | ``` 138 | 139 | 問題なく処理が終了していれば, カレントディレクトリに`sqlite.db`というファイルが生成されているはずです. 140 | この段階で, `tree`コマンドの出力は次のようになります. 141 | 142 | ``` 143 | $ tree 144 | . 145 | ├── lib 146 | │   └── MyApp 147 | │   ├── DB 148 | │   │   ├── Row.pm 149 | │   │   └── Schema.pm 150 | │   └── DB.pm 151 | ├── sqlite.db 152 | ├── sqlite.sql 153 | └── teng.pl 154 | 155 | 3 directories, 6 files 156 | ``` 157 | 158 | ## Tengによるデータベースへの接続 159 | 160 | それでは, `teng.pl`からTengを利用してSQLiteのデータベースに接続してみましょう. 161 | 162 | ```perl:teng.pl 163 | use strict; 164 | use warnings; 165 | use utf8; 166 | 167 | use lib 'lib'; 168 | use MyApp::DB; 169 | use MyApp::DB::Schema; 170 | 171 | my $teng = MyApp::DB->new( 172 | connect_info => ['dbi:SQLite:dbname=sqlite.db', '', '', +{ sqlite_unicode => 1 } ], 173 | schema_class => 'MyApp::DB::Schema', 174 | ); 175 | ``` 176 | 177 | Tengを継承した`MyApp::DB`のコンストラクタ(`new`)に, `connect_info`と`schema_class`を渡しています. 178 | 179 | `connect_info`は, DBIでデータベースに接続するときの`connect`メソッドに渡すパラメータと同じです(詳しくは, DBIのドキュメントの`connect`の項を参照して下さい. 日本語ドキュメントは[こちら](http://perldoc.jp/docs/modules/DBI-1.612/DBI.pod#connect)). 180 | 第1引数はDSN(接続対象となるデータベースを指定する為の識別名), 第2引数はデータベースに接続するユーザ名, 第3引数はデータベースに接続するときのパスワード, そして第4引数はオプションを指定します. 181 | 182 | `schema_class`は, 先程生成したTengのスキーマファイルを指定します. 今回は`MyApp::DB::Schema`という名前空間に用意したので, この文字列をそのまま渡します. 183 | 184 | これで, `$teng`からTengを利用することができるようになりました. 185 | これ以降, Tengで利用できる各種メソッドを紹介していきますが, このTengに接続するためのコードについては, 今後は省略していくことにします. 186 | 187 | # Tengの機能 188 | 189 | ## `insert` 190 | 191 | `insert`は, その名の通りデータベースにデータを挿入するメソッドです. 192 | 第1引数にテーブル名を, 第2引数に挿入するデータをハッシュリファレンス形式で指定することができます. 193 | 194 | ```perl:teng.pl(抜粋) 195 | my $obj = $teng->insert(user => { 196 | name => 'papix', 197 | created_at => scalar time(), 198 | updated_at => scalar time(), 199 | }); 200 | ``` 201 | 202 | なお, `insert`メソッドは, 返り値として挿入したデータのオブジェクトを返します(上記コードにおける`$obj`). 203 | 204 | `teng.pl`を実行した後に, `sqlite3`コマンドで挿入できていることを確認してみましょう. 205 | 206 | ``` 207 | $ perl teng.pl 208 | $ sqlite3 sqlite.db 209 | SQLite version 3.7.13 2012-07-17 17:46:21 210 | Enter ".help" for instructions 211 | Enter SQL statements terminated with a ";" 212 | sqlite> select * from user; 213 | 1|papix|1430897608|1430897608 214 | sqlite> 215 | ``` 216 | 217 | SQLiteのデータベースに, `insert`メソッドで追加したデータが問題なく追加されていることがわかります. 218 | `user`テーブルの中身が1行だけでは寂しいので, `insert`メソッドを使って, 更にいくつかデータを足しておくことにします. 219 | 220 | ``` 221 | $ sqlite3 sqlite.db 222 | SQLite version 3.7.13 2012-07-17 17:46:21 223 | Enter ".help" for instructions 224 | Enter SQL statements terminated with a ";" 225 | sqlite> select * from user; 226 | 1|papix|1430897608|1430897608 227 | 2|hoto|1430897796|1430897796 228 | 3|akms|1430897804|1430897804 229 | sqlite> 230 | ``` 231 | 232 | 以降は, このデータをベースに解説していきます. 233 | 234 | ## `single` 235 | 236 | 次に, データベースからデータを取得するためのメソッド, `single`を紹介します. 237 | `single`は, その名の通りデータベースから1件のデータを取得する為のメソッドです. 238 | 239 | ```perl:teng.pl(抜粋) 240 | my $obj = $teng->single('user' => { 241 | name => 'papix' # 'name = papix'という条件 242 | }); 243 | ``` 244 | 245 | `single`メソッドは, 第1引数に検索対象となるテーブル名を, 第2引数に検索条件(いわゆる'WHERE句')を指定できます. 246 | 該当するデータが存在すればそのオブジェクトを, 該当するデータが存在しなければ`undef`を返します. 247 | 248 | なお, TengはクエリビルダとしてSQL::Makerを利用しているので, WHERE句にあたる部分の書き方については[SQL::Maker::Condition](https://metacpan.org/pod/SQL::Maker::Condition)のドキュメントが参考になります. 249 | 250 | 内部的には, `single`メソッドは内部で`LIMIT 1`を指定することによって, 「1件のデータの取得」を実現しています. 251 | 万が一, 条件に該当するデータが複数存在した場合の挙動については, 利用しているRDBMSで`LIMIT 1`を指定した時と同じものが取得されることになります. 252 | 253 | ### オブジェクトから利用できるメソッド 〜getter〜 254 | 255 | `single`や, 次に説明する`search`メソッドなどで取得したオブジェクトからは, 初期状態でいくつかのメソッドが利用出来るようになっています. 256 | 257 | 例えば, オブジェクトはスキーマファイルで定義した「カラム名」のメソッドが利用できるようになっています. 258 | このメソッドを利用することで, そのオブジェクトに格納された, 各カラムのデータを取得することができます. 259 | 260 | ```perl:teng.pl(抜粋) 261 | my $obj = $teng->single('user' => { 262 | name => 'papix', 263 | }); 264 | print $obj->name; # => 'papix' 265 | print $obj->id; # => 1 266 | ``` 267 | 268 | ## `search` 269 | 270 | `search`メソッドは, データを1件のみ取得する`single`メソッドとは異なり, 条件に当てはまるデータを全て取得するメソッドです. 271 | 272 | ```perl:teng.pl(抜粋) 273 | my @users = $teng->search('user' => [ 'name' => { like => '%a%' }]); # 「'name'に'a'を含む」という条件 274 | ``` 275 | 276 | なお, 第2引数として与えることができる条件を省略し, 第1引数のテーブル名のみを与えた場合は, テーブルに含まれる全てのデータを取得することができます. 277 | 278 | ```perl:teng.pl(抜粋) 279 | my @all_users = $teng->search('user'); 280 | ``` 281 | 282 | ### コンテキストによる`search`メソッドの挙動 283 | 284 | さて, この`search`メソッドの返す値ですが, コンテキストによって挙動が異なります. 285 | 286 | ### スカラコンテキスト 287 | 288 | ``` 289 | my $itr = $teng->search('user'); 290 | ``` 291 | 292 | このようにスカラコンテキストで受けた場合, `search`メソッドはTeng::Iteratorのオブジェクトを返します. 293 | Teng::Iteratorからは`next`というメソッドが利用できるようになっており, このメソッドを利用して取得したデータを1件(1オブジェクト)ずつ取得することが可能です. 294 | 295 | `next`メソッドは, 1回目の呼び出しで取得したデータの1番目, 2回目の呼び出しで取得したデータの2番目... といった形でオブジェクトを返し, 全てのオブジェクトを返した後は`undef`を返します. 296 | そのため, Teng::Iteratorの`next`メソッドと`while`文を利用して, 検索した全てのデータを取得することができます. 297 | 298 | 299 | ``` 300 | while (my $user = $itr->next) { 301 | print $user->name; # => 'papix', 'hoto', 'akms'がそれぞれ順番に表示される 302 | } 303 | ``` 304 | 305 | また, Teng::Iteratorには`all`メソッドが用意されており, 取得した全てのデータのオブジェクトをリスト形式で返してくれます. 306 | 307 | ``` 308 | my $itr = $teng->search('user'); 309 | my @all_users = $itr->all; 310 | 311 | for my $user (@all_users) { 312 | print $user->name; # => 'papix', 'hoto', 'akms'がそれぞれ順番に表示される 313 | } 314 | ``` 315 | 316 | #### リストコンテキスト 317 | 318 | 一方, リストコンテキストの場合, Tengは内部でTeng::Iteratorに対して`all`メソッドを実行し, 取得した全てのデータのオブジェクトをリスト形式で返してくれます. 319 | 320 | ``` 321 | my @all_users = $teng->search('user'); 322 | 323 | for my $user (@all_users) { 324 | print $user->name; # => 'papix', 'hoto', 'akms'がそれぞれ順番に表示される 325 | } 326 | ``` 327 | 328 | ## `update` 329 | 330 | `update`メソッドは, データベースに格納されている任意のデータを更新するメソッドです. 331 | 332 | ```perl:teng.pl(抜粋) 333 | my $count = $teng->update('user' => { name => 'super papix' }, { id => 1 }); 334 | ``` 335 | 336 | 第1引数に変更対象となるテーブル名を, 第2引数にハッシュリファレンス形式で変更するデータを, そして第3引数にハッシュリファレンスで更新したいデータの条件を指定します. 337 | 上記のコードでは, `user`テーブルに格納されたデータについて, `id`が1であれば, `name`カラムの値を`super papix`に変更する, という更新を行います. 338 | 339 | なお, 万が一第3引数を省略した場合, 指定したテーブルに格納されている全てのデータが書き換えられてしまうので, 注意しましょう. 340 | 341 | `update`メソッドは返り値として, 変更したデータの数を返しますので, 更新が無事成功したかを確認することが可能です. 342 | 343 | ```perl:teng.pl(抜粋) 344 | my $count = $teng->update('user' => { name => 'super papix' }, { id => 1 }); 345 | 346 | if ($count) { 347 | print "成功!"; 348 | } else { 349 | print "失敗!"; 350 | } 351 | ``` 352 | 353 | ## `delete` 354 | 355 | `delete`メソッドは, データベースに格納されている任意のデータを削除するメソッドです. 356 | 357 | ```perl:teng.pl(抜粋) 358 | my $count = $teng->delete('user' => { name => 'super papix' }); 359 | ``` 360 | 361 | 第1引数に変更対象となるテーブル名を, 第2引数にハッシュリファレンス形式で削除するデータの条件をハッシュリファレンスで指定します. 362 | この場合, `user`テーブルに格納されたデータについて, `name`カラムが`super papix`であるデータを削除する, という処理になります. 363 | 364 | `delete`メソッドも, `update`メソッドと同じく, 変更した(削除した)データの数を返します. 365 | 366 | ## オブジェクトから利用できるメソッド 〜update/delete〜 367 | 368 | `update`及び`delete`メソッドは, `single`や`search`メソッドなどを利用して取得したオブジェクトからも利用することができます. 369 | 370 | ```perl:teng.pl(抜粋) 371 | my $obj = $teng->single(user => { id => 1 }); 372 | $obj->update({ name => 'papix' }); 373 | ``` 374 | 375 | このようにすれば, `$obj`に格納されたオブジェクトの`name`カラムの値を`papix`に書き換えることができます. 376 | 377 | また, 378 | 379 | ```perl:teng.pl(抜粋) 380 | my $obj = $teng->single(user => { id => 1 }); 381 | $obj->delete(); 382 | ``` 383 | 384 | このようにすれば, `$obj`に格納されたオブジェクトに当てはまるデータを, データベース上から削除することができます. 385 | 386 | ## オブジェクトから利用できるメソッド 〜refetch〜 387 | 388 | `refetch`は, オブジェクトに格納されているデータを再度データベースから引き直す処理です. 389 | 390 | ```perl:teng.pl(抜粋) 391 | my $papix = $teng->single(user => { id => 1 }); 392 | 393 | $teng->update(user => { name => 'super papix' }, { id => 1 }); 394 | 395 | print $papix->name; # => 'papix' -> 同期していないので, 'name'は'papix'のまま 396 | $papix = $papix->refetch; 397 | print $papix->name; # => 'super papix' -> `refetch`で同期したので, 'super papix'になった 398 | ``` 399 | 400 | オブジェクトに格納されているデータと, その元になったデータベース上のデータは同期していません. 401 | そのため, 上記のコードのように, オブジェクトを取得した後に, その元になったデータをデータベース上で書き換えたとしても, その変更は既に取得済みのオブジェクトには反映されません. 402 | 同期したい場合は`refetch`メソッドを利用して明示的に同期するようにしましょう. 403 | 404 | ## トランザクション(`txn_scope` / `commit` / `rollback`) 405 | 406 | データベースを操作する際, 「ある処理とある処理は, 同時に行われて欲しい」という場合があると思います. 407 | 例えば, 何らかのパラメータの交換処理が発生して, 「ユーザAのあるパラメータを100減らしてから, ユーザBのあるパラメータを100増やしたい」といった場合です. 408 | 409 | このとき, もしも「ユーザAのあるパラメータを100減らす」段階でエラーが発生してしまうと, ユーザAのあるパラメータが100減ったにも関わらず, ユーザBのあるパラメータが100増えない, という現象が発生してしまいます. 410 | 411 | これを防ぐためには, トランザクションを利用しましょう. 412 | トランザクションを利用すれば, 必ず同時に変更したい「ある処理A」と「ある処理B」があったとして, 413 | 414 | - 全ての変更が成功 -> 変更を適用(commit) 415 | - どちらかが失敗 / 途中でエラー -> 全ての変更を巻き戻し(変更前の状態に戻す = rollback) 416 | 417 | という振る舞いを実現することができます. 418 | 419 | ### トランザクションの開始 420 | 421 | トランザクションを開始するには, `txn_scope`メソッドを利用します. 422 | 423 | ```perl 424 | my $txn = $teng->txn_scope(); # トランザクションの開始 425 | ``` 426 | 427 | これ以降の処理がトランザクションの対象となります. 428 | 429 | ### 操作の確定 430 | 431 | 処理した内容を確定したい場合は, `$txn`オブジェクトから`commit`メソッドを実行します. 432 | 433 | ```perl 434 | my $txn = $teng->txn_scope(); 435 | 436 | ... データベースの操作 ... 437 | 438 | $txn->commit(); 439 | ``` 440 | 441 | これで, `txn_scope`メソッドを実行してから以降に行われたデータベースの操作が, データベースに適用されます. 442 | 443 | ### 操作の巻き戻し 444 | 445 | もし, 途中でエラーが発生した場合は, `$txn`オブジェクトから`rollback`メソッドを実行することで, データベースの状態をトランザクション開始前の状態に巻き戻す(rollback)ことが可能です. 446 | 447 | ```perl 448 | my $txn = $teng->txn_scope(); 449 | 450 | ... データベースの操作 ... 451 | 452 | $txn->rollback(); 453 | ``` 454 | 455 | ### トランザクションの例 456 | 457 | トランザクションは, 次のように[Try::Tiny](https://metacpan.org/pod/Try::Tiny)モジュールと組み合わせると便利です. 458 | 459 | ```perl 460 | use Try::Tiny; 461 | 462 | my $txn = $teng->txn_scope(); 463 | 464 | my $user1 = $teng->single( user => { id => 1 } ); 465 | my $user2 = $teng->single( user => { id => 2 } ); 466 | 467 | try { 468 | $user1->update({ name => 'super '.$user1->name }); 469 | $user2->update({ name => 'super '.$user2->name }); 470 | 471 | $txn->commit(); 472 | } catch { 473 | $txn->rollback(); # `try { ... }`の中で例外が発生した場合のみ実行される 474 | }; 475 | ``` 476 | 477 | Try::Tinyは, `try`の次にある1つ目のコードリファレンスで例外が発生した場合, `catch`の次にある, 2つ目のコードリファレンスの処理を行う, という機能を提供するモジュールです. 478 | 479 | これによって, `try`の中で例外が発生することなく無事に成功した場合, 最後に`$txn->commit()`を実行して変更を確定することができます. 480 | また, 万が一`try`の中で例外が発生した場合には, `catch`で`$txn->rollback()`を実行して, 変更した内容を変更前の状態に巻き戻しすることが出来ます. 481 | 482 | ### トランザクションとロック 483 | 484 | また, RDBMSとしてMySQLを利用しているのであれば, 次のようにして更新対象のデータをロックしてしまうと良いでしょう. 485 | 486 | ```perl 487 | use Try::Tiny; 488 | 489 | my $txn = $teng->txn_scope(); 490 | 491 | my $user1 = $teng->single( user => { id => 1 } ); 492 | my $user2 = $teng->single( user => { id => 2 } ); 493 | 494 | try { 495 | $user1->refetch({ for_update => 1 }); 496 | $user2->refetch({ for_update => 1 }); 497 | 498 | $user1->update({ name => 'super '.$user1->name }); 499 | $user2->update({ name => 'super '.$user2->name }); 500 | 501 | $txn->commit(); 502 | } catch { 503 | $txn->rollback(); 504 | }; 505 | ``` 506 | 507 | `refetch`の際に, `{ for_update => 1 }`を与えることで, そのオブジェクトの元になったデータベース上のデータに対して, ロック(悲観的ロック)をかけることができます. 508 | これによって, ロックをかけてから解除するまで, `$user1`や`$user2`に該当するデータベース上のデータは, 他の操作によって書き換えられないようにすることができます. 509 | なお, このロックは, `$txn->commit()`するか`$txn->rollback()`することで自動的に解除されます. 510 | 511 | # `inflate`と`deflate` 512 | 513 | Tengには, `inflate`と`deflate`という仕組みがあります. 514 | `inflate`を利用することで, Tengがデータベースからデータを取り出してオブジェクトにする際に任意のカラムの値に対して任意の処理を挟むことが出来ます. 515 | `deflate`はその逆で, Tengからデータベースにデータを書き込む際に任意のカラムの値に対して任意の処理を挟むことが出来ます. 516 | 517 | これを利用することで, データベース上ではDATETIME型などで保存している`created_at`などのカラムに格納されたデータを, Tengでオブジェクトにする段階でTime::Pieceのオブジェクトに変更したり, 逆にこのTime::Pieceのオブジェクトをデータベースに書き込む前に, DATETIME型に適応するフォーマットに変換してからデータベースに書き込んだりといった事が実現出来るようになります. 518 | 519 | 今回は, `user`テーブルの`created_at`と`updated_at`について, 520 | 521 | - `inflate`を利用して, SQLiteに格納されたepoch秒をTengでオブジェクト化する際にTime::Pieceのオブジェクトにする 522 | - `deflate`を利用して, Time::PieceのオブジェクトをSQLiteに書き込む前にepoch秒にする 523 | 524 | という処理を実現してみます. 525 | 526 | ## `inflate`/`deflate`の設定 527 | 528 | `inflate`及び`deflate`の設定は, スキーマファイルで行うことができます. 529 | これらの設定は, 基本的にはテーブル単位で行います(スキーマファイルの, `table`に与えるコードリファレンスの中に記載する). 530 | 531 | まずは`inflate`です. 532 | 533 | ``` 534 | inflate qr/^.+_at$/ => sub { 535 | my ($col_value) = @_; 536 | Time::Piece->strptime($col_value, '%s'); 537 | }; 538 | ``` 539 | 540 | 第1引数にフィルタリングするカラム名を, 第2引数にその処理をコードリファレンスで与えます. 541 | 第1引数は文字列の他, このように正規表現も与えることができます. 542 | この場合は, `created_at`など, 末尾が`_at`で終わる全てのカラムについて処理を行う事になります. 543 | 544 | 第2引数のコードリファレンスには, フィルタリングしたカラムのデータが渡ってきます. 545 | そして, このサブルーチンリファレンスの返り値が, 生成されるオブジェクトに格納されます. 546 | 547 | 今回の場合, `created_at`や`updated_at`はepoch秒が格納されているので, Tine::Pieceの`strptime`を利用して, epoch秒からTime::Pieceのオブジェクトを作成しています. 548 | 549 | `deflate`も同様です. 550 | 551 | ``` 552 | deflate qr/^.+_at$/ => sub { 553 | my ($col_value) = @_; 554 | $col_value->epoch; 555 | }; 556 | ``` 557 | 558 | データベースに書き込むデータのうち, 第1引数で指定した条件を満たすカラムのデータは, 第2引数で与えたコードリファレンスに渡り, その返り値がデータベースに書き込まれます. 559 | 560 | `inflate`が正しく動作していれば, `created_at`や`updated_at`にはTime::Pieceのオブジェクトが格納されているので, この`epoch`メソッドでepoch秒を取り出し, これを返すことによって, データベースにはepoch秒が数値で格納されることになります. 561 | 562 | ```perl:lib/MyApp/DB/Schema.pm 563 | package MyApp::DB::Schema; 564 | use strict; 565 | use warnings; 566 | use utf8; 567 | 568 | use Teng::Schema::Declare; 569 | use Time::Piece; 570 | 571 | base_row_class 'MyApp::DB::Row'; 572 | 573 | table { 574 | name 'user'; 575 | pk 'id'; 576 | columns qw(id name created_at updated_at); 577 | 578 | inflate qr/^.+_at$/ => sub { 579 | my ($col_value) = @_; 580 | Time::Piece->strptime($col_value, '%s'); 581 | }; 582 | deflate qr/^.+_at$/ => sub { 583 | my ($col_value) = @_; 584 | $col_value->epoch; 585 | }; 586 | }; 587 | 588 | 1; 589 | ``` 590 | 591 | それでは, 実際に`inflate`や`deflate`を使ってみましょう. 592 | まず, `inflate`からです. 次のコードを実行してみましょう. 593 | 594 | ```perl:teng.pl(抜粋) 595 | use Data::Dumper; 596 | my $user = $teng->single('user' => { id => 1 }); 597 | print Dumper $user->created_at; 598 | ``` 599 | 600 | 結果は次のようになります. 601 | 602 | ``` 603 | $ perl -Ilib teng.pl 604 | $VAR1 = bless( [ 605 | 28, 606 | 33, 607 | 7, 608 | 6, 609 | 4, 610 | 115, 611 | 3, 612 | 125, 613 | 0, 614 | undef, 615 | 0 616 | ], 'Time::Piece' ); 617 | ``` 618 | 619 | `$user->created_at`で, データベースに格納されているepoch秒ではなく, Time::Pieceのオブジェクトを得ることができました. 620 | 621 | ```perl:teng.pl(抜粋) 622 | use Time::Piece; 623 | 624 | my $user = $teng->single('user' => { id => 1 }); 625 | $user->update({ 626 | name => 'super papix', 627 | updated_at => scalar localtime, 628 | }); 629 | 630 | $user->refetch; 631 | print $user->updated_at->datetime; 632 | ``` 633 | 634 | 次は, あるオブジェクトを更新するときに, `deflate`が有効か確かめてみましょう. 635 | Time::Pieceを`use`すると, スカラコンテキストでの`localtime`の呼び出しで, 現在時刻のTime::Pieceのオブジェクトを取得することができるので, これを`updated_at`の更新内容として渡しています. 636 | 637 | 実行結果は次の通りになります(実際の時間より9時間ずれていますが, これはタイムゾーンの違いによるものです). 638 | 639 | ``` 640 | $ perl -Ilib teng.pl 641 | 2015-05-07T09:25:48 642 | ``` 643 | 644 | 実際に`sqlite3`コマンドでデータベースの中身を確認してみます. 645 | 646 | ``` 647 | SQLite version 3.8.5 2014-08-15 22:37:57 648 | Enter ".help" for usage hints. 649 | sqlite> .headers on 650 | sqlite> select * from user where id = 1; 651 | id|name|created_at|updated_at 652 | 1|super papix|1430897608|1430990748 653 | ``` 654 | 655 | `updated_at`の値が正しく書き換わっていることがわかります. 656 | 657 | # Rowオブジェクトの拡張 658 | 659 | Tengを利用してデータベースからデータを取得する際, TengはTeng::Row(今回の場合は, Teng::Rowを拡張したMyApp::DB::Row)をベースにオブジェクトを生成します. 660 | このMyApp::DB::Rowを利用して, オブジェクトを拡張することが可能です. 661 | 662 | ```perl:lib/MyApp/DB/Row.pm 663 | package MyApp::DB::Row; 664 | use strict; 665 | use warnings; 666 | use utf8; 667 | use parent qw(Teng::Row); 668 | 669 | 1; 670 | ``` 671 | 672 | この名前空間にサブルーチンを書くと, そのサブルーチンがオブジェクトから利用できるようになります. 673 | ここでは例として, `name`カラムのデータのprefixに`super`を付ける, `super_name`というメソッドを利用できるようにしてみましょう. 674 | 675 | ``` 676 | package MyApp::DB::Row; 677 | use strict; 678 | use warnings; 679 | use utf8; 680 | use parent qw(Teng::Row); 681 | 682 | sub super_name { 683 | my ($obj) = @_; 684 | 685 | return 'super ' . $obj->name; 686 | } 687 | 688 | 1; 689 | ``` 690 | 691 | それではスクリプトから利用してみましょう. 692 | 693 | ```perl:teng.pl(抜粋) 694 | my $user = $teng->single('user' => { id => 1 }); 695 | print $user->name."\n"; 696 | print $user->super_name."\n"; 697 | ``` 698 | 699 | 実行結果は次のようになります. 700 | 701 | ``` 702 | $ perl -Ilib teng.pl 703 | papix 704 | super papix 705 | ``` 706 | 707 | ## 特定のテーブルのオブジェクトのみの拡張 708 | 709 | 今回は, `MyApp::DB::Row`にサブルーチンを書いてオブジェクトを拡張しました. 710 | この名前空間で用意したサブルーチンは, `single`や`search`メソッドを利用して取得した全てのオブジェクトで利用することができます. 711 | しかし, 特定のテーブル(に関連するオブジェクト)だけで利用できるメソッドを提供したい, という場面もあるでしょう. 712 | 713 | 例えば, 先程用意した`super_name`メソッドを, `user`テーブルから取得したオブジェクトだけでのみ利用できるようにするには, `MyApp::DB::Row::User`という名前空間を利用し, 次のように書けばOKです. 714 | 715 | ```perl:lib/MyApp/DB/Row/User.pm 716 | package MyApp::DB::Row::User; 717 | use strict; 718 | use warnings; 719 | use utf8; 720 | use parent qw(MyApp::DB::Row); 721 | 722 | sub super_name { 723 | my ($obj) = @_; 724 | 725 | return 'super ' . $obj->name; 726 | } 727 | 728 | 1; 729 | ``` 730 | 731 | `MyApp::DB::Row`以下に, テーブル名をCamelCaseにしたもの(例えば, `team_user`なら`TeamUser`)を設置すれば, Tengはそのテーブルからオブジェクトを生成する際にそのファイルを利用してくれます. 732 | 733 | 実行結果は次のようになります. 734 | 735 | ``` 736 | $ perl -Ilib teng.pl 737 | papix 738 | super papix 739 | ``` 740 | 741 | このように, Tengのオブジェクトは自由に拡張することができます. 742 | かといって自由に拡張しすぎると, 逆に使い勝手が悪くなったり, コードの保守が難しくなってしまう可能性もあるでしょう. 743 | そのため, 実際にオブジェクトを拡張する際は, 用途を絞って(例えば, あるカラムのデータをフィルタリングしたり, データの内容に応じて真偽値を返したり)利用することをおすすめします. 744 | 745 | ## 参考資料 746 | 747 | - [Perl Advent Calendar 2011 - Teng Track](http://perl-users.jp/articles/advent-calendar/2011/teng/) 748 | - Perl Hackers Hub [「第30回 データベースプログラミング入門 - 汎用インタフェースDBIと, O/RマッパTengの使い方(3)」](http://gihyo.jp/dev/serial/01/perl-hackers-hub/003003) 749 | -------------------------------------------------------------------------------- /reactjs/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/reactjs/1.png -------------------------------------------------------------------------------- /reactjs/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/reactjs/2.png -------------------------------------------------------------------------------- /reactjs/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perl-entrance-org/Perl-Entrance-Textbook/a624adf1e91970f74ce94b5207ab02d0b1eb650a/reactjs/3.png -------------------------------------------------------------------------------- /test.md: -------------------------------------------------------------------------------- 1 | # Perlのテスト 2 | 3 | OOP入門で作成した`PerlEntrance::OOPTutorial`というモジュールを題材に, Perlというプログラミング言語におけるテストの実装方法と実施方法について学んでいきます. 4 | 5 | # テストの実行 6 | 7 | まず, テストを実行する方法について学びます. 8 | `minil new`でモジュールのひな形を作成した場合, 最初から`t`ディレクトリに`00_compile.t`というテストが設置されています. 9 | 10 | ```perl:t/00_compile.t 11 | use strict; 12 | use Test::More 0.98; 13 | 14 | use_ok $_ for qw( 15 | PerlEntrance::OOPTutorial 16 | ); 17 | 18 | done_testing; 19 | ``` 20 | 21 | 詳しい内容の解説は後にして, とりあえずこのテストを実行してみます. 22 | このテストもPerlのスクリプトですので, `perl`コマンドで実行することもできますが, テストを実行する場合は`prove`コマンドを利用するのが簡単です. 23 | 24 | ``` 25 | $ ls 26 | Build.PL Changes LICENSE META.json README.md cpanfile eg lib minil.toml t 27 | 28 | $ prove -l t 29 | [17:45:29] t/00_compile.t .. ok 27 ms 30 | [17:45:29] 31 | All tests successful. 32 | Files=1, Tests=1, 0 wallclock secs ( 0.03 usr 0.01 sys + 0.02 cusr 0.01 csys = 0.07 CPU) 33 | Result: PASS 34 | ``` 35 | 36 | `perl`コマンドで実行する場合と比べて, `prove`コマンドでテストを実行すると以下のメリットがあります. 37 | 38 | - `-l`で`-Ilib`と同等のモジュールのディレクトリ指定ができる 39 | - .provercに記述することで自動的にオプションを追加してくれて毎回指定する必要がない 40 | - ディレクトリをテスト対象とすれば, 再帰的にテストを検索し, 実行してくれる 41 | 42 | このため, 基本的には`prove -l t`でテストを実行すれば問題ないようになっています. 43 | 44 | ``` 45 | $ prove -l t/00_compile.t 46 | [17:47:29] t/00_compile.t .. ok 20 ms 47 | [17:47:29] 48 | All tests successful. 49 | Files=1, Tests=1, 0 wallclock secs ( 0.02 usr 0.01 sys + 0.02 cusr 0.00 csys = 0.05 CPU) 50 | ``` 51 | 52 | なお, このように, 任意のテストスクリプトを個別に実行することも可能です. 53 | 54 | # Test::More 55 | 56 | Perlでは, `Test::More`というモジュールを利用してテストを書くことが一般的です. 57 | Test::MoreはPerlのコアモジュール(デフォルトでインストールされており, `cpanm`コマンド等で別途インストールする必要がない)であり, 多くのユーザが利用しているテスト用モジュールです. 58 | `minil new`した際に生成されるテストスクリプトも, この`Test::More`を利用して実装されています. 59 | 60 | Test::Moreの詳細については[POD](https://metacpan.org/pod/Test::More)が参考になりますが, ドキュメントの日本語訳が[perldoc.jp](http://perldoc.jp/)で[提供](http://perldoc.jp/docs/modules/Test-Simple-0.99/lib/Test/More.pod)されているので, こちらを参考にするのも良いでしょう(日本語訳は少し古いバージョンの日本語訳となっているので, 注意しましょう). 61 | 62 | `Test::More`を利用したテストは, 次のように記述します. 63 | 64 | ```perl:t/00_compile.t 65 | use strict; 66 | use Test::More 0.98; 67 | 68 | ... テスト ... 69 | 70 | done_testing; 71 | ``` 72 | 73 | `Test::More`を利用したテストでは, スクリプトの最後に`done_testing;`を記述するようにしましょう. 74 | これは, テストが終了したことを`Test::More`に伝えるコードであり, `Test::More`を利用したテストにおける「お約束」と思っておいて下さい. 75 | 76 | それでは, `Test::More`が提供するテスト用サブルーチンを紹介していきます. 77 | これらのサブルーチンは, `Test::More`を`use`するだけで利用できるようになります. 78 | 79 | ## `use_ok` 80 | 81 | まず, `minil new`でモジュールのひな形を生成したときに生成される, `t/00_compile.t`に最初から記載されている, `use_ok`について解説します. 82 | 83 | ```perl:t/00_compile.t 84 | use strict; 85 | use Test::More 0.98; 86 | 87 | use_ok $_ for qw( 88 | PerlEntrance::OOPTutorial 89 | ); 90 | 91 | done_testing; 92 | ``` 93 | 94 | `use_ok`は, 引数として与えたモジュールが正しく`use`できるかをテストするメソッドです. 95 | ここでは, `PerlEntrance::OOPTutorial`のテストのみを行っているので, `PerlEntrance::OOPTutorial::Engineer`についてもテストするように書き換えてみましょう. 96 | 97 | ```perl:t/00_compile.t 98 | use strict; 99 | use Test::More 0.98; 100 | 101 | use_ok $_ for qw( 102 | PerlEntrance::OOPTutorial 103 | PerlEntrance::OOPTutorial::Engineer 104 | ); 105 | 106 | done_testing; 107 | ``` 108 | 109 | テストを実行してみます. 110 | 111 | ``` 112 | $ prove -l t 113 | [17:53:01] t/00_compile.t .. ok 22 ms 114 | [17:53:01] 115 | All tests successful. 116 | Files=1, Tests=2, 0 wallclock secs ( 0.03 usr 0.01 sys + 0.03 cusr 0.01 csys = 0.08 CPU) 117 | Result: PASS 118 | ``` 119 | 120 | もし, テストに失敗した場合は次のような出力が得られます(`PerlEntrance::OOPTutorial`で, 最後に`1`ではなく`0`を返すように書き換えました). 121 | 122 | ``` 123 | [17:54:04] t/00_compile.t .. 1/? 124 | not ok 1 - use PerlEntrance::OOPTutorial; 125 | [17:54:04] t/00_compile.t .. Dubious, test returned 1 (wstat 256, 0x100) 126 | Failed 1/2 subtests 127 | [17:54:04] 128 | 129 | Test Summary Report 130 | ------------------- 131 | t/00_compile.t (Wstat: 256 Tests: 2 Failed: 1) 132 | Failed test: 1 133 | Non-zero exit status: 1 134 | Files=1, Tests=2, 0 wallclock secs ( 0.03 usr 0.00 sys + 0.02 cusr 0.01 csys = 0.06 CPU) 135 | Result: FAIL 136 | ``` 137 | 138 | ## `is` 139 | 140 | `is`は, 単純に値を比較するテストです. 141 | `is`を利用したテストを, `PerlEntrance::OOPTutorial::Engineer`についてテストを行う, `01_engineer.t`を`t`ディレクトリで試してみます. 142 | 143 | ```perl:t/01_engineer.t 144 | use strict; 145 | use Test::More 0.98; 146 | 147 | use PerlEntrance::OOPTutorial::Engineer; 148 | 149 | my $papix = PerlEntrance::OOPTutorial::Engineer->new( name => 'papix' ); 150 | my $hoto = PerlEntrance::OOPTutorial::Engineer->new( name => 'hoto' ); 151 | 152 | is $papix->name, 'papix'; 153 | is $hoto->name, 'hoto'; 154 | 155 | done_testing; 156 | ``` 157 | 158 | `is`は, 第1引数と第2引数が等しければテストが通り, 異なっていればテストが失敗します. 159 | 基本的に, テストしたい値を第1引数に, 期待値を第2引数に与えることが多いです. 160 | 161 | ## `is_deeply` 162 | 163 | `is_deeply`は, リファレンスを比較する際に利用するサブルーチンです. 164 | 165 | ```perl:test_more.t 166 | use strict; 167 | use warnings; 168 | use Test::More; 169 | 170 | my $hashref = { test => 'more' }; 171 | 172 | is $hashref, { test => 'more' }; 173 | 174 | done_testing; 175 | ``` 176 | 177 | 例えば次のようなテストを書いた場合, テストを実行すると失敗します. 178 | 179 | ``` 180 | $ perl test_more.t 181 | not ok 1 182 | # Failed test at test_more.pl line 7. 183 | # got: 'HASH(0x7fec4c0032b8)' 184 | # expected: 'HASH(0x7fec4d007ff0)' 185 | 1..1 186 | # Looks like you failed 1 test of 1. 187 | ``` 188 | 189 | これは, リファレンスに格納されたデータとしては等しいですが, `$hashref`と`{ test => 'more' }`は異なるリファレンスを指しているためです. 190 | 191 | `is_deeply`は, `is`とは異なり, リファレンスに格納されたデータを全て比較し, 等しいか否かを判定します. そのため, 192 | 193 | ```perl:test_more.t 194 | use strict; 195 | use warnings; 196 | use Test::More; 197 | 198 | my $hashref = { test => 'more' }; 199 | 200 | is_deeply $hashref, { test => 'more' }; 201 | 202 | done_testing; 203 | ``` 204 | 205 | このようなテストにすれば, `$hashref`に格納されたデータと`{ test => 'more' }`が表すデータは等しいので, 206 | 207 | ``` 208 | ok 1 209 | 1..1 210 | ``` 211 | 212 | テストが通ります. 213 | 214 | ## `like` 215 | 216 | `like`は, 第1引数の中に第2引数の正規表現が示す文字列が含まれるかをテストするサブルーチンです. 217 | 218 | ```perl:test_more.t 219 | use strict; 220 | use warnings; 221 | use Test::More; 222 | 223 | like 'Engineer', qr/ee/; 224 | 225 | done_testing; 226 | ``` 227 | 228 | 例えば, このように記述すると, 第1引数の`Engineer`という文字列が, `qr/ee/`という正規表現にマッチするか, つまり`ee`という文字列を含むかどうかをテストすることができます. 229 | 230 | ## `can_ok` 231 | 232 | `can_ok`は, あるオブジェクトが第1引数で指定したメソッドを持っているかをテストするサブルーチンです. 233 | 例えば, `PerlEntrance::OOPTutorial::Engineer`から生成したオブジェクトが, `name`メソッドを実行できることを確かめるには, 234 | 235 | ```perl:t/01_engineer.t 236 | use strict; 237 | use Test::More 0.98; 238 | 239 | use PerlEntrance::OOPTutorial::Engineer; 240 | 241 | my $papix = PerlEntrance::OOPTutorial::Engineer->new( name => 'papix' ); 242 | 243 | can_ok $papix, 'name'; 244 | 245 | done_testing; 246 | ``` 247 | 248 | のようなテストを書くことで確認できます. 249 | 250 | ## `isa_ok` 251 | 252 | `isa_ok`は, あるオブジェクトが第1引数で指定した名前空間に紐付いているかをテストするサブルーチンです. 253 | 例えば, `PerlEntrance::OOPTutorial::Engineer`から生成したオブジェクトは, `PerlEntrance::OOPTutorial::Engineer`という名前空間に紐付いているはずです. 254 | これを確かめるには, 255 | 256 | ```perl:t/01_engineer.t 257 | use strict; 258 | use Test::More 0.98; 259 | 260 | use PerlEntrance::OOPTutorial::Engineer; 261 | 262 | my $papix = PerlEntrance::OOPTutorial::Engineer->new( name => 'papix' ); 263 | 264 | isa_ok $papix, 'PerlEntrance::OOPTutorial::Engineer'; 265 | 266 | done_testing; 267 | ``` 268 | 269 | のようなテストを書くことで確認できます. 270 | 271 | ## `subtest` 272 | 273 | `subtest`を利用することで, テストスクリプト内のテストをまとめることができます. 274 | 275 | ```perl:t/01_engineer.t 276 | use strict; 277 | use Test::More 0.98; 278 | 279 | use PerlEntrance::OOPTutorial::Engineer; 280 | 281 | my $papix = PerlEntrance::OOPTutorial::Engineer->new( name => 'papix' ); 282 | my $hoto = PerlEntrance::OOPTutorial::Engineer->new( name => 'hoto' ); 283 | 284 | can_ok $papix, 'name'; 285 | 286 | isa_ok $papix, 'PerlEntrance::OOPTutorial::Engineer'; 287 | 288 | is $papix->name, 'papix'; 289 | is $hoto->name, 'hoto'; 290 | 291 | done_testing; 292 | ``` 293 | 294 | このテストは, `subtest`を使えば, 次のようにまとめることができます. 295 | 296 | ```perl:t/01_engineer.t 297 | use strict; 298 | use Test::More 0.98; 299 | 300 | use PerlEntrance::OOPTutorial::Engineer; 301 | 302 | my $papix = PerlEntrance::OOPTutorial::Engineer->new( name => 'papix' ); 303 | my $hoto = PerlEntrance::OOPTutorial::Engineer->new( name => 'hoto' ); 304 | 305 | subtest q{PerlEntrance::OOPTutorial::Engineerが正しく実装されていること} => sub { 306 | isa_ok $papix, ''; 307 | can_ok $papix, 'name'; 308 | }; 309 | 310 | subtest q{nameメソッドが正しく実装されていること} => sub { 311 | is $papix->name, 'papix'; 312 | is $hoto->name, 'hoto'; 313 | }; 314 | 315 | done_testing; 316 | ``` 317 | 318 | `prove`コマンドで実行する際, `-v`オプションを指定すると, `subtest`ごとのテスト結果を出力してくれます. 319 | 320 | ``` 321 | $ prove -l -v t/01_engineer.t 322 | [18:21:18] t/01_engineer.t .. 323 | # Subtest: PerlEntrance::OOPTutorial::Engineerが正しく実装されていること 324 | ok 1 - An object of class 'PerlEntrance::OOPTutorial::Engineer' isa 'PerlEntrance::OOPTutorial::Engineer' 325 | ok 2 - PerlEntrance::OOPTutorial::Engineer->can('name') 326 | 1..2 327 | ok 1 - PerlEntrance::OOPTutorial::Engineerが正しく実装されていること 328 | # Subtest: nameメソッドが正しく実装されていること 329 | ok 1 330 | ok 2 331 | 1..2 332 | ok 2 - nameメソッドが正しく実装されていること 333 | 1..2 334 | ok 24 ms 335 | [18:21:18] 336 | All tests successful. 337 | Files=1, Tests=2, 0 wallclock secs ( 0.03 usr 0.01 sys + 0.03 cusr 0.01 csys = 0.08 CPU) 338 | Result: PASS 339 | ``` 340 | 341 | また, `subtest`は, 次のようにネストすることも可能です. 342 | 343 | ```perl:t/01_engineer.t 344 | use strict; 345 | use Test::More 0.98; 346 | 347 | use PerlEntrance::OOPTutorial::Engineer; 348 | 349 | my $papix = PerlEntrance::OOPTutorial::Engineer->new( name => 'papix' ); 350 | my $hoto = PerlEntrance::OOPTutorial::Engineer->new( name => 'hoto' ); 351 | 352 | subtest q{PerlEntrance::OOPTutorial::Engineerが正しく実装されていること} => sub { 353 | subtest q{オブジェクトがPerlEntrance::OOPTutorial::Engineerに紐付いていること} => sub { 354 | isa_ok $papix, 'PerlEntrance::OOPTutorial::Engineer'; 355 | }; 356 | subtest q{オブジェクトがnameメソッドを利用可能であること} => sub { 357 | can_ok $papix, 'name'; 358 | }; 359 | }; 360 | 361 | subtest q{nameメソッドが正しく実装されていること} => sub { 362 | is $papix->name, 'papix'; 363 | is $hoto->name, 'hoto'; 364 | }; 365 | 366 | done_testing; 367 | ``` 368 | 369 | 実行結果は次のようになります. 370 | 371 | ``` 372 | $ prove -l -v t/01_engineer.t 373 | [18:22:28] t/01_engineer.t .. 374 | # Subtest: PerlEntrance::OOPTutorial::Engineerが正しく実装されていること 375 | # Subtest: オブジェクトがPerlEntrance::OOPTutorial::Engineerに紐付いていること 376 | ok 1 - An object of class 'PerlEntrance::OOPTutorial::Engineer' isa 'PerlEntrance::OOPTutorial::Engineer' 377 | 1..1 378 | ok 1 - オブジェクトがPerlEntrance::OOPTutorial::Engineerに紐付いていること 379 | # Subtest: オブジェクトがnameメソッドを利用可能であること 380 | ok 1 - PerlEntrance::OOPTutorial::Engineer->can('name') 381 | 1..1 382 | ok 2 - オブジェクトがnameメソッドを利用可能であること 383 | 1..2 384 | ok 1 - PerlEntrance::OOPTutorial::Engineerが正しく実装されていること 385 | # Subtest: nameメソッドが正しく実装されていること 386 | ok 1 387 | ok 2 388 | 1..2 389 | ok 2 - nameメソッドが正しく実装されていること 390 | 1..2 391 | ok 29 ms 392 | [18:22:28] 393 | All tests successful. 394 | Files=1, Tests=2, 0 wallclock secs ( 0.03 usr 0.01 sys + 0.03 cusr 0.00 csys = 0.07 CPU) 395 | Result: PASS 396 | ``` 397 | 398 | 適切な粒度でテストスクリプトを分割するのも重要ですが, 1つのテストスクリプト内でも, `subtest`を利用してテストをグルーピングし, 何のテストをしているのか分かりやすくするのも重要です. 399 | 400 | # テストを支援するモジュール 401 | 402 | `Test::More`はPerlのテストを実装する為のコアを担うモジュールですが, それ以外にもテストの実装をサポートするモジュールが提供されています. 403 | その中でもよく利用されるものを紹介します. 404 | 405 | ## `Test::MockTime` 406 | 407 | 時間に関連するテストを書く際に便利なモジュールです. 408 | 例えば, Perlでは現在のepoch秒を`time()`で取得することができます. 409 | 例として, 現在のepoch秒から1分後のepoch秒を取得するサブルーチン, `plus1min`と, それに対するテストを書いてみるとします. 410 | 411 | ```perl:test.t 412 | use strict; 413 | use warnings; 414 | use Test::More; 415 | 416 | is plus1min(), time + 60; 417 | 418 | sub plus1min { 419 | time + 60; 420 | } 421 | 422 | done_testing; 423 | ``` 424 | 425 | これを, `Test::MockTime`を利用すると, 次のように書くことができます. 426 | 427 | ```perl:test.t 428 | use strict; 429 | use warnings; 430 | use Test::More; 431 | use Test::MockTime qw/ set_fixed_time /; 432 | 433 | set_fixed_time(0); 434 | 435 | is plus1min(), 60; 436 | 437 | sub plus1min { 438 | time + 60; 439 | } 440 | 441 | done_testing; 442 | ``` 443 | 444 | `Test::MockTime`が提供する`set_fixed_time`を利用すると, このテストスクリプト中でのepoch秒を任意の値に固定(上記のコードの場合, `0`)に固定することができます. 445 | 446 | `set_fixed_time`を利用した場合, それ以降はいくら時間が経過しようと`set_fixed_time`で指定した時間のまま固定されます. 447 | そのため, 次のテストは失敗せず, 正しく終了します. 448 | 449 | ```perl:test.t 450 | use strict; 451 | use warnings; 452 | use Test::More; 453 | use Test::MockTime qw/ set_fixed_time /; 454 | 455 | set_fixed_time(0); 456 | 457 | is plus1min(), 60; 458 | sleep 10; # 10秒経過 459 | is plus1min(), 60; # 10秒経過しても, `set_fixed_time`で現在のepoch秒が0に固定されているので, テストが通る 460 | 461 | sub plus1min { 462 | time + 60; 463 | } 464 | 465 | done_testing; 466 | ``` 467 | 468 | 一方, `Test::MockTime`が提供する`set_absolute_time`は, 現在のepoch秒を`set_absolute_time`の引数に変更します. 469 | それ以降は1秒ごとにepoch秒がインクリメントするので, 470 | 471 | ```perl:test.t 472 | use strict; 473 | use warnings; 474 | use Test::More; 475 | use Test::MockTime qw/ set_absolute_time /; 476 | 477 | set_absolute_time(0); 478 | 479 | is plus1min(), 60; 480 | sleep 10; # 10秒経過 481 | is plus1min(), 60; # 10秒経過したので, timeは0ではなく10を返すので, plus1minは70になる 482 | 483 | sub plus1min { 484 | time + 60; 485 | } 486 | 487 | done_testing; 488 | ``` 489 | 490 | このテストは失敗します. 491 | 492 | ``` 493 | $ perl test.t 494 | ok 1 495 | not ok 2 496 | # Failed test at test.t line 10. 497 | # got: '70' 498 | # expected: '60' 499 | 1..2 500 | # Looks like you failed 1 test of 2. 501 | ``` 502 | 503 | `set_fixed_time`や`set_absolute_time`で変更したepoch秒は, `restore_time`で復元することができます. 504 | 505 | ```perl:test.t 506 | use strict; 507 | use warnings; 508 | use Test::More; 509 | use Test::MockTime qw/ set_fixed_time restore_time /; 510 | 511 | set_fixed_time(0); 512 | 513 | is plus1min(), 60; 514 | restore_time(); 515 | is plus1min(), 60; # restore_time()したので, timeは0ではなく現在のepoch秒を返す 516 | 517 | sub plus1min { 518 | time + 60; 519 | } 520 | 521 | done_testing; 522 | ``` 523 | 524 | 実行結果は次の通りとなります. 525 | 526 | ``` 527 | $ perl test.t 528 | ok 1 529 | not ok 2 530 | # Failed test at test.t line 10. 531 | # got: '1430826672' 532 | # expected: '60' 533 | 1..2 534 | # Looks like you failed 1 test of 2. 535 | ``` 536 | 537 | ## `Test::Mock::Guard` 538 | 539 | 任意の名前空間の任意のサブルーチンを上書きすることができます. 540 | 541 | ```perl:test.t 542 | package Some::Class; 543 | use strict; 544 | use warnings; 545 | 546 | sub hoge { 'foofoo' } 547 | sub fuga { 'barbar' } 548 | 549 | package main; 550 | use strict; 551 | use warnings; 552 | use Test::More; 553 | use Test::Mock::Guard qw/ mock_guard /; 554 | 555 | my $guard = mock_guard( 556 | 'Some::Class' => { 557 | hoge => sub { 'foo' }, 558 | fuga => sub { 'bar' }, 559 | }, 560 | ); 561 | 562 | is Some::Class::hoge(), 'foo'; 563 | is Some::Class::fuga(), 'bar'; 564 | 565 | done_testing; 566 | ``` 567 | 568 | `Some::Class`という名前空間で, `foofoo`を返す`hoge`というサブルーチンと, `barbar`を返す`fuga`というサブルーチンを定義しています. 569 | 一方`main`名前空間では, `Test::Mock::Guard`を利用して, これらのサブルーチンを上書きしています. 570 | 571 | ```perl:test.t(抜粋) 572 | my $guard = mock_guard( 573 | 'Some::Class' => { 574 | hoge => sub { 'foo' }, 575 | fuga => sub { 'bar' }, 576 | }, 577 | ); 578 | ``` 579 | 580 | ここでは, `Some::Class`という名前空間に存在するサブルーチンについて, `hoge`というサブルーチンは`sub { 'foo' }`で, `fuga`というサブルーチンは`sub { 'bar' }`で上書きしています. 581 | 582 | そのため, 583 | 584 | ```perl:test.t(抜粋) 585 | is Some::Class::hoge(), 'foo'; 586 | is Some::Class::fuga(), 'bar'; 587 | ``` 588 | 589 | というテストコードにおいて, `Some::Class::hoge()`と`Some::Class::fuga()`はそれぞれ`foo`と`bar`を返すので, このテストは問題なくパスします. 590 | 591 | ``` 592 | $ perl test.t 593 | ok 1 594 | ok 2 595 | 1..2 596 | ``` 597 | 598 | なお, `Test::Mock::Guard`を利用したサブルーチンの上書きは, `mock_guard`を利用したスコープ内のみとなります. 599 | 600 | ```perl:test.t 601 | package Some::Class; 602 | use strict; 603 | use warnings; 604 | 605 | sub hoge { 'foofoo' } 606 | sub fuga { 'barbar' } 607 | 608 | package main; 609 | use strict; 610 | use warnings; 611 | use Test::More; 612 | use Test::Mock::Guard qw/ mock_guard /; 613 | 614 | subtest 'mock' => sub { 615 | my $guard = mock_guard( 616 | 'Some::Class' => { 617 | hoge => sub { 'foo' }, 618 | fuga => sub { 'bar' }, 619 | }, 620 | ); 621 | is Some::Class::hoge(), 'foo'; # 622 | is Some::Class::fuga(), 'bar'; # mock_guardと同じスコープなので, Test::Mock::Guardで上書きしたサブルーチンが呼び出される 623 | }; 624 | 625 | is Some::Class::hoge(), 'foo'; # 626 | is Some::Class::fuga(), 'bar'; # mock_guardと同じスコープではないので, Test::Mock::Guardで上書きする前のサブルーチンが呼ばれる 627 | 628 | done_testing; 629 | ``` 630 | 631 | そのため, このテストの実行結果は次のようになります. 632 | 633 | ``` 634 | $ perl test.t 635 | # Subtest: mock 636 | ok 1 637 | ok 2 638 | 1..2 639 | ok 1 - mock 640 | not ok 2 641 | # Failed test at test.t line 26. 642 | # got: 'foofoo' 643 | # expected: 'foo' 644 | not ok 3 645 | # Failed test at test.t line 27. 646 | # got: 'barbar' 647 | # expected: 'bar' 648 | 1..3 649 | # Looks like you failed 2 tests of 3. 650 | ``` 651 | 652 | テストスクリプトから外部のリソース(例えばAPIや, MySQLやRedis, Dockerなど様々なミドルウェアなど)をPerlから利用するとき, 毎回直接リソースを操作していると時間がかかりますし, そもそもテストを実行する度に, 外部リソースを適切な状態に初期化する必要があります. 653 | また, 外部のAPIを利用する場合はAPIに対して負荷がかかってしまいます(また, 万が一APIが落ちている場合はテストが出来なくなってしまいます). 654 | 655 | このような場合に, Test::Mock::Guardを利用して, 「本来外部リソースが返すべき値」を返すように外部リソースへアクセスするサブルーチンを上書きして, 実行時間の短縮等を狙うことが可能です. 656 | 657 | なお, APIなどのURLアクセスをモックする場合, [Test::Mock::LWP](https://metacpan.org/pod/Test::Mock::LWP)や[Test::Mock::Furl](https://metacpan.org/pod/Test::Mock::Furl)などを利用することも出来ます. 658 | 659 | ## `Capture::Tiny` 660 | 661 | 任意のコードについて, そのコード内で出力される標準出力や標準エラー出力の内容を取得することができます. 662 | 例えば, 次のコードのサブルーチン`p`は, 標準出力に`hello!`という文字列を出力するサブルーチンです. 663 | 664 | このサブルーチンが, 正しく`hello!`を出力しているかを確かめる為には, `Capture::Tiny`を利用して, 次のように書くことができます. 665 | 666 | ``` 667 | use strict; 668 | use warnings; 669 | use Test::More; 670 | use Capture::Tiny qw/ tee /; 671 | 672 | my ($stdout, $stderr) = tee { 673 | p(); 674 | }; 675 | 676 | is $stdout, 'hello!'; 677 | 678 | sub p { 679 | print "hello!"; 680 | } 681 | 682 | done_testing; 683 | ``` 684 | 685 | ## `Test::Pretty` / `App::Prove::Plugin::Pretty` 686 | 687 | テストの出力を整形してくれるモジュールです. 688 | `prove`コマンドからは, `App::Prove::Plugin::Pretty`を利用すると便利です. 689 | 690 | `PerlEntrance::OOPTutorial::Engineer`のために書いた`t/01_engineer.t`を例にして, `App::Prove::Plugin::Pretty`を利用した時の出力を紹介します. 691 | 692 | ```perl:t/01_engineer.t 693 | use strict; 694 | use utf8; 695 | use Test::More 0.98; 696 | 697 | use PerlEntrance::OOPTutorial::Engineer; 698 | 699 | my $papix = PerlEntrance::OOPTutorial::Engineer->new( name => 'papix' ); 700 | my $hoto = PerlEntrance::OOPTutorial::Engineer->new( name => 'hoto' ); 701 | 702 | subtest q{PerlEntrance::OOPTutorial::Engineerが正しく実装されていること} => sub { 703 | subtest q{オブジェクトがPerlEntrance::OOPTutorial::Engineerに紐付いていること} => sub { 704 | isa_ok $papix, 'PerlEntrance::OOPTutorial::Engineer'; 705 | }; 706 | subtest q{オブジェクトがnameメソッドを利用可能であること} => sub { 707 | can_ok $papix, 'name'; 708 | }; 709 | }; 710 | 711 | subtest q{nameメソッドが正しく実装されていること} => sub { 712 | is $papix->name, 'papix'; 713 | is $hoto->name, 'hoto'; 714 | }; 715 | 716 | done_testing; 717 | ``` 718 | 719 | `prove`コマンドから`App::Prove::Plugin::Pretty`を利用する場合, オプションに`-PPretty`を与えます. 720 | 721 | ``` 722 | $ prove -l -v -PPretty t/01_engineer.t 723 | 724 | PerlEntrance::OOPTutorial::Engineerが正しく実装されていること 725 | オブジェクトがPerlEntrance::OOPTutorial::Engineerに紐付いていること 726 | ✓ An object of class 'PerlEntrance::OOPTutorial::Engineer' isa 'PerlEntrance::OOPTutorial::Engineer' 727 | オブジェクトがnameメソッドを利用可能であること 728 | ✓ PerlEntrance::OOPTutorial::Engineer->can('name') 729 | nameメソッドが正しく実装されていること 730 | ✓ L20: is $papix->name, 'papix'; 731 | ✓ L21: is $hoto->name, 'hoto'; 732 | 733 | ok 734 | ``` 735 | 736 | `-PPretty`を利用しない場合と比べ, 非常に見やすい出力になっています. 737 | なお, 毎回`-PPretty`を指定するのが面倒な場合, `prove`コマンドへのデフォルトのオプションを`~/.proverc`で指定することができるので, 738 | 739 | ```:.proverc 740 | -PPretty 741 | ``` 742 | 743 | のように記述しておけば, 毎回`-PPretty`オプションを付けた上で`prove`コマンドを実行することができます(同様に, `-l`を書いておけば, 毎回`-l`オプションを付けた上で`prove`コマンドを実行することができます). 744 | 745 | なお, `-P`以外に`prove`コマンドで指定できるオプションについては, Perl Advent Calendar 2011の[prove についてのおさらい](http://perl-users.jp/articles/advent-calendar/2011/test/21)という記事が参考になります. 746 | 747 | # 練習問題 748 | 749 | 「OOP入門」でそれぞれ作成した`PerlEntrance::OOPTutorial`モジュールについて, この記事で得た知識を使って, テストを書いてみよう. 750 | -------------------------------------------------------------------------------- /webapp-test.md: -------------------------------------------------------------------------------- 1 | # はじめに 2 | 3 | Webアプリケーションに対するテストを実施する際には, 更に幾つかの知識と工夫が必要となります. 4 | 例えば, RDBMSやKVSを利用したテストを実施したいのであれば, テストを実行する前にこれらを初期化し, 一定の状態にしておかなければなりません. 5 | もし, テストを実行する時点でRDBMSやKVSに一意でないデータが格納されていると, これが原因でテストの結果が変わってしまうかもしれないからです. 6 | Perlでは, このような状況で役に立つWebアプリケーションに特化したテスト支援モジュールがいくつか用意されています. 7 | 8 | この資料では, まず最初にテスト実行時にテスト用のMySQLとRedisを準備してくれるTest::mysqldとTest::RedisServerを紹介します. 9 | また, これら以外のテスト支援モジュールについても, 概要を簡単に説明します. 10 | 最後に, Test::WWW::Mechanize::PSGIを利用したWebアプリケーションのテストと, Test::JsonAPI::Autodocを利用したAPIサーバのテストの実施方法について解説を行います. 11 | 12 | どちらかといえば, この資料は予め読んでおくべき資料というよりは, アプリケーション開発を実施する際に「このテストを楽に行う為には?」と感じたら読むべき資料なのかもしれません. 13 | 14 | なお, この資料ではWAFはAmon2を利用するものとして記述することとします. 15 | 16 | # テスト支援モジュール 17 | 18 | ## [Test::mysqld](https://metacpan.org/pod/Test::mysqld) 19 | 20 | Test::mysqldは, テストスクリプトごとにテスト専用のMySQLサーバ(`mysqld`)を用意してくれるモジュールです. 21 | このモジュールを使うことによって, テスト実行時にデータベース内に格納されているデータが常に同じであることを保証することが可能です. 22 | 23 | Test::mysqldは, newメソッドを利用してインスタンスを用意すると, その段階で新しいMySQLサーバ(`mysqld`)を立ち上げます. 24 | 更に, このインスタンスから`dsn`メソッドを利用することで, このMySQLサーバへ接続するためのDSN(Data Source Name)を取得することが出来ます. 25 | 26 | ```perl 27 | use strict; 28 | use warnings; 29 | 30 | use Test::mysqld; 31 | 32 | my $mysqld = Test::mysqld->new(); # この時点で, 新しいMySQLサーバが立ち上がる 33 | my $dsn = $mysqld->dsn; # 立ち上げた`mysqld`へのDSNが取得出来る 34 | ``` 35 | 36 | 更に, Test::mysqldはスクリプトが終了した段階(正確に言えば, 上記コードにおける`$mysqld`のデストラクタが動作した段階)で, 立ち上げたMySQLサーバが自動的に終了するようになっています. 37 | 38 | これによって, 開発者はテスト用MySQLサーバの起動と停止を意識することなくテストを記述, 実行することができます. 39 | 40 | ### Amon2での利用例 41 | 42 | Amon2では, `t/Util.pm`にテストの為のユーティリティ的なコードを書く事が出来ます. 43 | Test::mysqldと, 次に説明するTest::RedisServerを利用する場合, 必要なコードは`t/Util.pm`に記述することが多いです. 44 | 45 | ```perl:t/Util.pm(抜粋) 46 | use Test::mysqld; 47 | 48 | my $MYSQLD; 49 | unless (defined $ENV{TEST_DSN}) { 50 | $MYSQLD = Test::mysqld->new( 51 | my_cnf => { 52 | "skip-networking" => "", # TCPソケットを利用しない 53 | } 54 | ); 55 | $ENV{TEST_DSN} = $MYSQLD->dsn; 56 | } 57 | ``` 58 | 59 | ここでは, 環境変数`TEST_DSN`に, Test::mysqldが生成したMySQLサーバに接続するためのDSNを格納しています. 60 | 後は, テスト用の設定ファイル`config/test.pl`で次のように設定すれば, アプリケーションはTest::mysqldによって生成したMySQLサーバを利用するようになります(Tengを利用してRDBMSに接続する際, `connect_info`として `$c->config->{DBI}`を与えている場合). 61 | 62 | ```perl:config/test.pl 63 | +{ 64 | 'DBI' => [ $ENV{TEST_DSN}, '', ''], 65 | }; 66 | ``` 67 | 68 | なお, Test::mysqldによって生成したMySQLサーバとその中のテスト用データベースには, `sql/mysql.sql`などに記述しているデータベースのスキーマ等は一切適用されていません. 69 | そのため, 例えば次のようなコードで, 予めスキーマの流し込みを行う必要があるでしょう. 70 | 71 | ```perl:t/Util.pm(抜粋) 72 | use DBI; 73 | use Path::Tiny; 74 | use Test::mysqld; 75 | 76 | unless (defined $ENV{TEST_DSN}) { 77 | $MYSQLD = Test::mysqld->new( 78 | my_cnf => { 79 | "skip-networking" => "" 80 | } 81 | ); 82 | $ENV{TEST_DSN} = $MYSQLD->dsn; 83 | 84 | # スキーマの流し込み 85 | my $dbh = DBI->connect($MYSQLD->dsn); 86 | my $source = path('sql/mysql.sql')->slurp; 87 | for my $stmt (split /;/, $source) { 88 | next unless $stmt =~ /\S/; 89 | $dbh->do($stmt) or die $dbh->errstr; 90 | } 91 | } 92 | ``` 93 | 94 | ## [Test::RedisServer](https://metacpan.org/pod/Test::RedisServer) 95 | 96 | Test::RedisServerは, Test::mysqldのRedis版と言えるモジュールです. 97 | テストスクリプトごとにテスト専用のRedisを利用することが可能です. 98 | 99 | ```perl 100 | use strict; 101 | use warnings; 102 | 103 | use Test::RedisServer; 104 | use Redis; 105 | 106 | my $redis_server = Test::RedisServer->new(); # この時点で新しいRedisサーバが立ち上がる 107 | 108 | my $redis = Redis->new( $redis_server->connect_info ); 109 | ``` 110 | 111 | ### Amon2での利用例 112 | 113 | Test::RedisServerに関する設定も, Amon2であれば`t/Util.pm`に記載することが多いです. 114 | 115 | ```perl:t/Util.pm(抜粋) 116 | use Test::RedisServer; 117 | 118 | my $REDIS; 119 | unless (defined $ENV{TEST_REDIS}) { 120 | $REDIS = Test::RedisServer->new; 121 | $ENV{TEST_REDIS} = $REDIS->connect_info; 122 | } 123 | ``` 124 | 125 | これに対応するように, 設定ファイルでは次のように記述すると良いでしょう. 126 | 127 | ```perl:config/test.pl 128 | +{ 129 | 'DBI' => [ $ENV{TEST_DSN}, '', ''], 130 | redis => $ENV{TEST_REDIS}, 131 | }; 132 | ``` 133 | 134 | これで, Amon2のコンテキストを利用して`$c->config->{redis}`の形で, Test::RedisServerが起動したRedisに接続する為の情報が利用できるようになりますので, 135 | 136 | ``` 137 | my $redis = Redis->new( $c->config->{redis} ); 138 | ``` 139 | 140 | このような形で, Redisを利用できるようになります. 141 | 142 | ## その他 143 | 144 | ### [Harriet](https://metacpan.org/pod/Harriet) 145 | Test::mysqldやTest::RedisServerは, テストスクリプト単位でMySQLサーバやRedisの起動を行います. 146 | ただ, テストスクリプト単位でMySQLサーバやRedisを起動してしまうと, 起動や終了のコストが重く, テストスクリプトの数に比例してテストにかかる時間が長くなってしまいます. 147 | 148 | Harrietを使うことで, テスト実行前にMySQLサーバやRedisなどを起動して, テスト中はそれを使い回せるようになります. 149 | これによって, MySQLサーバやRedisの起動を1回に抑えることが可能です. 150 | 151 | 但し, MySQLサーバやRedisを全てのテストスクリプトで使い回すので, 何もしないとテストを実行する度にデータベース内にデータが蓄積され続けてしまいます. 152 | そのため, `t/Util.pm`などを利用して, テストスクリプトごと等の単位でMySQLサーバやRedisに格納されたデータを削除するような施策が別途必要になります. 153 | 154 | ### [App::Prove::Plugin::MySQLPool](https://metacpan.org/pod/App::Prove::Plugin::MySQLPool) 155 | 156 | Harrietのように, テスト前に自動的にMySQLサーバを立ち上げ, それを全てのテストスクリプトで使い回すことが出来るようになります. 157 | Harrietとの違いは, `prove`コマンドの`-j N`オプション(並列実行, 例えば`-j 4`でテストを4並列実行する)を与えた時に, `N`の数だけMySQLサーバを立ち上げてくれる点です. 158 | 159 | これまでのTest::mysqldやHarrietの場合, 基本的には1つのMySQLサーバしか立ち上げることができないので, 並列実行してしまうと異なるテストスクリプトが1つのMySQLサーバを操作することになり, データベース操作の整合性が取れずにテストが失敗してしまう例が多いです. 160 | 161 | App::Prove::Plugin::MySQLPoolを利用することによって, MySQLサーバを利用したテストについて, `prove`コマンドの`-j`オプションを利用して並列実行し, テスト実行時間を削減することが可能です. 162 | 163 | # WebアプリケーションにおけるE2Eテスト 164 | 165 | 次に, WebアプリケーションにおけるE2Eテスト(リクエストを投げ, それに対応するレスポンスが返ってくるかどうか)で有用なモジュールを紹介します. 166 | 167 | ## [Test::WWW::Mechanize::PSGI](https://metacpan.org/pod/Test::WWW::Mechanize::PSGI) 168 | 169 | Test::WWW::Mechanize::PSGIは, WWW::Mechanizeというブラウザ操作の自動化モジュールを利用したテストモジュールです. 170 | これを利用することで, 「'/'にアクセスしたら, 'Hello!'という文字列が表示される」であったり, 「フォームに'Bob'と入力してフォームを送信すると, 'Hello, Bob!'と表示される」といったテストを実施することが出来ます. 171 | 172 | ```perl 173 | use strict; 174 | use warnings; 175 | use utf8; 176 | use t::Util; 177 | use Plack::Test; 178 | use Plack::Util; 179 | use Test::More; 180 | use Test::Requires 'Test::WWW::Mechanize::PSGI'; 181 | 182 | my $app = Plack::Util::load_psgi 'script/myapp-server'; 183 | 184 | my $mech = Test::WWW::Mechanize::PSGI->new(app => $app); 185 | $mech->get_ok('/'); 186 | 187 | done_testing; 188 | ``` 189 | 190 | このスクリプトは, Test::WWW::Mechanize::PSGIを使った単純なテストの1つです. 191 | `Plack::Util::load_psgi`でWebアプリケーションの起動用スクリプト(今回の場合, `script/myapp-server`)をロードし, Test::WWW::Mechanize::PSGIのコンストラクタに対して渡すことで, Test::WWW::Mechanize::PSGIのインスタンスはこのWebアプリケーションを操作出来るようになります. 192 | 193 | Test::WWW::Mechanize::PSGIのインスタンスから実行している`get_ok`は, 第1引数のパスにGETメソッドで出来ればテスト成功, 出来なければテスト失敗と扱うメソッドです. 194 | これによって, Webアプリケーションの'/'にGETでアクセスできるかどうかというテストを実現することが可能です. 195 | 196 | ### Test::WWW::Mechanize::PSGIで利用できるメソッド 197 | 198 | 詳細は, Test::WWW::Mechanize::PSGIのドキュメントの[HTTP VERBS](https://metacpan.org/pod/Test::WWW::Mechanize::PSGI#METHODS:-HTTP-VERBS)や[CONTENT CHECKING](https://metacpan.org/pod/Test::WWW::Mechanize::PSGI#METHODS:-CONTENT-CHECKING)などを参考にして下さい. 199 | 200 | 例えば, 次のようなテストを用意した場合, `title_is`メソッドでページのタイトルが`MyApp`であるかどうかを, `content_contains`メソッドでページの中に`Amon2`という文字列があるかどうかをテストすることが可能です. 201 | 202 | ```perl 203 | use strict; 204 | use warnings; 205 | use utf8; 206 | use t::Util; 207 | use Plack::Test; 208 | use Plack::Util; 209 | use Test::More; 210 | use Test::Requires 'Test::WWW::Mechanize::PSGI'; 211 | 212 | my $app = Plack::Util::load_psgi 'script/myapp-server'; 213 | 214 | my $mech = Test::WWW::Mechanize::PSGI->new(app => $app); 215 | $mech->get_ok('/'); 216 | $mech->title_is('MyApp'); 217 | $mech->content_contains('Amon2'); 218 | 219 | done_testing; 220 | ``` 221 | 222 | ## [Test::JsonAPI::Autodoc](https://metacpan.org/pod/Test::JsonAPI::Autodoc) 223 | 224 | Test::JsonAPI::Autodocを利用することで, APIのテストからAPIのドキュメントを作成することが可能になります. 225 | 元々は, Ruby製のautodocというライブラリがあり, これをPerlに移植したものがTest::JsonAPI::Autodocです. 226 | 227 | 例えば, GETメソッドで`/success`にアクセスすると, ステータスコードが200で`{ 'status': 200 }`のようなJSONが返ってくるWebアプリケーションがあったとします. 228 | 229 | このテストは, 次のように記述することが可能です. 230 | 231 | ```perl:sample.t 232 | use strict; 233 | use warnings; 234 | use utf8; 235 | use Test::More; 236 | use Test::JSON; 237 | use Test::JsonAPI::Autodoc; 238 | use Plack::Util; 239 | use Plack::Test; 240 | use HTTP::Request::Common; 241 | use JSON; 242 | 243 | my $app = Plack::Util::load_psgi 'script/myapp-server'; 244 | 245 | test_psgi $app, sub { 246 | my $cb = shift; 247 | 248 | describe 'GET /success' => sub { 249 | my $req = GET '/success'; 250 | plack_ok($cb, $req, 200, 'success!'); 251 | }; 252 | 253 | subtest 'レスポンスのJSONが正しいこと' => sub { 254 | my $req = GET '/success'; 255 | my $res = $cb->($req); 256 | 257 | is_json $res->content, encode_json({ 258 | status => 200, 259 | }); 260 | }; 261 | }; 262 | 263 | done_testing; 264 | ``` 265 | 266 | ここでは, Plack::Testを利用してテストを実装しています. 267 | 268 | ```perl:sample.t(抜粋) 269 | my $app = Plack::Util::load_psgi 'script/myapp-server'; 270 | 271 | test_psgi $app, sub { 272 | my $cb = shift; 273 | 274 | ... 275 | 276 | my $res = $cb->($req); # (1) 277 | 278 | ... 279 | }; 280 | ``` 281 | 282 | `Plack::Util::load_psgi`でWebアプリケーションの実行スクリプトを読み込み, `test_psgi`の第1引数に与えます. 283 | これによって, `script/myapp-server`を起動することで動作するWebアプリケーションのテストを実行することが可能です. 284 | 285 | テストコードは, `test_psgi`の第2引数に渡されるサブルーチンリファレンスに渡します. 286 | このサブルーチンリファレンスの第1引数はサブルーチンリファレンス(コード内の`$cb`)になっており, これに対してHTTP::Requestなどで作成したリクエストを送ると, それに対応したレスポンスを得ることが出来ます(コード内の(1)の部分). 287 | なお, このテストスクリプトでは, HTTP::Request::Commonを利用してHTTPのリクエストを生成しています. 288 | 289 | テストコードは2つあります. 290 | 最初の`describe`以下がTest::JsonAPI::Autodocを利用したAPIのテスト, そして次の`subtest`の以下が実際にAPIが返すJSONを確認するコードです. 291 | 292 | ```perl:sample.t(抜粋) 293 | describe 'GET /success' => sub { 294 | my $req = GET '/success'; 295 | plack_ok($cb, $req, 200, 'success!'); 296 | }; 297 | ``` 298 | 299 | ここでは, `plack_ok`を利用して, GETメソッドで`/success`にアクセスした際, ステータスコードが200であることを確認するテストを書いています. 300 | Test::JsonAPI::Autodocは, `describe`以下の`plack_ok`もしくは`http_ok`を利用してAPIのドキュメントを作成します. 301 | 302 | テストを実行する際に, 環境変数として`TEST_JSONAPI_AUTODOC`を`1`にした場合, Test::JsonAPI::Autodocはドキュメントの自動生成を行います. 303 | 304 | ``` 305 | $ TEST_JSONAPI_AUTODOC=1 prove t/sample.t 306 | [10:52:34] t/sample.t .. 307 | GET /health_check 308 | ✓ L20: plack_ok($cb, $req, 200, 'success!'); 309 | レスポンスのJSONが正しいこと 310 | ✓ L27: is_json $res->content, encode_json({ 311 | 312 | ok 313 | ok 3087 ms 314 | [10:52:37] 315 | All tests successful. 316 | Files=1, Tests=1, 3 wallclock secs ( 0.03 usr 0.01 sys + 0.59 cusr 0.13 csys = 0.76 CPU) 317 | Result: PASS 318 | ``` 319 | 320 | ドキュメントは, `docs`ディレクトリ内に生成されます. 321 | 322 | ```markdown:docs/sample.t 323 | generated at: 2015-05-20 10:52:37 324 | 325 | ## GET /success 326 | 327 | success! 328 | 329 | ### Target Server 330 | 331 | http://localhost 332 | 333 | (Plack application) 334 | 335 | ### Parameters 336 | 337 | 338 | ### Request 339 | 340 | GET /success 341 | 342 | ### Response 343 | 344 | - Status: 200 345 | - Content-Type: application/json 346 | 347 | ```json 348 | { 349 | "status" : 200 350 | } 351 | 352 | ``` 353 | 354 | このようなAPIドキュメントを自動的に取得することが出来ます. 355 | 利用者向けに提供するには少し不親切ですが, エンジニア(例えばAPIサーバを利用するフロントエンドのエンジニア)間の情報共有には十分有用に使えると思います. 356 | 357 | # 参考資料 358 | 359 | - [Test::mysqldのcopy_data_fromでテストが更に捗る話](http://www.songmu.jp/riji/archives/2013/06/testmysqldcopy.html) 360 | - [Harriet ー テストのときつかうにデーモンの取扱を簡単にするためのフレームワーク](http://blog.64p.org/entry/2013/05/16/201916) 361 | - [Test::JsonAPI::Autodocをリリースしました](http://moznion.hatenadiary.com/entry/2013/11/02/232144) 362 | --------------------------------------------------------------------------------