rukurxの日記

自分の日々の作業や調べたことのメモ

DockerでローカルのMySQL環境を構築する

Macのローカル環境にDockerでMySQL立てた時のメモ。

ディレクトリ構成は以下を想定。

docker-compose.yml
docker
  - db
    - conf.d
      - my.cnf
    - data
    - initdb.d
      - 001_init.sql

まずはdocker-compose.ymlを用意。 Volumesでローカルのディレクトリ/ファイルをコンテナにマウントする。 /docker/db/data でデータを永続化できる データ消す場合はこのディレクトリ以下を削除する必要がある。

version: "3"

services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: mydb
      MYSQL_USER: mydb
      MYSQL_PASSWORD: mydb
      TZ: "Asia/Tokyo"
    volumes:
      - ./docker/db/conf.d:/etc/mysql/conf.d
      - ./docker/db/initdb.d:/docker-entrypoint-initdb.d
      - ./docker/db/data:/var/lib/mysql
    ports:
      - "3306:3306"

続いてmy.cnfを用意。

[mysql]
default-character-set = utf8mb4

[mysqld]
character-set-server = utf8mb4
sql_mode = STRICT_TRANS_TABLES
skip-character-set-client-handshake
collation-server = utf8mb4_general_ci
init-connect = SET NAMES utf8mb4
bind-address = 0.0.0.0

これだけあればとりあえずMySQLのコンテナを立ち上げられるので試す。

$ docker-compose up db

rootユーザでログインする。

$ mysql -uroot -p -h 127.0.0.1
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.24 MySQL Community Server (GPL)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mydb               |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.02 sec)

docker-compose.ymlで指定したユーザmydbでログインする。

$ mysql -umydb -p -h 127.0.0.1
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.7.24 MySQL Community Server (GPL)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mydb               |
+--------------------+
2 rows in set (0.01 sec)

起動時に初期化したい場合は docker/db/initdb.d/ にシェルやSQLファイルを置くと実行される。 試しに初期化時に2つ目のテスト用のテーブルを作成する。 001-create.sql

CREATE DATABASE IF NOT EXISTS mydb;
GRANT ALL ON *.* to 'mydb'@'%';

シェルにしたい場合。 001-create.sh

#!/bin/sh

echo "CREATE DATABASE IF NOT EXISTS test_mydb;"
echo "GRANT ALL ON *.* to 'mydb'@'%';"

環境変数を使って docker-compose.yml を汎用的にする

.env

MYSQL_PORT=3306
MYSQL_DATABASE=mydb
MYSQL_USER=mydb
MYSQL_PASSWORD=mydb
MYSQL_TEST_DATABASE=test_mydb

環境変数使った docker-compose.yml

version: '3'

services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_TEST_DATABASE: ${MYSQL_TEST_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      TZ: "Asia/Tokyo"
    volumes:
      - ./docker/db/conf.d:/etc/mysql/conf.d
      - ./docker/db/initdb.d:/docker-entrypoint-initdb.d
      - ./docker/db/data:/var/lib/mysql
    ports:
      - "${MYSQL_PORT:-3306}:3306"

初期化用のシェルも環境変数使う場合。

#!/bin/sh

# Django TEST DB
if [ "$MYSQL_TEST_DATABASE" ]; then
    echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_TEST_DATABASE\` ;" | "${mysql[@]}"
    echo "GRANT ALL ON \`$MYSQL_TEST_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" | "${mysql[@]}"
    echo 'FLUSH PRIVILEGES ;' | "${mysql[@]}"
fi

プログラム側からMySQLに接続するように以下の環境変数も追加しておくと便利。 .env

MYSQL_HOST=127.0.0.1
DATABASE_URL=mysql://mydb:mydb@127.0.0.1:3306/mydb

Lambdaから他のLambda関数を呼び出す

Lambda使っていて、ふとLambdaから他のLambda関数って呼べないのかなと思って調べてみたらできたのでその時のメモ。

InvocationTypeで同期処理か非同期処理かの呼び出し方法を指定できる。 期処理がよければRequestResponseを指定、非同期ならEventを指定する。 非同期処理にすると最初のLambdaはすぐにレスポンスを返してくれるので、Lambda呼び出し先のタイムアウト等を気にする必要がなくなる。 呼び出した関数からのレスポンスを使いたいば同期処理にすればよい。

同期で関数を呼びたい場合

import os
import json
import boto3
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# 呼び出したい関数名
function_name = os.environ.get('LAMBDA_FUNCTION_NAME')

def lambda_handler(event: dict, context):
    # ログ出力
    logging.info(json.dumps(event))

    client = boto3.client('lambda')
    response = client.invoke(
        FunctionName=function_name,
        InvocationType='RequestResponse',
        LogType='Tail',
        Payload=json.dumps(event)
    )

    return {
        'statusCode': 200,
        'body': json.dumps('OK')
    }

非同期で関数を呼びたい場合

import os
import json
import boto3
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# 呼び出したい関数名
function_name = os.environ.get('LAMBDA_FUNCTION_NAME')

def lambda_handler(event: dict, context):
    # ログ出力
    logging.info(json.dumps(event))

    client = boto3.client('lambda')
    client.invoke(
        FunctionName=function_name,
        InvocationType='Event',
        LogType='Tail',
        Payload=json.dumps(event)
    )

    return {
        'statusCode': 200,
        'body': json.dumps('OK')
    }

やっていてハマったのが、非同期処理にしていても以下の用にレスポンスを変数で受け取るようにすると結果が帰ってくるまで最初のLambdaの実行が終わらない。 そのため、場合によっては呼び出し元がタイムアウトと勘違いをしてリトライしてくる可能性がある。 SlackからLambdaのAPIをたたいていて実際にあった。Slackは3秒以内にレスポンスが帰らなければリトライするようです。

    response = client.invoke(
        FunctionName=function_name,
        InvocationType='Event',
        LogType='Tail',
        Payload=json.dumps(event)
    )

ちょっと長い処理だったり、条件によって処理をわけたい場合等に使うと力を発揮してくれそう。 個人的には非同期処理がLambdaだけでできるのは助かるな。

参考

AWS LambdaからLambdaを非同期で呼び出す(Python) - Qiita

[AWS lambda] Slack Botがタイムアウトで何度もレスポンスするのを防ぐ - Qiita

[AWS][Python]Lambdaファンクションから他のLambdaファンクションを呼ぶ : 工作と競馬

DjangoのモデルでHaving句を使う

ユーザと趣味モデルを定義して、趣味が3つ以上ある既婚者を取得する例。

from django.db import models

class Hobby(models.Model):
    """
    趣味モデル
    """
    name = models.CharField(max_length=255,  null=False, blank=False)

class User(models.Model):
    """
    ユーザモデル
    """
    name = models.CharField(max_length=255,  null=False, blank=False)
    is_married = models.BooleanField(default=False)
    hobbies = models.ManyToManyField(
        to=Hobby,
    )
# 既婚者で趣味が3つ以上あるユーザ取得
User.objects.annotate(hobby_count=Count('hobbies')).filter(is_married=True, hobby_count__gte=3)

参照

stackoverflow.com

Pythonで電話番号をハイフン区切りに変換する

電話番号をハイフン区切りにできる便利なライブラリがないか探していたところ、GoogleGithubで公開しているlibphonenumberを見つけました! libphonenumberは国際電話番号を解析・変換・バリデーションすることができます。

github.com

だがしかし!Java, C++, JavaScript の実装しかない… 諦めかけたその時、サードパーティが多言語に移植してくれている一覧を発見!C#, Go, Objective-c, PHP, PostgreSQL in-database types, Python, Ruby …!!! Pythonもありました! 名前は phonenumbers になってますね。わからいやすいな。 早速こちらのライブラリを試して見ました。

github.com

まずはライブラリのインストール

$ pip install phonenumbers

Pythonコンソール起動して実行

$ python
>>> import phonenumbers
>>> tel = '0300000000'
>>> tel_hyphen = phonenumbers.parse(tel, 'JP')
>>> phonenumbers.format_number(tel_hyphen, phonenumbers.PhoneNumberFormat.NATIONAL)
u'03-0000-0000'

ちゃんとハイフン区切りに変換されました!素晴らしい… 変換する電話番の形式がおかしかったりすると例外投げるので、入力ミスった値が来た時用に例外処理は入れといた方がよさそう。

libphonenumberの解説をしてくれているスライドあったので気になったら見てみよう。

speakerdeck.com

Djangoのテンプレート内でリレーション関係の子データをソートする

リレーション組んで取得した子データの表示順がバラバラだったのでどうにかできないかと思ったらテンプレートの組み込みタグである dictsort を使って解決できました!

dictsortを指定したキーで昇順にする

<ul>
{% for user in users %}
  {% for hobby in user.hobbies.all|dictsort:"id"%}
      <li>{{ hobby.category.name }}: {{ hobby.name }}</li>
  {% endfor %}
{% endfor %}
</ul>

降順にしたい場合はdictsortreversedを指定する。てっきりorder_byみたいに-つければいいのかと思った...

<ul>
{% for user in users %}
  {% for hobby in user.hobbies.all|dictsortreversed:"id"%}
      <li>{{ hobby.category.name }}: {{ hobby.name }}</li>
  {% endfor %}
{% endfor %}
</ul>

更にリレーション関係のあるキーでソートしたい場合は、カンマ区切りで指定できる。

<ul>
{% for user in users %}
  {% for hobby in user.hobbies.all|dictsort:"category.id"%}
      <li>{{ hobby.category.name }}: {{ hobby.name }}</li>
  {% endfor %}
{% endfor %}
</ul>

参考

docs.djangoproject.com

aliasで設定したコマンドをシェルスクリプト内で実行する

シェルスクリプトを実行したらaliasで追加したコマンドが無いよと怒られた...

./hoge.sh: 行 40: hoge: コマンドが見つかりません

普段コマンドラインで使っているコマンドだったので、???状態。 man bash で見たらインタラクティブモードじゃないとaliasで登録したコマンドが使いえないらしい...

Aliases are not expanded when the shell is not interactive, unless the expand_aliases shell option is set using shopt (see the description of shopt under SHELL BUILTIN COMMANDS below).

そこで、 /etc/bashrc にaliasを登録して、環境変数を渡すことでシェルスクリプト内でもコマンドを実行できるようにした。

$ BASH_ENV=~/.bashrc bash -O expand_aliases  hoge.sh

参考

takafumi-s.hatenablog.com