データベースを構築するにあたって,DockerコンテナでDBもアプリも動くといいですよね?ローカルで動かす際にdockerコンテナ内で処理が完結するのでシームレスに通信することができる.
本記事ではPythonまたはGoを用いてDockerコンテナ上のMySQLサーバーと通信する方法について記載する.
今回は以下の知識はある前提で話をする
SQLの文法
Dockerの基本
仮想環境とDockerコンテナの違い
ディレクトリ構造の確認
.
├── .env
├── docker-compose.yml
├── mysql
│ └── Dockerfile
└── python
├── Dockerfile
├── main.py
└── requirements.txt
├── .env
├── docker-compose.yml
├── mysql
│ └── Dockerfile
└── golang
├── Dockerfile
├── go.mod
├── go.sum
└── main.go
このような構造であることを前提に作成していく.
環境変数の設定
まず,.env
ファイルに必要な環境変数を設定する.
環境変数を設定するメリット
アプリケーションとデータベースの接続情報を安全に管理できる.
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=testdb
MYSQL_USER=user
MYSQL_PASSWORD=password
DB_HOST=db
DB_PORT=3306
この情報を使って,MySQLサーバーとPythonまたはGoでやりとりする.
docker環境の構築
docker-compose.yml
ファイルで,MySQLサービスとアプリケーションサービスを定義する.環境変数は.envファイルから読み込む.
python-app
またはgolang-app
サービスのcommand
で${COMMAND:-export}
を定義しているのはこの後コマンドライン引数でデータを挿入import
するか抽出export
するかを指定し,.env
から読み出すためである.
${COMMAND:-export}
は何も指定しなければデフォルトでデータを抽出export
する.
また,環境変数の情報はdockervolumeに格納しておく.
Dockerfileでは使用する言語とバージョン,依存関係のインストールをする.実行コマンドはdocker-compose.yml
に指定するので不要である.
まずmysql
ディレクトリにDockerfile
を作成する.ここではmysqlのバージョンのみ指定する.
FROM mysql:8.0
Pythonバージョン
ルートにdocker-compose.yml
ファイルを定義する.
version: '3'
services:
db:
build: ./mysql
env_file: .env
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
ports:
- "3306:3306"
python_app:
build:
context: ./python
dockerfile: Dockerfile
env_file: .env
volumes:
- ./.env:/app/.env
command: python /app/main.py ${COMMAND:-export}
depends_on:
- db
python
ディレクトリでDockerfile
ファイルを定義する.
FROM python:3.9
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
Goバージョン
ルートにdocker-compose.yml
ファイルを定義する.
version: '3'
services:
db:
build: ./mysql
env_file: .env
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
ports:
- "3306:3306"
golang_app:
build:
context: ./golang
dockerfile: Dockerfile
env_file: .env
volumes:
- ./.env:/app/.env
command: /app/main ${COMMAND:-export}
depends_on:
- db
golang
ディレクトリでDockerfile
ファイルを定義する.
FROM golang:1.18
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN go build -o main .
依存関係の定義
インストールする依存関係のリストを記載しておく.
Pythonバージョン
アプリケーションディレクトリにrequirements.txt
を作成.
mysql-connector-python==8.0.28
python-dotenv==0.19.2
Goバージョン
アプリケーションディレクトリにgo.mod
を作成.
module example.com/yourappname
go 1.18
require (
github.com/go-sql-driver/mysql v1.7.1
github.com/joho/godotenv v1.5.1
)
この後以下のコマンドを実行する.
go mod tidy
DB操作用のコードを書く
詳しい説明はコードに記載しているが,概要として...行っていることは以下の通りである.
-
環境変数からDB接続情報を取得
-
MySQLデータベースに接続. 接続失敗時は30回まで再試行
-
Userテーブルが存在しない場合, 作成
-
コマンドライン引数に基づいて操作を実行
- import: サンプルユーザーデータをテーブルに挿入
- export: テーブルの全データを取得し表示
-
データベース接続を適切に閉じる.
Pythonバージョン
import mysql.connector
from datetime import date
import time
import os
import sys
from dotenv import load_dotenv
load_dotenv()
def mysql_operations(command):
# ここでDB接続情報を環境変数から取得
db_config = {
'host': os.getenv('DB_HOST'),
'user': os.getenv('MYSQL_USER'),
'password': os.getenv('MYSQL_PASSWORD'),
'database': os.getenv('MYSQL_DATABASE'),
}
# 接続再試行用のカウント.30カウントしたら失敗とする.設定する理由は接続情報はあっているのに1回でつながらない場合がよくあるからである
for _ in range(30):
try:
conn = mysql.connector.connect(**db_config)
break
except mysql.connector.Error:
time.sleep(1)
else:
print("データベースへの接続に失敗しました")
return
cursor = conn.cursor()
# テーブルを作成するSQLクエリ(テーブルが存在しない場合のみ実行)
create_table_query = """
CREATE TABLE IF NOT EXISTS User (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL
)
"""
# ここでSQLを実行(execute)する
cursor.execute(create_table_query)
print("テーブルが作成されました")
# コマンドライン引数がimportならデータ挿入.INSERT命令を実行
if command == "import":
insert_query = "INSERT INTO User (name, email) VALUES (%s, %s)"
users = [
("John Doe", "john@example.com"),
("Jane Smith", "jane@example.com"),
]
cursor.executemany(insert_query, users)
conn.commit()
print(f"{cursor.rowcount}行のデータが挿入されました")
# コマンドライン引数がexportならデータ抽出.SELECT命令を実行
elif command == "export":
select_query = "SELECT * FROM User"
cursor.execute(select_query)
results = cursor.fetchall()
print("ユーザーデータ:")
for row in results:
print(f"ID: {row[0]}, Name: {row[1]}, Email: {row[2]}")
else:
print(f"無効なコマンドです: {command}")
# DBを閉じる
cursor.close()
conn.close()
# 実行されたらここが初めに呼び出される
if __name__ == "__main__":
if len(sys.argv) < 2:
print("使用方法: python main.py [import|export]")
sys.exit(1)
# sys.argv[1]でコマンドライン引数を取得
mysql_operations(sys.argv[1])
Goバージョン
package main
// 依存関係のインポート
import (
"database/sql"
"fmt"
"log"
"os"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/joho/godotenv"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("使用方法: go run main.go [import|export]")
os.Exit(1)
}
// sys.argv[1]でコマンドライン引数を取得
command := os.Args[1]
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
// ここでDB接続情報を環境変数から取得
dbUser := os.Getenv("MYSQL_USER")
dbPass := os.Getenv("MYSQL_PASSWORD")
dbName := os.Getenv("MYSQL_DATABASE")
dbHost := os.Getenv("DB_HOST")
dbPort := os.Getenv("DB_PORT")
var db *sql.DB
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName)
// 接続再試行用のカウント.30カウントしたら失敗とする.設定する理由は接続情報はあっているのに1回でつながらない場合がよくあるからである
for i := 0; i < 30; i++ {
db, err = sql.Open("mysql", dsn)
if err == nil {
err = db.Ping()
if err == nil {
break
}
}
time.Sleep(time.Second)
}
if err != nil {
log.Fatal("データベースへの接続に失敗しました:", err)
}
defer db.Close()
// テーブルを作成するSQLクエリ(テーブルが存在しない場合のみ実行)ついでに実行(exec)
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS User (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL
)
`)
if err != nil {
log.Fatal(err)
}
fmt.Println("テーブルが作成されました")
// コマンドライン引数がimportならデータ挿入.INSERT命令を実行
switch command {
case "import":
stmt, err := db.Prepare("INSERT INTO User (name, email) VALUES (?, ?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
users := []struct {
name string
email string
}{
{"John Doe", "john@example.com"},
{"Jane Smith", "jane@example.com"},
}
for _, user := range users {
_, err = stmt.Exec(user.name, user.email)
if err != nil {
log.Fatal(err)
}
}
fmt.Println("データが挿入されました")
// コマンドライン引数がexportならデータ抽出.SELECT命令を実行
case "export":
rows, err := db.Query("SELECT * FROM User")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
fmt.Println("ユーザーデータ:")
for rows.Next() {
var id int
var name, email string
err := rows.Scan(&id, &name, &email)
if err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d, Name: %s, Email: %s\n", id, name, email)
}
default:
fmt.Println("無効なコマンドです。import または export を指定してください。")
}
}
コマンドライン引数の環境変数を追加
このコードは以下のように実行されることを前提としている.
Python main.py import
Python main.py export
go main.go import
go main.go export
しかしdocker-compose.yml
に直接コマンドライン引数を定義することは不適切なため,.env
ファイルにコマンドライン引数を定義しておく.
そのため先ほど
command
は${COMMAND:-export}
を使って定義した.
.env
に以下の行を追加する
COMMAND=import
実行時にはここの文字をimport
にするか,export
にするかを指定して,データを挿入するのか抽出するのかを選択する.
実行
これで以下のコマンドを実行してコンテナを立ち上げてアプリケーションを実行する.
docker compose up
DBサーバーにアクセスしたい場合
DBサーバーに直接アクセスしたい場合は以下のコマンドを実行すること.DBサーバー内でSQLクエリを実行することができる.
docker compose up -d
docker compose exec db mysql -u user -p password testdb