SAVEPOINTの使いどころがよくわからない

SQLにはSAVEPOINTというステートメントがある。

SAVEPOINTは、サブトランザクション入れ子トランザクションとも呼ばれる)を実現するための、データベース言語SQLステートメントの1つである。

via. SAVEPOINT (SQL) - Wikipedia)

標準SQLにも入っているので、おそらく広く使われている機能だと思うのだが、いまいち使いどころがわからなかった。

機能としてはWikipediaの例をみれば十分理解できるものの、これまでの経験*1 サブトランザクションが生じざるを得ない時点でなにかデータ構造の設計を誤っている気がする。

ひとまず、わからないことがあった、ということの記録として。

参考

*1:まあこれが大したことがないからだ、と言われるとそれまでかもなのだが

WSL2のDockerをログイン時に自動起動する

小ネタ。WSL2ではsystemdがPID1で起動しないので、ログイン時に自動起動するように設定します。

ログインユーザーがdockerグループに所属していることを確認しておく

$ getent group docker
docker:x:999:mk55

ログインユーザーでdockerを起動するために、sudo visudoを実行してsudoers に以下を追記する。

# Allow members of docker group to start docker service
%docker ALL=(ALL)  NOPASSWD: /usr/sbin/service docker start

ログインしたときにdockerが起動していなければ起動するように、~/.profile に以下を追記する

# if not runnig docker daemon, start docker daemon.
service docker status > /dev/null || sudo service docker start > /dev/null

VSCodeのRemote-Containerを使って、開発環境を準備する

最近全然、家での技術もブログもできてないのだが、リハビリがてら自分用のメモ

拡張機能のインストール

VSCode上で、Remote DevelopmentのExtension Packをインストールする。

SSH/WSL/Containers等の関連する拡張機能が一括でインストールされる

WSL2の設定

拡張機能を利用できるようにするためにWSL2側にも設定が必要。でやった手順は割愛しているので一からやるときはこちらも対応しておくこと。Windows 10 に開発環境を作る(WSL2 Docker編) - mk_55's diaryで対応したことは割愛してるので、一から環境を作る場合はこちらも実施すること。

# 作業前の更新
sudo apt update
sudo apt upgrade

# docker-composeのインストール(入れないとRemote-Containerで環境を初期化できない)
sudo apt install docker-compose -y

# dockerデーモンの起動
sudo service docker start

# ユーザーがdockerを操作できるようにグループに追加
sudo gpasswd -a ユーザー名 docker

環境作成

  • WSLに接続した状態で、開発に利用したいディレクトリに移動する
  • code .を実行してカレントディレクトリをVSCodeで開く
  • VSCodeの左下をクリックして、Reopen in Containerを選択して、コンテナ環境で現在のディレクトリを開きなおす
  • ダイアログに従って利用したいコンテナの設定を選んでいく

上記の手順を実行すると、ディレクトリの中に以下のファイルが作成される

.devcontainer
├── devcontainer.json #devcontainerの設定ファイル
└── Dockerfile        #自動で作成されるDockerファイル

残件

Dockerの起動を自動化できてない。毎度、serviceで起動するのは面倒なのだが... systemctlがない環境で常時起動ってどうやればいいのかは今度調べて対応する。

Tavernを使ってREST APIのテストをCLIから実行してみる

  • API単位のテストを自動化したい
    • 色々なツールがある
    • ぱっと見の印象は以下の通り
      • karateとかREST-Assured: 高機能だけど、Javaの知識が必要なことがネック。私一人ならともかく、GradleやMavenの仕組みからほかの人に教えて立ち上げるのはつらい...。DSLは羨ましい。
      • postman + newman: Collectionsのフォーマットがイマイチなのと、基本GUIなのがいまいち。企業もバックにいて一番開発が盛んそうではある。
      • tavern: よさそう。エコシステムが弱そうなのはネックかもしれない。
    • ひとまず、シンプルに導入できそうなtavernを試してみよう
  • 以下は個人的な作業メモです。

インストール

pip install tavern

試しに使ってみる

Postman Echoに対してリクエストを送ってみます。

以下のyamltest_postman_echo.tavern.yamlとして保存する。

test_name: Testing Postman Echo

stages:
  - name: Get Request
    request:
      url: https://postman-echo.com/get
      params:
        foo1: "bar1"
        foo2: "bar2"      
      headers:
        Authorization: "hoge"
      method: GET
    response:
      strict: # response のheadersではリクエストで明示的に設定したHeader以外があってもOKにするため
        - json:off
      status_code: 200
      json:
        args:  
          foo1: "bar1"
          foo2: "bar2"
        headers: 
          authorization: "hoge"
        url: "https://postman-echo.com/get?foo1=bar1&foo2=bar2"

そして、テストを実行してみると、出力は以下のような形で出力される。

tavern-ci.exe ./test_postman_echo.tavern.yaml

========================================================================================================================================== test session starts ===========================================================================================================================================
platform win32 -- Python 3.10.1, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: <path/to/rootdir>
plugins: tavern-1.19.0
collected 1 item

test_postman_echo.tavern.yaml .                                                                                                                                                                                                                                                                     [100%]

============================================================================================================================================ warnings summary ============================================================================================================================================ 
test_postman_echo.tavern.yaml::Testing Postman Echo
  <frozen importlib._bootstrap>:283: DeprecationWarning: the load_module() method is deprecated and slated for removal in Python 3.12; use exec_module() instead

-- Docs: https://docs.pytest.org/en/stable/warnings.html
====================================================================================================================================== 1 passed, 1 warning in 1.17s ====================================================================================================================================== 

使ってみて

  • エラーの表示に癖があって、ちょっと読み解くのに慣れが必要そう
  • 外部のyammlファイルを読み込むとエラーになる事象の解消。
  • 外部関数の呼び出しを試す。

PostgreSQLのトリガーをデバックする方法

ちょっとトリガーを触る機会があって、デバックする良い方法がないかな、と探したのでメモ。 基本的には、debugging postgresql trigger - Stack Overflowに書いてあることの一部試してみた、という記事。

EXPLAIN ANALYZEでTriggerが実行されたか確認する

実行計画を表示するために使うEXPLAINだが、EXPLAIN ANALYZEすると実際にSQL文を実行してくれる。

この時、そのSQLでトリガーが呼び出された場合は、その実行結果(実行時間、回数)も一緒に表示してくれるので、意図した呼び出しが行われているか確認できる。

PL/pgSQLRAISE文でプリントデバックする

トリガーで呼び出す関数を作成するPL/pgSQLにはRAISE文という、エラーをメッセージとして出力したり、そもそもエラーとして処理を停止させるための仕組みがある。

これを利用すると、トリガー内の任意の箇所で、メッセージを出力できるので、プリントデバックが可能。*1

42.9. エラーとメッセージ

試してみる

前提の環境

dockerで適当なコンテナを立ち上げて、接続する。

docker --version
# Docker version 20.10.8, build 3967b7d

sudo docker run --name example-postgres -e POSTGRES_PASSWORD=hoge -d 
# postgres

docker exec -it example-postgres bash
psql --version
# psql (PostgreSQL) 14.0 (Debian 14.0-1.pgdg110+1)

psql -U postgres

DDL

適当にusersテーブルとusers_historyテーブルを作って、usersの行が更新されたら、変更前後のusers.nameusers_historyに書き込むようなトリガーを作る。 write_user_history()の中に前述のRAISE文も書いておく。

CREATE TABLE users(
   id serial NOT NULL,
   name text,
   PRIMARY KEY (id)
);

CREATE TABLE users_history(
   id serial NOT NULL,
   user_id integer,
   old_name text,
   new_name text,
   PRIMARY KEY (id)
);


CREATE OR REPLACE FUNCTION write_user_history() RETURNS "trigger" AS
$$
DECLARE
BEGIN
  RAISE NOTICE 'messagea';
  insert into  users_history(
    user_id,
    new_name,
    old_name
  ) values (
    new.id,
    new.name,
    old.name
  );
  RETURN new;
END;
$$
LANGUAGE plpgsql VOLATILE;

CREATE TRIGGER write_user_historys_trigger
  AFTER UPDATE
  ON users
  FOR EACH ROW
  EXECUTE PROCEDURE write_user_history();

トリガーを動かしてみる

実際にトリガーを動かしてみる。 なお、トリガーを動かす前に、NOTICEレベルのログをクライアント(psql)とログに出力するための設定を行う。

ALTER SYSTEM SET client_min_messages = NOTICE;
ALTER SYSTEM SET log_min_messages = NOTICE;

usersテーブルに更新対象になるデータをINSERTします。

INSERT INTO users(name) values('hoge');

-- INSERT 0 1

SELECT * FROM users;

-- id | name
-- ----+------
--  1 | hoge
-- (1 row)

SELECT * FROM users_history;

-- id | user_id | old_name | new_name
-- ----+---------+----------+----------
-- (0 rows)

そのデータに対してEXPLAIN ANALYZEを利用したSQL文で更新をすると、前述の通り、実行計画とともにトリガーが実行されたことと、所要時間、実行回数が分かる。 また、この時点でRAISEしたメッセージもpsql上で確認することができる。

EXPLAIN ANALYZE UPDATE users SET name = 'fuga' where id = 1;

-- NOTICE:  messagea
 
--                                                      QUERY PLAN
-- 
-- -------------------------------------------------------------------------------------------------------------------------
--  Update on users  (cost=0.15..8.17 rows=0 width=0) (actual time=0.026..0.026 rows=0 loops=1)
--   ->  Index Scan using users_pkey on users  (cost=0.15..8.17 rows=1 width=38) (actual time=0.006..0.006 rows=1 loops=1)
--        Index Cond: (id = 1)
-- Planning Time: 0.083 ms
-- Trigger write_user_historys_trigger: time=0.414 calls=1
-- Execution Time: 0.456 ms
-- (6 rows)

users_historyテーブルにもちゃんとデータが入っている。

SELECT * FROM users;
SELECT * FROM users_history;

-- id | name
-- ----+------
--  1 | fuga
-- (1 row)

-- id | user_id | old_name | new_name
-- ----+---------+----------+----------
--   1 |       1 | hoge     | fuga
-- (1 row)

一旦コンテナから抜けて、docker logsを実行すると、RAISE文の結果がログにも出力されていることが確認できる。

docker logs example-postgres

# 2021-10-09 08:07:15.580 UTC [51] NOTICE:  messagea
# 2021-10-09 08:07:15.580 UTC [51] CONTEXT:  PL/pgSQL function write_user_history() line 4 at RAISE

わりと、PostgreSQLだけでもデバックに便利な機能はある、というのが発見だった。

他にもデバックという点でいうと

  • PL/pgSQLのASSERT文を使ってテストを書けそう
  • pgAdmin付属のデバッガ―を使ってブレークポイントを使ったりしてデバックできそう

ということもできるようだが、今回はいったんここまで。

*1:私はあまり複雑なトリガーを書いたことはないが、トランザクション自体を失敗にすることもできるので使いようはいろいろありそう

Windows 10 に開発環境を作る(WSL2 Docker編)

ようやくちょっと時間ができたので、自宅のWindows10に開発環境を準備しようとした作業メモ①です。

WSL2 で Ubunutu をインストールする

Windows 10 に WSL をインストールする | Microsoft Docsなどを参考にするとよい。

気を付けるべきこと

Ubuntuをインストールしたら、WSL2でインストールされていることを確認しておきましょう。後述しますが、昔使った環境を流用しようとしたらWSL1だったので、余計な時間を使いました。

> wsl --list --verbose
  NAME            STATE           VERSION
* Ubuntu-20.04    Stopped         2

Ubuntu をとりあえず最新化

sudo apt-get update
sudo apt-get upgrade

((そういえば、apt-getよりaptを使う方がよかったんだよなーとか実行してから気付いた)

Docker をインストール

とりあえず Ubuntu20.04 でapt searchしてみると、Ubuntu のレポジトリにはdocker.ioがあるので何も考えずapt installすることが可能。一方で公式の、Install Docker Engine on Ubuntu | Docker Documentationに従ってインストール方法もあるようだ。

  • 公式の方法で 既存の Ubuntu18.04 環境にインストールするも失敗
  • 20.04 に docker.io をインストールするも失敗

したので、結局、新しい Ubuntu20.04 環境に Docker 公式の方法でインストールすることにします。

sudo apt-get update
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

echo \
  "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

docker の動作確認

デーモン起動 & デーモン起動確認 & Hello Worldをします。

$ sudo service docker start
 * Starting Docker: docker                                                                                       [ OK ]
$ sudo service docker status
 * Docker is running
$ sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:61bd3cb6014296e214ff4c6407a5a7e7092dfa8eefdbbec539e133e97f63e09f
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/

ちゃんとHello Worldできました。よかった...

発生した問題

Docker デーモンが起動しない

Ubuntu18.04 において、Install Docker Engine on Ubuntu | Docker Documentationに従ってインストールを実施したところ、 Docker デーモンを起動しようとすると、一時的に起動しているようだがすぐ停止してしまう。

$ sudo service docker start
 * Starting Docker: docker                                                                                                        [ OK ]
$ sudo service docker status
 * Docker is running
$ sudo service docker status
 * Docker is not running

準備していた環境がだいぶ前に作ったUbuntu環境だったので、改めてUbuntu 20.04 LTSをインストールて、docker.ioを使ってみたり、改めて公式イメージを使ってみたが改善しない。

WSL 2でCannot connect to the Docker daemonと言われたら、デフォルトを確認する。 | Ginpen.comなどを確認すると、ちゃんと WSL2 が利用されているか確認した方がよさそうなので確認する。

> wsl --list --verbose
  NAME            STATE           VERSION
* Ubuntu-20.04    Running         1

...残念ながら WSL1 で動いていたようなので Version を変更する。うーん、無駄な時間を使ってしまった。

> wsl --set-default-version 2
この操作を正しく終了しました。

> wsl --set-version Ubuntu-20.04 2
変換中です。この処理には数分かかることがあります...
WSL 2 との主な違いについては、https://aka.ms/wsl2 を参照してください
変換が完了しました。

> wsl --list --verbose
  NAME            STATE           VERSION
* Ubuntu-20.04    Stopped         2

こうするとDockerデーモンが普通に起動するようになりました。

docker.io をインストールしても、Docker デーモンを起動できない

docker.ioをインストールする例が余り日本語で見つけられなかったのだが、apt show docker.ioでみるとパッケージ 1 つで動作しそうだし、apt changelog docker.ioしてみてもそこそこ更新はされてるようだったので、docker.ioを使っても大丈夫だろうと判断して実行しようとした。*1

しかし、docker.ioを WSL2 上の Ubuntu20.04 インストールして、docker デーモンを起動しようとするが、そもそもサービスとしてインストールされておらず起動できない。sudo service --status-allを実行しても表示されない。

$ sudo service docker start
docker: unrecognized service

どうやらdocker.iosystemctlで操作することが前提のようなのだが、WSL2 ではSystemdが無効化されていて、sytemctlは使えない。つまりdocker.ioを WSL2 で使うのは困難そうなことが分かった。このため Docker 公式の方法でインストールすることにした。

WSL2 において、なぜ Sytemd が使えないか&無理やり使う方法はこちらなどを参照のこと。 WSL2 で Systemd を使うハック - Qiita

※この後、UbuntuがWSL1で動いていたことが分かったのだが、どちらにしろ原因は上記だと思っています。

*1:docker.io と公式の docker の違いは、ubuntu - What is docker.io in relation to docker-ce and docker-ee? - Stack Overflowあたりを参考。

OpenAPI3の`#/components/examples/`で定義したexampleは`'#/components/schemas/`で`$ref`できない

タイトルままなのだけれど、OpenAPI3の#/components/examples/で定義したexampleは'#/components/schemas/$refできません。 APIのレスポンスボディの例として使おうとして気づきました。

NGパターン

OpenAPIで、#/components/examples/に例を定義して、'#/components/schemas/で定義したスキーマオブジェクトで参照してみます。 以下のようなyaml

openapi: "3.0.3"
info:
  title: sample
  description: "`hoge"
  version: "1.0.0"
servers:
  - url: http://example.com/api
paths:
  /user:
    post:
      description: create user
      responses:
        '201':
          description: created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas: 
    User:
      type: object 
      required: 
        - id
      properties:
        id: 
          type: integer 
          format: int64 
        name:
          type: string
      example:
        $ref: '#/components/examples/UserExample'
  examples:
    UserExample:
      value:
        id: 1
        name: John

このyamlをSwagger Editorで表示すると、エラーは出力されないものの、exampleの表示は$ref: '#/components/examples/UserExampleになって意図したオブジェクトが表示されません。 下記のようなイメージ。

exampleをうまく参照できていない
exampleをうまく参照できていない

スキーマのexample表示もなにかがおかしい

スキーマの例にもvalueが表示される
スキーマの例も何かがおかしい

OKパターン

色々試したんですが、APIのレスポンスボディとして#/components/examples/に定義した例を使いたい場合は、Media Type Objectexamplesフィールドで参照するとうまくいきます。

openapi: "3.0.3"
info:
  title: sample
  description: "`hoge"
  version: "1.0.0"
servers:
  - url: http://example.com/api
paths:
  /user:
    post:
      description: Returns all pets from the system that the user has access to
      responses:
        '201':
          description: create user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                User:
                  $ref: '#/components/examples/UserExample'
components:
  schemas: 
    User:
      type: object 
      required: 
        - id
      properties:
        id: 
          type: integer 
          format: int64 
        name:
          type: string
      example:
        id: 2
        name: Bob
  examples:
    UserExample:
      value:
        id: 1
        name: John

上記のように書くと、以下のようにちゃんと表示してくれます。

exampleをうまく参照できている
exampleをうまく参照できている

それもそのはずで、OpenAPIの仕様をちゃんと読むと、exampleフィールドとexamplesフィールドって明確に区別されてるのです。

例えば、Schema Objectなどにあるexampleフィールドの型はAnyでフリーフォーマットな記載が可能なものです。

一方で、#/components/examples/はExample Object を定義しているので、このObjectが利用可能なフィールドでだけ参照することができます。

仕様をちゃんと読まずに、しょうもないことに時間を使ってしまいまいた。英語力が欲しい。