つよく、やさしく、泥臭く生きていくブログ

日常とポエムと、ときどき技術

Mojolicious::LiteとDBIx::Classを使って簡単なwebアプリケーションを作る

  • 社内の勉強会で、「PerlでDBを扱う」というのを教える必要があったので、その時の資料の抜粋を書き留めておく。

  • 内容としては、

    • DBIx::Classの基本的な使い方をさらった後、
    • Mojolicious::Liteで簡単な書籍管理アプリを作り、DB操作を盛り込むというハンズオン。
  • 後半にある「Webアプリ上に実現してみよう」を順にこなすとwebアプリケーションができあがります。
  • 詳しく動作を理解するとか実践的な内容というよりも、Perlでこういうことがサクッとできますよ、という紹介です。
  • 弊社のPerl 人口は風前の灯である。

  • 想定対象者としては、Perl初心者、プログラミング初心者で、Perlの基本文法は(一応)分かり、(調べながらでも)fizzbuzz問題は書けるくらいの人

  • また、Mojolicious::Liteによるwebアプリを作るので、Perl入学式の第5回資料をさらった人。(弊社勉強会ではこれの前段階でPerl入学式の第5回資料を実施しました。)

github.com

注意

  • 勉強会のための方便的な記述や雑な記述があります
  • 足りないところは喋りで解説

準備

$ 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)の初期準備

  • DBにはいくつかの種類がありますが、今回は手軽さのためSQLiteを使います。
  • 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