@ledsun blog

無味の味は佳境に入らざればすなわち知れず

猫と竜 5

魔法を使う猫の群像劇です。平和な異世界住民の生活を描いた「ハクメイとミコチ」みたいな漫画です。

原作の書影はもっと竜っぽいです。

この表紙をみるとテメレア戦記を思い出します。

新装版が出てるんですね。今気がつきました。

包丁人味平

料理バトル漫画の元祖みたい*1な漫画。

courrier.jp

こんな記事を読んで、次のようなことを考えました。

僕の世代的には料理バトル漫画と言えば「ミスター味っ子」です。 が、93年のテレビ番組を作ったひとが読んでたとしたら、6年前の味っ子じゃなくて、20年前の味平のはずなんですよね。

おなじビッグ錠先生の「一本包丁満太郎」は床屋で読んだことがあります。 味平は未読でした。 良い機会なので読み始めました。

料理の場面がテレビ中継され、審査員がいて、実況中継される。今の料理バトルのフォーマットが描かれています。 また、この頃はまだフォーマットは固まっていなかったようで、1巻ではバトルしないのです。 普通に料理の修業をはじめるエピソードから始まります。 3巻になってようやく料理バトルが始まります。 はじめるといっても前振りで、実際の料理を作り始めるのは4巻でした。

これだけ料理バトルの舞台設定を丁寧に説明しているので、やはり、現代の料理バトルフォーマットを確立したのは味平なのかもしれません。

レコード削除時 ActiveRecord::StaleObjectError は発生する?

楽観的ロックシリーズの三回目です。

レコード削除時は ActiveRecord::StaleObjectError は発生するのか?

します。Ruby on Railsには削除時にActiveRecord::StaleObjectErrorが発生することを確認するテストケースがあります。

https://github.com/rails/rails/blob/c944893a5fba236378202f3580ad9f3a8944599f/activerecord/test/cases/locking_test.rb#L94-111

  def test_lock_destroy
    p1 = Person.find(1)
    p2 = Person.find(1)
    assert_equal 0, p1.lock_version
    assert_equal 0, p2.lock_version

    p1.first_name = "stu"
    p1.save!
    assert_equal 1, p1.lock_version
    assert_equal 0, p2.lock_version

    assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }

    assert p1.destroy
    assert_predicate p1, :frozen?
    assert_predicate p1, :destroyed?
    assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
  end

これをWebアプリケーションにどう組み込めばいいのでしょうか?

魔眼と弾丸を使って異世界をぶち抜く! 2

魔眼ってキーワードいいですよね。 表紙の塗りは今風です。 本編は昔ながらのペン、トーン、網掛けで、ホビージャパンて感じです。 すごく丁寧な展開で2巻で最初の冒険です。 次で無双しそうです。

異世界転生もの導入って、転生して未知の世界を手探りで進む最初の冒険からのチート無双でスカッとカタルシスまでがテンプレで、そっから個性が出てきます。 3巻くらいまで見ないとよくわかりません。 発刊ペース・・・年一かあ・・・。

楽観的ロックをあつかう編集画面

楽観的ロックを使ったRuby on Railsアプリケーションを作って動きを確かめる - @ledsun blog の続きです。

楽観的ロックで競合が起きるとActiveRecord::StaleObjectErrorが起きます。 エラー画面が表示されます。

編集画面に競合エラーを表示

ActiveRecord::StaleObjectErrorをハンドリングして次のように、編集画面にメッセージを表示してみましょう。

f:id:ledsun:20220413000516p:plain
フラッシュメッセージを表示して編集内容を復元します。

ArticlesController#update に例外処理を追加

つぎのように例外処理を追加します。

def update
  respond_to do |format|
    if @article.update(article_params)
      format.html { redirect_to article_url(@article), notice: "Article was successfully updated." }
      format.json { render :show, status: :ok, location: @article }
    else
      format.html { render :edit, status: :unprocessable_entity }
      format.json { render json: @article.errors, status: :unprocessable_entity }
    end
  end
rescue ActiveRecord::StaleObjectError
  @article.reload.attributes = article_params.reject{ _1 == 'lock_version' }

  flash.now[:error] = 'Another user has made a change to that record since you accessed the edit form.'
  render :edit, status: :conflict
end

やっていることは次の通りです。

  1. lock_versionを最新の値に更新
  2. 編集内容を復元
  3. フラッシュメッセージを設定
  4. 編集画面を表示

app/views/articles/edit.html.erb にフラッシュメッセージを追加

Scaffoldで生成した編集画面ではフラッシュメッセージが表示されません。 app/views/articles/edit.html.erbにフラッシュメッセージの表示を追加します。

<p style="color: red"><%= flash[:error] %></p>

<h1>Editing article</h1>

<%= render "form", article: @article %>

<br>

<div>
  <%= link_to "Show this article", @article %> |
  <%= link_to "Back to articles", articles_path %>
</div>

一番上の行です。

なにも考えずにUpdate Articleボタンを押すと更新されます。 別ユーザーが編集した内容を表示してあげると、より便利になりそうです。

参考

WSLにDockerをインストールする

結論から言うと、なんのことはなくUbuntuにインストールするのと同じです。

Dockerをインストール

Install Docker Engine on Debian | Docker Documentation に従って、インストールします。 普段fish-shellを使っています。 読み替えが面倒臭いので、各コマンドはbashで実行しました。

2回目のapt-get updateをすると、次のようなエラーが出ます。

Err:7 https://download.docker.com/linux/debian focal Release
  404  Not Found [IP: 13.249.170.87 443]

Can't install docker on Ubuntu 20.04 - #9 by flostar3000 - Open Source Registry - Docker Community Forums によると

deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian   focal stable

deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu   eoan stable

に変えると良さそうです。 動作確認します。

ledsun@MSI:~►sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:bfea6278a0a267fad2634554f4f0c6f31981eea41c553fdf5a83e95a41d40c38
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

動きました! sudoを要らなくするおまじないをします。

sudo usermod -aG docker ledsun  

シェルを起動し直さないと反映されません。 そのままのターミナルでdocker-composeコマンドを実行して、権限エラーを出して、時間を無駄にしました。

Docker Composeのインストール

Install Docker Compose | Docker Documentation に従って、インストールします。

docker-composeでお目当てのコンテナを起動しました。

ledsun@MSI:~/pubannotation►sudo docker-compose up
Creating network "pubannotation_default" with the default driver
Creating pubannotation_redis_1 ...
Creating pubannotation_db_1    ...
Creating pubannotation_db_1            ... error
WARNING: Host is already in use by another container

ERROR: for pubannotation_db_1  Cannot start service db: driver failed programming external connectivity on endpoint pubannotation_db_1 (1ac1d94f2bb6b6916c07908b0650b89686b7a91354bb2834e9e3155644fddf03): Error starting userland proxy: listen tcp 0.0.0.0:5432: bind: address already in use
Creating pubannotation_elasticsearch_1 ... error

Creating pubannotation_redis_1         ... done
csearch_1 (66ba56873f80c2536192a2b9a22829ec83515c3759b5dca772ff08dee3afa76a): Error starting userland proxy: listen tcp 0.0.0.0:9200: bind: address already in use

ERROR: for db  Cannot start service db: driver failed programming external connectivity on endpoint pubannotation_db_1 (1ac1d94f2bb6b6916c07908b0650b89686b7a91354bb2834e9e3155644fddf03): Error starting userland proxy: listen tcp 0.0.0.0:5432: bind: address already in use

ERROR: for elasticsearch  Cannot start service elasticsearch: driver failed programming external connectivity on endpoint pubannotation_elasticsearch_1 (66ba56873f80c2536192a2b9a22829ec83515c3759b5dca772ff08dee3afa76a): Error starting userland proxy: listen tcp 0.0.0.0:9200: bind: address already in use
ERROR: Encountered errors while bringing up the project.

あれあれ、起動したかったPostgreSQLもElasticSearchもインストール済みでした。 Docker入れなくてもよかったじゃん!

参考

RubyMineからWSL上のRubyを動かす

Ruby on Railsアプリケーションのソースコードを読もうとしました。 ソースコード読むにしてもアプリケーションを動かしたいです。 WSLにソースコードをおき、環境を作ってRuby on Railsアプリケーションを起動しました。 おもむろにVS Codeで読み始めたのですが、なんとなく興が乗りません。 RubyMineで読むことにしました。

WSL上のソースコードをRubyMineで開き、ふと公式の推奨手順を確認してみました。 なんと、面白いことが書いてあります。

WSL | RubyMine

For better performance, we recommend using WSL as a remote interpreter and storing your project in the Windows file system instead of WSL.

ソースコードWindows側において置いて、WSL上のRubyで実行しなさいとあります。 VS Codeとは逆です。 手順に従って設定します。

f:id:ledsun:20220413214019p:plain
RubyMineにWSL上のRubyのパスを設定します。

rbenvを使っているせいか/home/ledsun/.rbenv/shims/rubyでは、RubyMineが上手く認識しませんでした。 Rubyコマンドの実体が置いてある /home/ledsun/.rbenv/versions/2.7.4/bin/ruby を指定しました。

これでRubyMineからRuby on Railsアプリケーションを起動すると動きました。

f:id:ledsun:20220413214321p:plain
RubyMineのターミナルからRuby on Railsアプリケーションが起動しているログを確認できます。

これはこれで動くのですね。 なかなか新鮮です。

楽観的ロックを使ったRuby on Railsアプリケーションを作って動きを確かめる

昔からある機能です。 Rails 0.9.3からの機能あったようです*1。 よくわかっていないので、説明してみます。

Railsガイドの説明

Railsガイドでは次のように説明されています。

Active Record クエリインターフェイス - Railsガイド

楽観的ロックでは、複数のユーザーが同じレコードを同時編集することを許し、データの衝突が最小限であることを仮定しています。この方法では、レコードがオープンされてから変更されたことがあるかどうかをチェックします。そのような変更が行われ、かつ更新が無視された場合、ActiveRecord::StaleObjectError例外が発生します。

いまいちユースケースがピンと来ません。

使い方

Railsガイド中では次のような短いサンプルがあります。

c1 = Customer.find(1)
c2 = Customer.find(1)

c1.first_name = "Sandra"
c1.save

c2.first_name = "Michael"
c2.save # ActiveRecord::StaleObjectErrorが発生

これだとRuby on Railsアプリケーションの中でどう使えば良いのかよくわかりません。 画面からの編集リクエストとc1c2の関係が想像できません。

用途

GitHubWikiが楽観的ロックを使っていそうな動作だったような、おぼろげな記憶があります。 確かめてみると、現在は次のようにフラッシュメッセージがPushされます。

f:id:ledsun:20220412014550p:plain
Gihub Wikiを編集中に保存された時にはフラッシュメッセージが表示されます。

記憶の中のイメージでは、Saveボタンを押したときに保存失敗していました。

想像するに、ユースケースは、ある程度長文を保持するモデルで編集内容を上書きされたくない時のようです。 便利にするなら、マージするなり衝突したDiffを表示するなりしたいところです。 そこまで凝った実装をしないで、最低限、別のユーザーからの変更があったことをユーザーに伝えるための機能です。

楽観的ロックを使ったRuby on Railsアプリケーションを作る

なんとなく用途がイメージできたので、実際にアプリケーションを作ってみます。 長文を保持するので、記事モデルを一つ持つRuby on Railsアプリケーションを作ります。

楽観的ロックはlock_version カラムさえあれば動作します。 scaffold するときに lock_version カラムも指定しておきます。

bundle exec rails new -MCAJT --skip-active-job --skip-active-storage . 
bin/rails g scaffold Article title:string content:text lock_version:integer
bin/rails db:prepare

railsをインストールしていなければbundel initして出来たGemfileの中のコメントを外してbundleしてください。

触ってみる

次のコマンドでRuby on Railsアプリケーションを起動します。

bin/rails s

http://localhost:3000/articles を開くと記事の一覧画面です。

f:id:ledsun:20220412231805p:plain
記事の一覧画面

リンクをたどって記事作成画面を表示します。

f:id:ledsun:20220412231852p:plain
記事作成画面

scaffold するときに lock_version カラムも指定したので、lock_versionが編集出来ます。 適当に記事を作ります。

f:id:ledsun:20220412232034p:plain
記事の作成に成功しました

記事の本文を変更してて保存してみましょう。

f:id:ledsun:20220412232129p:plain
記事の本文を変えました

lock_versionを変えていないのに、勝手にインクリメントされました。

もう一度記事の本文を変更してみましょう。 今回はlock_versionも一緒に変更し、保存します。

f:id:ledsun:20220412232334p:plain
ActiveRecord::StaleObjectErrorが発生します

なんか狐につままれたよう気分になりますが、Ruby on Railsアプリケーションの外側でlock_version が変更されたらActiveRecord::StaleObjectErrorが発生します。 以上が楽観的ロックの動作です。

今日はここまで

楽観的ロックの動作はわかりました。 実際のアプリケーションでつかうにはもう少し工夫が必要そうです。

参考

*1:https://github.com/rails/rails/blob/v0.9.3/activerecord/lib/active_record/locking.rbにlock_versionの記述があります。DHHのコミットで追加されました。

PHPで作っているTODOリストアプリケーションにPRGパターンを適用する

PHPでTODOリストをつくる、追加まで - @ledsun blog で、タスクを追加したときの画面遷移にPRGパターンを使うと良いことに気がつきました。 次のように実装しました。

<!DOCTYPE html>

<head>
  <title>TODOリスト</title>
</head>

<body>
  <form method="post">
    <input type="text" name="name" autofocus>
    <button>追加</button>
  </form>
  <ul>
    <?php
    foreach (restore_todo_list() as $todo) {
      echo "<li>" . $todo . "</li>";
    }
    ?>
  </ul>
</body>

<?php
if ($todo = $_POST["name"]) {
  $todo_list = restore_todo_list();
  array_unshift($todo_list, $todo);
  save_todo_list($todo_list);

  // GETリクエストするようにリダイレクトします。
  header('Location: todo.php');
}

function restore_todo_list()
{
  return array_filter(explode(",", $_COOKIE["todo_list"]));
}

function save_todo_list($todo_list)
{
  $todo_list_str = array_reduce($todo_list, function ($carry, $todo) {
    return $carry . "," . $todo;
  });
  setcookie("todo_list", $todo_list_str);
}
?>

このコードを見ているとToDoリストがデータとその操作を持つオブジェクトに見えてきました。 つまり、TodoListクラスにして、restore/saveメソッドを持たせたいです。 次はPHPでクラスを書く事に挑戦しようと思います。

参考