Header Logo
Ⓒ 2024- @itatibs
[Postgres]IF NOT EXISTSがないCREATEにIF NOT EXISTSする

Category: tech
Tags(仮): Postgres,Docker
Date: 2021/5/31

やりたいこと

DockerでPostgresを使う時、/docker-entrypoint-initdb.dディレクトリ配下に.sql,.sql.gz,.shのいずれかの名前でファイルを作成することで、初期起動時に実行してくれます。

たとえば、公式にあるような/docker-entrypoint-initdb.d/init-user-db.shというファイルに

#!/bin/bash set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE USER docker; CREATE DATABASE docker; GRANT ALL PRIVILEGES ON DATABASE docker TO docker; EOSQL

と記載して起動すれば、

# \l
                                                           データベース一覧
   名前    |        所有者        | エンコーディング |  照合順序  | Ctype(変換演算子) |                 アクセス権限                  
-----------+----------------------+------------------+------------+-------------------+-----------------------------------------------
 docker    | ***** | UTF8             | ja_JP.utf8 | ja_JP.utf8        | =Tc/*****                     +
           |                      |                  |            |                   | *****=CTc/*****+
           |                      |                  |            |                   | docker=CTc/*****
 postgres  | ***** | UTF8             | ja_JP.utf8 | ja_JP.utf8        | 
 template0 | ***** | UTF8             | ja_JP.utf8 | ja_JP.utf8        | =c/*****                      +
           |                      |                  |            |                   | *****=CTc/*****
 template1 | ***** | UTF8             | ja_JP.utf8 | ja_JP.utf8        | =c/*****                      +
           |                      |                  |            |                   | *****=CTc/*****
 test      | ***** | UTF8             | ja_JP.utf8 | ja_JP.utf8        | 

という感じでdockerデータベースが自動で作成されます。

/docker-entrypoint-initdb.d/init-user-db.shは初回コンテナ作成時に実行されるのですが、.shの通り、bashなので単独で実行しても問題ないです。

そうやって2回目以降実行するときに、すでに作成済みのテーブルがあればIF NOT EXISTSをつけてやれば良いのですが、CREATE DATABASEみたいなIF NOT EXISTSが無いときはどうすれば良い?が気になったので試しました。

たとえば、先程の/docker-entrypoint-initdb.d/init-user-db.shを再度実行してみると(dbというコンテナ名で起動しています)

$ docker-compose exec db bash /docker-entrypoint-initdb.d/init-user-db.sh
ERROR:  ロール"docker"はすでに存在します

という感じで、dockerロール(ユーザー)がすでに存在しています、と怒ってきます。

結論

stackoverflowのSimulate CREATE DATABASE IF NOT EXISTS for PostgreSQL? を参考にしました。

/docker-entrypoint-initdb.d/init-user-db.shを以下のように書き換えます。

#!/bin/bash set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL SELECT 'CREATE USER docker' WHERE NOT EXISTS (SELECT usename FROM pg_user WHERE usename = 'docker')\gexec SELECT 'CREATE DATABASE docker' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'docker')\gexec GRANT ALL PRIVILEGES ON DATABASE docker TO docker; EOSQL

それぞれ、サブクエリとしてユーザーであればpg_user、データベースであればpg_databaseと基本情報として存在するテーブルに問い合わせをして、存在していなければSELECT文にあるSQLを実行する、というような流れになっています。
文末の\gexecを記載することで、SQLとして実行してくれます。

このように設定することで、何度でもbashを実行してもエラーが発生されなくなります。

$ docker-compose exec db bash /docker-entrypoint-initdb.d/init-user-db.sh
GRANT

$ docker-compose exec db bash /docker-entrypoint-initdb.d/init-user-db.sh
GRANT

そのほかのIF NOT EXISTSがないコマンドについても、同じような思想で問い合わせすることで回避できるようになるかと思います。