社内の勉強会で、「PerlでDBを扱う」というのを教える必要があったので、その時の資料の抜粋を書き留めておく。
内容としては、
- DBIx::Classの基本的な使い方をさらった後、
- Mojolicious::Liteで簡単な書籍管理アプリを作り、DB操作を盛り込むというハンズオン。
- 後半にある「Webアプリ上に実現してみよう」を順にこなすとwebアプリケーションができあがります。
- 詳しく動作を理解するとか実践的な内容というよりも、Perlでこういうことがサクッとできますよ、という紹介です。
弊社のPerl 人口は風前の灯である。
想定対象者としては、Perl初心者、プログラミング初心者で、Perlの基本文法は(一応)分かり、(調べながらでも)fizzbuzz問題は書けるくらいの人
- また、Mojolicious::Liteによるwebアプリを作るので、Perl入学式の第5回資料をさらった人。(弊社勉強会ではこれの前段階でPerl入学式の第5回資料を実施しました。)
注意
- 勉強会のための方便的な記述や雑な記述があります
- 足りないところは喋りで解説
準備
$ cpanm -l ~/extlib local::lib $ perl -I ~/extlib/lib/perl5 -Mlocal::lib=~/extlib | tee -a ~/.bash_profile $ exec $SHELL -l $ cpanm Mojolicious $ cpanm DBIx::Class $ cpanm DBD::SQLite $ cpanm DBIx::Class::Schema::Loader
DB (SQLite)の初期準備
$ sqlite3 sqlite.db # booksという名前のテーブル作成 sqlite> CREATE TABLE books(id INTEGER PRIMARY KEY AUTOINCREMENT, author text, title text, price integer); # 適当にレコードを入れる sqlite> INSERT INTO books (author, title, price) VALUES ('Dazai Osamu', 'Hashire merosu', '1111'); sqlite> INSERT INTO books (author, title, price) VALUES ('Dazai Osamu', 'Ningen shikkaku', '2222'); sqlite> INSERT INTO books (author, title, price) VALUES ('Miyazawa Kenji', 'Ginga tetsudo no yoru', '1234'); # SELCT結果を見やすく整形する sqlite> .header ON sqlite> .mode column # SELECTしてみる sqlite> select * from books; id author title price ---------- ----------- -------------- ---------- 1 Dazai Osamu Hashire merosu 1111 2 Dazai Osamu Ningen shikkak 2222 3 Miyazawa Ke Ginga tetsudo 1234
DB操作
スキーマ情報を取得する
参考:
DB操作といえばSQL文が一般的かと思います。
- Perlでも、SQL文をコードに書いてDB操作を行うことができます。
- ただし今回使うDBIx::Classは、ORM(O/Rマッパー)と呼ばれるもので、SQL文を書かずに使えます。
- DB操作をオブジェクトのように扱えます。
- DBIx::Classは、事前に「データベース」に対応したクラス(スキーマクラス)や、「テーブル」に対応したクラスを用意しておき、プログラムではそいつらを通して DB にアクセスします。
- このスキーマクラスは、既存のDBから自動で作成することができます。
スキーマ作成するためのスクリプト
- これはDBに変更がなければ初回に一回やるだけでよいものです
以下を make_schema.pl として保存する #!/usr/bin/perl use strict; use warnings; use FindBin; use File::Spec; use DBIx::Class::Schema::Loader qw/make_schema_at/; make_schema_at( 'MyDB::Schema', { # 出力先のディレクトリ dump_directory => File::Spec->catfile( $FindBin::Bin ), really_erase_my_files => 1, debug => 1, }, [ 'dbi:SQLite:dbname=sqlite.db', { on_connect_do => ['SET NAMES utf8'], } ] );
$ perl make_schema.pl これで自分のディレクトリにMyDBというディレクトリができてその中にいろいろできていればOK
DBに接続する
- DBに接続してDBの情報を取得してみましょう
- 以下を db.pl として保存してください
#!/usr/bin/perl use strict; use warnings; use MyDB::Schema; # スキーマクラスのインスタンスを作成 my $schema = MyDB::Schema->connect( "DBI:SQLite:dbname=sqlite.db" ); # ResultSet を作成し全レコード取得 my $rs = $schema->resultset('Book')->search; # イテレートして出力 while (my $row = $rs->next) { printf "%s : %s : %s" , $row->id , $row->author, $row->title; print "\n"; }
検索 (search)
- search で、authorを検索
- 先ほどsearchしていた箇所をこのように変えてみましょう
# author がDazai Osamu のものを検索 my $rs = $schema->resultset('Book'); my $result=$rs->search( { author => 'Dazai Osamu', } );
- ライク検索
# author がDazai何とか のものを検索 my $rs = $schema->resultset('Book'); my $result=$rs->search( { author => { like => "%Dazai%" }, } );
- OR検索
# author がMitazawa何とか or タイトルがNingen shikkakuのものを検索 my $rs = $schema->resultset('Book'); my $result=$rs->search( { -or => { author => { like => "Miyazawa%" }, title => "Ningen shikkaku", }, } );
- AND検索
# author がDazai何とかで、タイトルがNingen何とかのものを検索 my $rs = $schema->resultset('Book'); my $result=$rs->search( { -and => { author => { like => "Dazai%" }, title => { like => "Ningen%"}, }, } );
- ANDとORの組み合わせ
# ちょっと複雑な検索条件 # (authorがDazai何とか かつ titleがNingenなんとか ) または authorがMiyazawa Kenji を検索 my $rs = $schema->resultset('Book'); my $result=$rs->search( { -or => { -and => { author => { like => "Dazai%" }, title => { like => "Ningen%"}, }, author => 'Miyazawa Kenji', }, }, );
- ORDER BY (並び替え)
# 全件検索して、値段順に並び変える my $rs = $schema->resultset('Book'); my $result=$rs->search( { # 検索条件は空 なので 全件取得 }, { # オプションを付けられる。order_byは順序 order_by => ['price'], } ); # 出力のところで値段も出すようにしましょう while (my $row = $result->next) { printf "%s : %s : %s : %s" , $row->id , $row->author, $row->title, $row->price; print "\n"; }
行追加、変更
- CREATE (追加)
my $rs = $schema->resultset('Book'); $rs->create( { author => 'Matayoshi', title => 'Hibana', price => 999, } ); my $result = $rs->search(); # イテレートして出力 while (my $row = $result->next) { printf "%s : %s : %s : %s" , $row->id , $row->author, $row->title, $row->price; print "\n"; }
- UPDATE
my $rs = $schema->resultset('Book'); # 変更対象のレコードを取得して my $id4 = $rs->search( { id => 4 } ); # そいつをアップデート $id4->update( { title => 'Kinikuman', author => 'Yudetamago' } ); my $result = $rs->search(); # イテレートして出力 while (my $row = $result->next) { printf "%s : %s : %s : %s" , $row->id , $row->author, $row->title, $row->price; print "\n"; }
- メソッドチェーンのようにつなげられる
my $rs = $schema->resultset('Book'); $rs->search( { id => 4 } )->update( { title => 'Kinikuman', author => 'Yudetamago' } ); my $result = $rs->search();
- 複数レコードをまとめて処理することも可能
# Dazai Osamu のレコード全てpriceを変更 $rs->search( { author => 'Dazai Osamu', # Dazaiのレコードは2つある } )->update( { price => 99999 } );
- イテレートする中でupdateしたりもできる
my $rs = $schema->resultset('Book'); my $result = $rs->search(); # イテレートして、それぞれupdateして、出力 while (my $row = $result->next) { my $price = $row->price; $price = $price * 3; # 現在の値段の3倍をとって # その行を更新する $row->update( { price => $price, } ); printf "%s : %s : %s : %s" , $row->id , $row->author, $row->title, $row->price; print "\n"; }
行削除
- DELETE
my $rs = $schema->resultset('Book'); $rs->search( { id => 5 } )->delete();
Webアプリ上に実現してみよう
- DB操作を盛り込んだWebアプリを作ってみましょう。
- 簡単な書籍管理アプリです。
- 一覧の表示
- 検索した結果の表示
- 書籍情報の追加
- 変更、削除
- Mojolicious::Liteを使って作ります。
Mojolicious::Liteの準備
- ひな形作成
$ mojo generate lite_app books_list.pl
一覧表示
- /にアクセスされたときに、booksテーブルの中身を全部表示します。
- use MyDB::Schema;を追加
- get / の中身を変更
- テンプレートの変更
- index.html.epの変更
- default.html.epで、bootstrapを使うように追記
GET /
#!/usr/bin/env perl use Mojolicious::Lite; use MyDB::Schema; get '/' => sub { my $c = shift; my $schema = MyDB::Schema->connect( "DBI:SQLite:dbname=sqlite.db", ); my $rs = $schema->resultset('Book'); my $result = $rs->search(); $c->stash(result => $result); $c->render(template => 'index'); }; app->start;
- まずは、コントローラの中身を変更します。
- テーブルの全レコードを取得して、stashを使い、テンプレートへ渡します。
@@ index.html.ep % layout 'default'; % title 'Books List'; <div class="container"> <div class="row border-bottom border-dark bg-light"> <div class="col-sm"> id </div> <div class="col-sm"> author </div> <div class="col-sm"> title </div> <div class="col-sm"> price </div> </div> % while (my $row = $result->next) { <div class="row border-bottom"> <div class="col-sm"> <%= $row->id %> </div> <div class="col-sm"> <%= $row->author %> </div> <div class="col-sm"> <%= $row->title %> </div> <div class="col-sm"> <%= $row->price %> </div> </div> % } </div> @@ layouts/default.html.ep <!DOCTYPE html> <html> <head> <title><%= title %></title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> </head> <body><%= content %></body> </html>
- index.html.epと,default.html.epをそれぞれ変更します。
- indexでは、コントローラから渡された$resultを使いながらhtmlを記述しています。
- defaultでは、CSSフレームワークのbootstrapを読み込むようにしています。
- これで見た目をサクッときれいにしちゃいます。
- ここまで出来たら、morboを起動して表示を確認しましょう。
$ morbo books_list.pl
検索
- フォームを作り、入力された値を使って検索した結果をテンプレートに渡します
post /search
post '/' => sub { my $c = shift; # POSTされた値を取る my $author = $c->param('author'); my $title = $c->param('title'); # POSTされた値をもとに、検索条件を作る my $query = {}; if ( $author ) { $query->{author} = { like => "%$author%"}; } if ( $title ) { $query->{title} = { like => "%$title%"}; } my $schema = MyDB::Schema->connect( "DBI:SQLite:dbname=sqlite.db", ); # 検索する my $rs = $schema->resultset('Book'); my $result = $rs->search( { -and => $query, } ); $c->stash(result => $result); $c->render(template => 'index'); };
- post '/' を新たに追記します。
- schemaをとるところがgetで書いたものと冗長ですね。後で直します。
%= form_for '/' => method => 'POST' => begin <div class="col-md-3"> 著者名 <%= text_field 'author' , class => 'form-control' %> </div> <div class="col-md-3"> 作品名 <%= text_field 'title' , class => 'form-control' %> </div> %= submit_button '検索' , class => 'btn btn-primary mt-3' % end
- 先ほどのindex.html.epに、フォーム部分を追記します。
- form_forといった機能を使っていますが、普通に
- ここまで書けたら、morboを起動し、検索を実行してみましょう。
サブルーチンへの切り出し
#!/usr/bin/env perl use Mojolicious::Lite; use MyDB::Schema; sub get_schema { return MyDB::Schema->connect( "DBI:SQLite:dbname=sqlite.db", ); } sub get_book_rs { return get_schema()->resultset('Book'); } get '/' => sub { my $c = shift; my $rs = get_book_rs(); my $result = $rs->search(); $c->stash(result => $result); $c->render(template => 'index'); }; post '/' => sub { my $c = shift; my $author = $c->param('author'); my $title = $c->param('title'); my $query = {}; if ( $author ) { $query->{author} = { like => "%$author%"}; } if ( $title ) { $query->{title} = { like => "%$title%"}; } my $rs = get_book_rs(); my $result = $rs->search( { -and => $query, } ); $c->stash(result => $result); $c->render(template => 'index'); }; app->start;
- get と postでどちらも schemaを取るところは同じなので、サブルーチンにまとめます。
書籍追加機能
- 追加するためのページを一枚用意します
GET /add
get '/add' => sub { my $c = shift; $c->render(template => 'add'); };
中略 %= submit_button '検索' , class => 'btn btn-primary mt-3' % end <a class="btn btn-warning" href="<%= url_for('/add') %>">新規追加</a> @@ add.html.ep % layout 'default'; % title 'Books List add'; %= form_for '/add' => method => 'POST' => begin <div class="col-md-3"> 著者名 <%= text_field 'author' , class => 'form-control' %> </div> <div class="col-md-3"> 作品名 <%= text_field 'title' , class => 'form-control' %> </div> <div class="col-md-3"> 価格 <%= text_field 'price' , class => 'form-control' %> </div> %= submit_button '追加する' , class => 'btn btn-primary mt-3' % end
- 検索フォームのすぐ下に、新規追加画面へのリンクを置きます。
- add.html.epを追記します。
- フォームで追加ボタンを押すと、/add へPOSTします
POST /add
get '/add' => sub { my $c = shift; $c->render(template => 'add'); }; post '/add' => sub { my $c = shift; my $author = $c->param('author'); my $title = $c->param('title'); my $price = $c->param('price'); # 本来ならこの辺で入力値のチェックをするべき my $query = { author => $author, title => $title, price => $price, }; my $rs = book_rs(); $rs->create( $query ); $c->redirect_to('/'); };
- 新規追加でPOSTされてきたときに、DBへ追加します。
- 追加処理だけしたあとは、index(/)へリダイレクトします、
- 本来は、入力された値は妥当かどうかをチェックし、妥当でなければその旨を表示させるのが一般的です。
日本語対応
- SQLiteに日本語が入っていて文字化けする場合、このようにする
use Encode qw( decode ); # テンプレート内で使えるhelperという関数を定義する helper decode_utf8 => sub { my ($self,$string) = @_; my $string_utf8 = decode ('utf-8', $string); return $string_utf8; }; 中略 テンプレート内で、DBの内容で日本語がありうるところにdecode_utf8を入れる <div class="col-sm"> <%= decode_utf8( $row->author ) %> </div> <div class="col-sm"> <%= decode_utf8( $row->title ) %> </div>
変更
GET /edit/:id
get '/edit/:id' => sub { my $c = shift; my $id = $c->param('id'); my $rs = get_book_rs(); my $result = $rs->search( { id => $id } )->first; $c->stash(row => $result); $c->render(template => 'edit'); };
- get '/edit/:id' を追加します。
- /edit/:id の「:id」はプレースホルダと呼ばれるもので、URLの一部を取得することができます。
- 取得した部分は、 my $id = $c->param('id'); で取り出します。
<div class="container"> <div class="row border-bottom border-dark bg-light"> <div class="col-sm"> </div> <div class="col-sm"> id </div> <div class="col-sm"> author </div> <div class="col-sm"> title </div> <div class="col-sm"> price </div> </div> % while ( my $row = $result->next ) { <div class="row border-bottom"> <a class="btn btn-light" href="<%= url_for('/edit/' . $row->id) %>">変更</a> <div class="col-sm"> <%= $row->id %> </div> <div class="col-sm"> <%= decode_utf8( $row->author ) %> </div> <div class="col-sm"> <%= decode_utf8( $row->title ) %> </div> <div class="col-sm"> <%= $row->price %> </div> </div> % } </div>
@@ edit.html.ep % layout 'default'; % title 'Books List edit'; %= form_for '/edit/'. $row->id => method => 'POST' => begin <div class="col-md-3"> 著者名 <%= text_field 'author' => decode_utf8($row->author) , class => 'form-control' %> </div> <div class="col-md-3"> 作品名 <%= text_field 'title' => decode_utf8($row->title) , class => 'form-control' %> </div> <div class="col-md-3"> 価格 <%= text_field 'price' => $row->price , class => 'form-control' %> </div> %= submit_button '変更する' , class => 'btn btn-primary mt-3' % end
- index.html.ep に変更ボタンを付けます。
- 表示されるボタンのURLに注目してください。
- また、edit.html.epテンプレートを作成します。
POST /edit/:id
post '/edit/:id' => sub { my $c = shift; my $id = $c->param('id'); my $author = $c->param('author'); my $title = $c->param('title'); my $price = $c->param('price'); # 本来ならこの辺で入力値のチェックをするべき my $query = { author => $author, title => $title, price => $price, }; # 指定したid のレコードを更新 my $rs = get_book_rs(); my $result = $rs->search( { id => $id } )->update( $query ); $c->redirect_to('/'); };
- post '/edit/:id' を追加します。
- 受け取った値で、レコードを更新します
- 更新処理した後は、indexページへリダイレクトします。
削除
POST /delete/:id
post '/delete/:id' => sub { my $c = shift; my $id = $c->param('id'); my $rs = get_book_rs(); my $result = $rs->search( { id => $id } )->delete; $c->redirect_to('/'); };
- post '/delete/:id' を追加する
- delete処理をした後はindexページへリダイレクトします
@@ edit.html.ep % layout 'default'; % title 'Books List edit'; %= form_for '/edit/'. $row->id => method => 'POST' => begin <div class="col-md-3"> 著者名 <%= text_field 'author' => decode_utf8($row->author) , class => 'form-control' %> </div> <div class="col-md-3"> 作品名 <%= text_field 'title' => decode_utf8($row->title) , class => 'form-control' %> </div> <div class="col-md-3"> 価格 <%= text_field 'price' => $row->price , class => 'form-control' %> </div> %= submit_button '変更する' , class => 'btn btn-primary mt-3' % end %= form_for '/delete/'. $row->id => method => 'POST' => begin %= submit_button '削除する' , class => 'btn btn-danger mt-3' % end
- editテンプレート内に、削除ボタンを追記します。
参考
- 今回作成の最終的なアプリ
books_list.pl
#!/usr/bin/env perl use Mojolicious::Lite; use MyDB::Schema; use Encode qw ( encode decode ); sub get_schema { return MyDB::Schema->connect( "DBI:SQLite:dbname=sqlite.db", ); } sub get_book_rs { return get_schema()->resultset('Book'); } # テンプレート内で簡単に使える関数を定義できる helper decode_utf8 => sub { my ($self,$string) = @_; my $string_utf8 = decode ('utf-8', $string); return $string_utf8; }; # /にGETアクセスされたときの処理 get '/' => sub { my $c = shift; my $rs = get_book_rs(); my $result = $rs->search(); $c->stash(result => $result); $c->render(template => 'index'); }; # /にPOSTアクセスされたときの処理 post '/' => sub { my $c = shift; my $author = $c->param('author'); my $title = $c->param('title'); my $query = {}; if ( $author ) { $query->{author} = { like => "%$author%"}; } if ( $title ) { $query->{title} = { like => "%$title%"}; } my $rs = get_book_rs(); my $result = $rs->search( { -and => $query, } ); # 上記は変数展開されるとこうなる # { # -and => { # author => { like => "%$author%"}, # ここと # title => { like => "%$title%"} # ここが $queryの中身 # } # } $c->stash(result => $result); $c->render(template => 'index'); }; get '/add' => sub { my $c = shift; $c->render(template => 'add'); }; post '/add' => sub { my $c = shift; my $author = $c->param('author'); my $title = $c->param('title'); my $price = $c->param('price'); # 本来ならこの辺で入力値のチェックをするべき my $query = { author => $author, title => $title, price => $price, }; my $rs = get_book_rs(); $rs->create( $query ); # DBに入れる処理だけして # TOPページに飛ばす $c->redirect_to('/'); }; # /edit/:hoge/ という「:hoge」のような書き方をすると、URLの一部を取得できる get '/edit/:id' => sub { my $c = shift; my $id = $c->param('id'); # http://x.x.x.x:3000/edit/12345 だと、12345が取れる my $rs = get_book_rs(); my $result = $rs->search( { id => $id } )->first; $c->stash(row => $result); $c->render(template => 'edit'); }; post '/edit/:id' => sub { my $c = shift; my $id = $c->param('id'); my $author = $c->param('author'); my $title = $c->param('title'); my $price = $c->param('price'); # 本来ならこの辺で入力値のチェックをするべき my $query = { author => $author, title => $title, price => $price, }; # 指定したid のレコードを更新 my $rs = get_book_rs(); my $result = $rs->search( { id => $id } )->update( $query ); $c->redirect_to('/'); }; post '/delete/:id' => sub { my $c = shift; my $id = $c->param('id'); my $rs = get_book_rs(); my $result = $rs->search( { id => $id } )->delete; $c->redirect_to('/'); }; app->start; __DATA__ @@ index.html.ep % layout 'default'; % title 'Books List'; %= form_for '/' => method => 'POST' => begin <div class="col-md-3"> 著者名 <%= text_field 'author' , class => 'form-control' %> </div> <div class="col-md-3"> 作品名 <%= text_field 'title' , class => 'form-control' %> </div> %= submit_button '検索' , class => 'btn btn-primary mt-3' % end <a class="btn btn-warning" href="<%= url_for('/add') %>">新規追加</a> <div class="container"> <div class="row border-bottom border-dark bg-light"> <div class="col-sm"> </div> <div class="col-sm"> id </div> <div class="col-sm"> author </div> <div class="col-sm"> title </div> <div class="col-sm"> price </div> </div> % while ( my $row = $result->next ) { <div class="row border-bottom"> <a class="btn btn-light" href="<%= url_for('/edit/' . $row->id) %>">変更</a> <div class="col-sm"> <%= $row->id %> </div> <div class="col-sm"> <%= decode_utf8( $row->author ) %> </div> <div class="col-sm"> <%= decode_utf8( $row->title ) %> </div> <div class="col-sm"> <%= $row->price %> </div> </div> % } </div> @@ add.html.ep % layout 'default'; % title 'Books List add'; %= form_for '/add' => method => 'POST' => begin <div class="col-md-3"> 著者名 <%= text_field 'author' , class => 'form-control' %> </div> <div class="col-md-3"> 作品名 <%= text_field 'title' , class => 'form-control' %> </div> <div class="col-md-3"> 価格 <%= text_field 'price' , class => 'form-control' %> </div> %= submit_button '追加する' , class => 'btn btn-primary mt-3' % end @@ edit.html.ep % layout 'default'; % title 'Books List edit'; %= form_for '/edit/'. $row->id => method => 'POST' => begin <div class="col-md-3"> 著者名 <%= text_field 'author' => decode_utf8($row->author) , class => 'form-control' %> </div> <div class="col-md-3"> 作品名 <%= text_field 'title' => decode_utf8($row->title) , class => 'form-control' %> </div> <div class="col-md-3"> 価格 <%= text_field 'price' => $row->price , class => 'form-control' %> </div> %= submit_button '変更する' , class => 'btn btn-primary mt-3' % end %= form_for '/delete/'. $row->id => method => 'POST' => begin %= submit_button '削除する' , class => 'btn btn-danger mt-3' % end @@ layouts/default.html.ep <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title><%= title %></title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> </head> <body><%= content %></body> </html>
参考: http://nekokak.jf.land.to/wiki/wiki.cgi/sub?page=Perl%2FDBIC