- 投稿日:2021-01-03T19:49:12+09:00
MySQL カラムコメントを修正する方法
目的
- MySQLのカラムコメントを変更するSQLを紹介する
環境
- ハードウェア環境
項目 情報 OS macOS Catalina(10.15.5) ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports) プロセッサ 2 GHz クアッドコアIntel Core i5 メモリ 32 GB 3733 MHz LPDDR4 グラフィックス Intel Iris Plus Graphics 1536 MB
- ソフトウェア環境
項目 情報 備考 MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする 情報
- Macに直接立てたMySQLサーバでこの記事の内容を検証した。
方法
下記SQLを実行してカラムコメントを変更する。(カラム名が2回出てくるが記載ミスではない。)
alter table テーブル名 change column カラム名 カラム名 カラム型 その他のカラム設定 comment '更新したいコメント'例えばusersテーブルのflagカラム(TinyInt型 not null)のコメントを「フラッグ用のカラム(1:->on 0->off)」としたい時のSQLは下記となる。
alter table users change column flag flag tinyint not null comment 'フラッグ用のカラム(1:->on 0->off)'
- 投稿日:2021-01-03T19:44:25+09:00
【SequelPro】NOW()を一時的に日本時間にする
概要
SequelProでクエリを書いてINSERTやUPDATEをする際、現在日時を登録したい場合に、
NOW()
を使うことがよくあります。
DBのデフォルトのタイムゾーンの設定が日本時間になっていれば良いですが、そうでない場合は日本時間ではなく、9時間ずれた日時が入ってしまいます。会社の環境であればデフォルトの設定を変更するのは諸事情で難しかったりします。
都度直接日時を記述しても良いですが、面倒です。僕は今まで直接日時を記述したり、NOW()で入れた後に手動で日時を書き換えたりしていましたが、そんなことしなくてもクエリでNOW()を一時的に日本時間にする方法がありました。
やり方
SET SESSION time_zone = 'Asia/Tokyo';SequelProでINSERTやUPDATEを実行する前にこれを実行する。それだけ。
デフォルトでは
time_zone = 'UTC'
なのを、日本時間に変更しています。
これでこの後にINSERTやUPDATEでNOW()
を使うと、日本時間でDBへ保存されます。あとがき
正直このやり方が普通なのか、もっと別のやり方があるのか、わかりません。
ただ私の周りの環境では、NOW()は日本時間にならないから使わない、日時を直接記述する、という場合が多かったです。
スプレッドシートなどで、貼り付けたデータを元に関数でINSERT文やUPDATE文を作成することがあり、その際に日時をいちいち書き換えたくないのでNOW()を日本時間にする方法を調べました。参考
AWS RDS MySQL のタイムゾーンが UTC の状態で DATETIME フィールドに JST で入れてしまった場合の復旧
- 投稿日:2021-01-03T14:41:25+09:00
Tinkerを使ってみYO!
はじめに
Laravelを学ぶ中でTinkerが便利だなと感じたので記事にしてみました!
TinkerはLaravel標準で使える対話型のコマンドラインツールで、「PHP/Laravelの関数を実行」「DBのレコード操作」などを行うことができます。今回はユーザデータを見たり消したりしてみようと思います。目次
$ php artisan tinker
dockerの場合は、docker-compose.ymlファイルある場所で下記コマンド実行
$ docker-compose exec コンテナ名 php artisan tinker
を実行すると下記のような表示がされ、対話的にコマンドを実行することができます。
>>>使用できるコマンドを確認できます。
>>> help次はユーザーモデル全件を確認
>>> App\User::all();Userクラスを使っていますが、クラスを使う場合はApp\Userといったように名前空間も記述するようにしてください。
名前空間とは、app/User.phpのnamespaceの後に記述されている部分です。
└──laravel
└── app
└── User.phpちなみにこのappディレクトリはアプリケーションのコアとなるコードを含む重要なディレクトリです。
アプリケーション内のクラスのほとんどはこの中に設置されます。このメソッドは下記のようにコレクション(Illuminate\Database\Eloquent\Collection)が帰ってきました。このようにメソッドが配列を返すか、コレクションを返すかといったことも確認できます。
your.terminal=> Illuminate\Database\Eloquent\Collection {#3053 all: [ App\User {#3978 id: 1, name: "hoge", email: "hoge@example.com", email_verified_at: null, created_at: "2021-01-03 23:10:40", updated_at: "2021-01-03 23:10:40", }, ], }なお、App\Userを確認していただくとわかりますが、passwordやremember_tokenといったプロパティがあるのに、それらが表示されていません。(Userモデルで表示しない指定がされているため)
ユーザーモデルを削除
次にhogeというユーザーが不要なので、これを削除します。ユーザーhogeのidは1であるので、以下のコードを実行してください。
your.terminal>>> App\User::destroy(1);
=> 1
といったように削除件数が表示されれば削除は成功です。TinkerはLaravel標準で使える対話型のコマンドラインツールで、「PHP/Laravelの関数を実行」「DBのレコード操作」などを行うことができます。使いこなせるように積極的に使用していきます。
- 投稿日:2021-01-03T14:41:25+09:00
Tinkerでデータ操作してみました
はじめに
Laravelを学ぶ中でTinkerが便利だなと感じたので記事にしてみました!
TinkerはLaravel標準で使える対話型のコマンドラインツールで、「PHP/Laravelの関数を実行」「DBのレコード操作」などを行うことができます。今回はユーザデータを見たり消したりしてみようと思います。Tinkerを起動
$ php artisan tinker
dockerの場合は、docker-compose.ymlファイルある場所で下記コマンド実行
$ docker-compose exec コンテナ名 php artisan tinker
を実行すると下記のような表示がされ、対話的にコマンドを実行できます。
>>>下記コマンド(help)で使用できるコマンドを確認できます。
>>> helpユーザーモデル全件を確認
>>> App\User::all();Userクラスを使っていますが、クラスを使う場合はApp\Userといったように名前空間も記述するようにしてください。
名前空間とは、
app/User.phpのnamespace
の後に記述されている部分です。└──laravel
└── app
└── User.phpちなみにこのappディレクトリはアプリケーションのコアとなるコードを含む重要なディレクトリです。
アプリケーション内のクラスのほとんどはこの中に設置されます。この all() メソッドからは下記のようにコレクション(Illuminate\Database\Eloquent\Collection)が帰ってきました。このようにメソッドがどんな値を返すか、配列かコレクションかといったようなことも確認できます。
your.terminal=> Illuminate\Database\Eloquent\Collection {#3053 all: [ App\User {#3978 id: 1, name: "hoge", email: "hoge@example.com", email_verified_at: null, created_at: "2021-01-03 23:10:40", updated_at: "2021-01-03 23:10:40", }, ], }なお、App\Userを確認していただくとわかりますが、passwordやremember_tokenといったプロパティがあるのに、それらが表示されていません。(Userモデルで表示しない指定がされているため)
ユーザーモデルを削除
次にhogeというユーザーが不要なので、これを削除します。
ユーザーhogeのidは1であるので、以下のコードを実行してください。
your.terminal>>> App\User::destroy(1);
=> 1
といったように削除件数が表示されれば削除は成功です。まとめ
TinkerはLaravel標準で使える対話型のコマンドラインツールで、「PHP/Laravelの関数を実行」「DBのレコード操作」などを行うことができます。使いこなせるように積極的に使用していきます。
- 投稿日:2021-01-03T14:41:25+09:00
Tinkerでデータ操作
はじめに
Laravelを学ぶ中でTinkerが便利だなと感じたので記事にしてみました!
TinkerはLaravel標準で使える対話型のコマンドラインツールで、「PHP/Laravelの関数を実行」「DBのレコード操作」などを行うことができます。今回はユーザデータを見たり消したりしてみようと思います。Tinkerを起動
$ php artisan tinker
dockerの場合は、docker-compose.ymlファイルある場所で下記コマンド実行
$ docker-compose exec コンテナ名 php artisan tinker
を実行すると下記のような表示がされ、対話的にコマンドを実行できます。
>>>下記コマンド(help)で使用できるコマンドを確認できます。
>>> helpユーザーデータ全件を確認
>>> App\User::all();Userクラスを使っていますが、クラスを使う場合はApp\Userといったように名前空間も記述するようにしてください。
名前空間とは、
app/User.phpのnamespace
の後に記述されている部分です。└──laravel
└── app
└── User.phpちなみにこのappディレクトリはアプリケーションのコアとなるコードを含む重要なディレクトリです。
アプリケーション内のクラスのほとんどはこの中に設置されます。この all() メソッドからは下記のようにコレクション(Illuminate\Database\Eloquent\Collection)が帰ってきました。このようにメソッドがどんな値を返すか、配列かコレクションかといったようなことも確認できます。
your.terminal=> Illuminate\Database\Eloquent\Collection {#3053 all: [ App\User {#3978 id: 1, name: "hoge", email: "hoge@example.com", email_verified_at: null, created_at: "2021-01-03 23:10:40", updated_at: "2021-01-03 23:10:40", }, ], }なお、App\Userを確認していただくとわかりますが、passwordやremember_tokenといったプロパティがあるのに、それらが表示されていません。(Userモデルで表示しない指定がされているため)
ユーザーデータを削除
次にhogeというユーザーが不要なので、これをデータベースから削除します。
ユーザーhogeのidは1であるので、以下のコードを実行してください。
your.terminal>>> App\User::destroy(1);
=> 1
といったように削除件数が表示されれば削除は成功です。まとめ
今回は初歩的な操作としてユーザデータを見たり消したりしました。
TinkerはLaravel標準で使える対話型のコマンドラインツールで、「PHP/Laravelの関数を実行」「DBのレコード操作」などを行うことができます。使いこなせるように積極的に使用していきたいです。
- 投稿日:2021-01-03T01:12:32+09:00
DockerでReact+Django+Nginx+MySQLの環境構築
はじめに
Docker環境でコマンドを打つだけで一発でDjango,React,MySQLなどを立ち上げて開発をできるようにしたくて、今回の記事を書きました。
この記事は一から環境を構築することを目指していますが、完成形だけをみたいかたはこちらのGitHubからどうぞ:django-react-nginx-mysql-docker
(READMEに書いてあることを実行すれば、うまくいくはずです)目標
- 仮想環境などは一切使わず、終始dockerでプロジェクトの作成などを行う
docker-compose up
でウェブ開発に必要なすべてのコンテナが立ち上がるようにする- 最終的にK8sにデプロイする(次回の記事になると思います)。
最初は仮想環境で立ち上げて、そのあとにdockerfileを作成して環境構築をできるようにする、という記事は多く見かけます。ですが私は
virtualenv
とかyarn
をローカルに入れるのが面倒なので、終始Dockerで全部プロジェクトの管理を行いたいと思います。前提
- Dockerインストール済み
- docker-composeインストール済み
- LinuxまたはMac(強めのCPUとメモリがあるのが好ましいです)
流れ
以下のように進めていきます。
- バックエンドとDBの構築
- バックエンドのAPIを実際に触ってみて、データを追加してみる
- フロントエンドの構築
- APIでデータを取得し、フロントにて表示してみる
使う技術
- Docker (docker-compose)
- django (django rest framework)
- nginx
- mysql
- react
- next
- typescript
ルートディレクトリにフォルダ作成
ではまずフォルダの作成から始めます。
プロジェクトフォルダを作成して、その直下で以下のコマンドを打ちます。$ mkdir backend $ mkdir frontend $ mkdir mysql $ mkdir mysql_volume $ mkdir sql $ touch docker-compose.yml以下のようになっているはずです。
$ tree . ├── backend ├── docker-compose.yml ├── frontend ├── mysql ├── mysql_volume └── sql 5 directories, 1 file1. BackendとDBの構築
web-back
とnginx
のフォルダを作成します。nginx
とweb-back
は今後K8sにデプロイするときには同じポッドにしようと思っているので、このような構成になります。フロントのときのweb-front
とnginx
も同じです。$ cd backend $ mkdir web-back $ mkdir nginxweb-backの用意
$ cd web-back $ touch .env Dockerfile requirements.txt
.env
はAPIのKEYなど、センシティブな情報を含むファイルです。今はシークレットキーなどはないので、とりあえずテキトーに埋めておきます。backend/web-back/.envSECRET_KEY='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' DEBUG=FalsePython環境のDockerfileです。
# backend/web-back/Dockerfile # set base image FROM python:3.7 # set environment variables ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 # set work directory WORKDIR /code # install dependencies COPY requirements.txt ./ RUN python3 -m pip install --upgrade pip setuptools RUN pip install -r requirements.txt # Copy project COPY . ./ # Expose application port EXPOSE 8000pipでインストールするモジュールです。
backend/web-back/requirements.txtasgiref==3.2.7 Django==3.0.5 django-cors-headers==3.2.1 djangorestframework==3.11.0 gunicorn==20.0.4 psycopg2-binary==2.8.5 python-dotenv==0.13.0 pytz==2019.3 sqlparse==0.3.1 mysqlclient==2.0.2nginxの用意
nginxフォルダに入ってDockerfileとconfファイルを作成します。
今後デプロイするとき用にファイルを分けたいので、dev
を入れておいて区別できるようにします。$ cd ../nginx $ touch Dockerfile.dev default.dev.confnginxのDockerfileです。
backend/nginx/Dockerfile.devFROM nginx:1.17.4-alpine RUN rm /etc/nginx/conf.d/default.conf COPY default.dev.conf /etc/nginx/conf.dnginxコンテナに回ってきた通信をすべてdjangoのコンテナに流すようにします。
backend/nginx/default.dev.confupstream django { server web-back:8000; } server { listen 80; location = /healthz { return 200; } location / { proxy_pass http://django; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect off; } location /static/ { alias /code/staticfiles/; } }MySQLの用意
mysql
フォルダ直下にDockerfileとmy.cnfを作成します。$ cd ../../mysql $ touch Dockerfile my.cnfmysqlのバージョンは
8.0.0
を使うことにします。FROM mysql:8.0.0 RUN echo "USE mysql;" > /docker-entrypoint-initdb.d/timezones.sql && mysql_tzinfo_to_sql /usr/share/zoneinfo >> /docker-entrypoint-initdb.d/timezones.sql COPY ./my.cnf /etc/mysql/conf.d/my.cnf文字コードなどの設定を
my.cnf
に書き込みます。mysql/my.cnf# MySQLサーバーへの設定 [mysqld] # 文字コード/照合順序の設定 character_set_server=utf8mb4 collation_server=utf8mb4_bin # タイムゾーンの設定 default_time_zone=SYSTEM log_timestamps=SYSTEM # デフォルト認証プラグインの設定 default_authentication_plugin=mysql_native_password # mysqlオプションの設定 [mysql] # 文字コードの設定 default_character_set=utf8mb4 # mysqlクライアントツールの設定 [client] # 文字コードの設定 default_character_set=utf8mb4sqlフォルダの用意
SQLフォルダに移動し、
init.sql
を作成します。$ cd ../sql $ touch init.sqlsql/init.sqlGRANT ALL PRIVILEGES ON test_todoList.* TO 'user'@'%'; FLUSH PRIVILEGES;docker-composeでバックエンドの立ち上げ
ここまででファイルは以下のようになっているはずです。
$ tree -a . ├── backend │ ├── nginx │ │ ├── default.dev.conf │ │ └── Dockerfile.dev │ └── web-back │ ├── Dockerfile │ ├── .env │ └── requirements.txt ├── docker-compose.yml ├── frontend ├── mysql │ ├── Dockerfile │ └── my.cnf ├── mysql_volume └── sql └── init.sql 7 directories, 9 filesフロントエンドはまたあとでやるので、とりあえずバックエンドの立ち上げを行っていきます。以下のように
docker-compose.yml
ファイルを用意します。docker-compose.ymlversion: "3.7" services: web-back: container_name: python-backend env_file: ./backend/web-back/.env build: ./backend/web-back/. volumes: - ./backend/web-back:/code/ - static_volume:/code/staticfiles # <-- bind the static volume stdin_open: true tty: true command: gunicorn --bind :8000 config.wsgi:application networks: - backend_network environment: - CHOKIDAR_USEPOLLING=true - DJANGO_SETTINGS_MODULE=config.local_settings depends_on: - db backend-server: container_name: nginx_back build: context: ./backend/nginx/. dockerfile: Dockerfile.dev volumes: - static_volume:/code/staticfiles # <-- bind the static volume ports: - "8080:80" depends_on: - web-back networks: - backend_network db: build: ./mysql command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: todoList MYSQL_USER: user MYSQL_PASSWORD: password TZ: 'Asia/Tokyo' volumes: - ./mysql_volume:/var/lib/mysql - ./sql:/docker-entrypoint-initdb.d networks: - backend_network networks: backend_network: driver: bridge volumes: static_volume:内容が多いので少し難しいですね。
今回はとりあえず動くものを作りたいので、意味については割愛させていただきます。
ではDjangoのプロジェクトを作成しましょう!まずはconfig
というプロジェクトを作成します。$ docker-compose run --rm web-back sh -c "django-admin startproject config ." Creating backend_web-back_run ... done etc..... $ docker-compose run --rm web-back sh -c "python manage.py startapp todo" Creating backend_web-back_run ... doneうまくいったら以下のように
config
とtodo
が作成されているはずです。$ tree . ├── backend │ ├── nginx │ │ ├── default.dev.conf │ │ └── Dockerfile.dev │ └── web-back │ ├── config │ │ ├── asgi.py │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── Dockerfile │ ├── manage.py │ ├── requirements.txt │ ├── staticfiles │ └── todo │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ............................開発環境用のsettingファイルの作成
開発環境と本番環境で設定ファイルを分けたいので、
config
フォルダにてlocal_setting.py
ファイルを作成します。settings.py
の情報を引き継ぐようにして、データベースの情報だけここで塗り替えます。config/local_settings.pyfrom .settings import * DEBUG = True ALLOWED_HOSTS = ['*'] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'todoList', 'USER': 'user', 'PASSWORD': 'password', 'HOST': 'db', 'PORT': '3306', } }これでビルドしてみましょう。
$ docker-compose up --build Starting python-backend ... done Starting nginx ... done Attaching to python-backend, nginx python-backend | [2020-12-28 14:59:49 +0000] [1] [INFO] Starting gunicorn 20.0.4 python-backend | [2020-12-28 14:59:49 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) python-backend | [2020-12-28 14:59:49 +0000] [1] [INFO] Using worker: sync python-backend | [2020-12-28 14:59:49 +0000] [10] [INFO] Booting worker with pid: 10これで
localhost:8080
にアクセスしてみましょう。以下の画面が出てくるはずです。
8080
ポートにアクセスすると、nginxが8000
ポートに通信を流してくれます。それによってdjangoの提供してくれるページにアクセスできます。マイグレーションの準備と実行
- rest frameworkを使いたい
- APIを操作するための管理画面ページを使いたい
上記がまだできていないので、ここではそのためのデータベースのマイグレーションの準備を行います。以下3つのファイルを編集していきます。
settings.py
todo/models.py
todo/admin.py
settings.py
を以下のように編集します。ついでにこの際にcors
の部分も追加し、あとからフロントエンドからバックエンドのAPIを呼び出せるようにしておきます。config/settings.py""" Django settings for config project. Generated by 'django-admin startproject' using Django 3.0.5. For more information on this file, see https://docs.djangoproject.com/en/3.0/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.0/ref/settings/ """ import os from dotenv import load_dotenv # 追加 # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.basename(BASE_DIR) # 追加 # .envの読み込み load_dotenv(os.path.join(BASE_DIR, '.env')) # 追加 # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '1_vj5u9p3nm4fwufe_96e9^6li1htp9avbg8+7*i#h%klp#&0=' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 3rd party 'rest_framework', 'corsheaders', # Local 'todo.apps.TodoConfig', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'corsheaders.middleware.CorsMiddleware', ] ROOT_URLCONF = 'config.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'config.wsgi.application' # Password validation # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ LANGUAGE_CODE = 'ja' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = '/static/' # 開発環境下で静的ファイルを参照する先 STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # 追加 # 本番環境で静的ファイルを参照する先 STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # 追加 # メディアファイルpath MEDIA_URL = '/media/' # 追加 # 追加 REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.AllowAny', ] } CORS_ORIGIN_WHITELIST = ( 'http://localhost', )
todo
の中のmodels.py
を編集します。todo/models.pyfrom django.db import models class Todo(models.Model): title = models.CharField(max_length=200) body = models.TextField() def __str__(self): return self.title
todo
の中のadmin.py
を編集します。todo/admin.pyfrom django.contrib import admin from .models import Todo admin.site.register(Todo)これでマイグレーションを以下のように実行します。ついでにsuperuserも作成しておきます。パスワードなどは好きなように設定してください。
$ docker-compose run --rm web-back sh -c "python manage.py makemigrations" Creating backend_web-back_run ... done Migrations for 'todo': todo/migrations/0001_initial.py - Create model Todo $ docker-compose run --rm web-back sh -c "python manage.py migrate" Creating backend_web-back_run ... done Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions, todo Running migrations: Applying contenttypes.0001_initial... OK ........ $ docker-compose run --rm web-back sh -c "python manage.py createsuperuser" Creating backend_web-back_run ... done ユーザー名 (leave blank to use 'root'): メールアドレス: example@gmail.com Password:root .........URLの設定
admin
とapi
のページに飛べるように設定します。backend/web-back/config/urls.pyfrom django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('todo.urls')) # 追加 ]
todo
でもURLの設定などを行わなければいけません。また、JSONに変換するserializerファイルも作成します。backend/web-back/todo $ touch urls.py serializers.pyurls.pyfrom django.urls import path, include from .views import ListTodo, DetailTodo urlpatterns = [ path('<int:pk>/', DetailTodo.as_view()), path('', ListTodo.as_view()) ]serializers.pyfrom rest_framework import serializers from .models import Todo class TodoSerializer(serializers.ModelSerializer): class Meta: model = Todo fields = ('id', 'title', 'body')viewも編集します。
views.pyfrom django.shortcuts import render # Create your views here. from django.shortcuts import render from rest_framework import generics from .models import Todo from .serializers import TodoSerializer class ListTodo(generics.ListAPIView): queryset = Todo.objects.all() serializer_class = TodoSerializer class DetailTodo(generics.RetrieveAPIView): queryset = Todo.objects.all() serializer_class = TodoSerializer2.APIを触ってデータを追加してみる。
もう一度走らせて、
admin
とapi
にアクセスするこのままではcssファイルなどが反映されないので、staticなファイルをまず整理してから立ち上げます。
$ cd backend/web-back $ mkdir static $ docker-compose run --rm web-back sh -c "python manage.py collectstatic" Starting ... done 163 static files copied to '/code/staticfiles'.$ docker-compose up
localhost:8080/admin
は以下のようになります。先ほど作成したsuperuserでログインしましょう。ログインしたらtodoの管理などができる画面に入ります。
こんな感じで追加しておきます。
これで
localhost:8080/api/1
に行くと、見つかります。これで以下のことができるようになりました。
- 管理画面へのログイン
- APIでデータの取得
これでフロントエンドの構築を始めることができます。
mysql dbでも確認してみる
以下のようにコンテナに入って確認すると、たしかにデータが格納されています。
$ docker exec -it container_db bash root@e34e5d2a20e1:/# mysql -u root -p mysql> use todoList; mysql> select * from todo_todo; +----+-------------+--------------+ | id | title | body | +----+-------------+--------------+ | 1 | do homework | finish maths | +----+-------------+--------------+ 1 row in set (0.00 sec)(余談)テストファイルの作成と実行
今すぐ必要というわけではないですが、テストファイルの作成と実行も一通りここでやっておきます。以下のテストファイルを走らせます。
backend/web-back/todo/tests.pyfrom django.test import TestCase # Create your tests here. from django.test import TestCase from .models import Todo class TodoModelTest(TestCase): @classmethod def setUpTestData(cls): Todo.objects.create(title="first todo", body="a body here") def test_title_content(self): todo = Todo.objects.get(id=1) excepted_object_name = f'{todo.title}' self.assertEqual(excepted_object_name, 'first todo') def test_body_content(self): todo = Todo.objects.get(id=1) excepted_object_name = f'{todo.body}' self.assertEqual(excepted_object_name, 'a body here')テストをコンテナの中で走らせます。うまく通るはずです。
$ docker-compose run --rm web-back sh -c "python manage.py test" Creating backend_web-back_run ... done Creating test database for alias 'default'... System check identified no issues (0 silenced). .. ---------------------------------------------------------------------- Ran 2 tests in 0.005s OK Destroying test database for alias 'default'...3. フロントエンドの構築
それでは、フロントエンドのほうのnginxとreact+next.jsの環境を構築していきます。
$ cd frontend/ $ mkdir nginx web-front $ cd nginx $ touch Dockerfile.dev default.dev.conf wait.sh以下のようなファイル構成にします。
$ cd ../ $ tree . ├── nginx │ ├── default.dev.conf │ ├── Dockerfile.dev │ └── wait.sh └── web-front 2 directories, 3 files以下の2つのファイルはバックエンドのときとほぼ同じです。
frontend/nginx/default.dev.confupstream react { server web-front:3000; } server { listen 80; location = /healthz { return 200; } location / { proxy_pass http://react; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect off; } location /sockjs-node { proxy_pass http://react; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }frontend/nginx/Dockerfile.devFROM nginx:1.17.4-alpine RUN apk add --no-cache bash COPY wait.sh /wait.sh RUN chmod +x /wait.sh CMD ["/wait.sh", "web-front:3000", "--", "nginx", "-g", "daemon off;"] RUN rm /etc/nginx/conf.d/default.conf COPY default.dev.conf /etc/nginx/conf.dこのまま進めると、reactのコンテナは毎回nginxより遅く立ち上がってしまい、nginxは接続エラーだと勘違いしてexitしてしまいます。それを阻止するために以下のシェルファイルを用意してnginxコンテナの立ち上げを遅らせます。こちらのファイルはvishnubob/wait-for-itのレポジトリからコピーしてきたものです。
wait.sh#!/usr/bin/env bash # Use this script to test if a given TCP host/port are available WAITFORIT_cmdname=${0##*/} echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } usage() { cat << USAGE >&2 Usage: $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] -h HOST | --host=HOST Host or IP under test -p PORT | --port=PORT TCP port under test Alternatively, you specify the host and port as host:port -s | --strict Only execute subcommand if the test succeeds -q | --quiet Don't output any status messages -t TIMEOUT | --timeout=TIMEOUT Timeout in seconds, zero for no timeout -- COMMAND ARGS Execute command with args after the test finishes USAGE exit 1 } wait_for() { if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" else echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" fi WAITFORIT_start_ts=$(date +%s) while : do if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then nc -z $WAITFORIT_HOST $WAITFORIT_PORT WAITFORIT_result=$? else (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 WAITFORIT_result=$? fi if [[ $WAITFORIT_result -eq 0 ]]; then WAITFORIT_end_ts=$(date +%s) echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" break fi sleep 1 done return $WAITFORIT_result } wait_for_wrapper() { # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 if [[ $WAITFORIT_QUIET -eq 1 ]]; then timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & else timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & fi WAITFORIT_PID=$! trap "kill -INT -$WAITFORIT_PID" INT wait $WAITFORIT_PID WAITFORIT_RESULT=$? if [[ $WAITFORIT_RESULT -ne 0 ]]; then echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" fi return $WAITFORIT_RESULT } # process arguments while [[ $# -gt 0 ]] do case "$1" in *:* ) WAITFORIT_hostport=(${1//:/ }) WAITFORIT_HOST=${WAITFORIT_hostport[0]} WAITFORIT_PORT=${WAITFORIT_hostport[1]} shift 1 ;; --child) WAITFORIT_CHILD=1 shift 1 ;; -q | --quiet) WAITFORIT_QUIET=1 shift 1 ;; -s | --strict) WAITFORIT_STRICT=1 shift 1 ;; -h) WAITFORIT_HOST="$2" if [[ $WAITFORIT_HOST == "" ]]; then break; fi shift 2 ;; --host=*) WAITFORIT_HOST="${1#*=}" shift 1 ;; -p) WAITFORIT_PORT="$2" if [[ $WAITFORIT_PORT == "" ]]; then break; fi shift 2 ;; --port=*) WAITFORIT_PORT="${1#*=}" shift 1 ;; -t) WAITFORIT_TIMEOUT="$2" if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi shift 2 ;; --timeout=*) WAITFORIT_TIMEOUT="${1#*=}" shift 1 ;; --) shift WAITFORIT_CLI=("$@") break ;; --help) usage ;; *) echoerr "Unknown argument: $1" usage ;; esac done if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then echoerr "Error: you need to provide a host and port to test." usage fi WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} # Check to see if timeout is from busybox? WAITFORIT_TIMEOUT_PATH=$(type -p timeout) WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) WAITFORIT_BUSYTIMEFLAG="" if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then WAITFORIT_ISBUSY=1 # Check if busybox timeout uses -t flag # (recent Alpine versions don't support -t anymore) if timeout &>/dev/stdout | grep -q -e '-t '; then WAITFORIT_BUSYTIMEFLAG="-t" fi else WAITFORIT_ISBUSY=0 fi if [[ $WAITFORIT_CHILD -gt 0 ]]; then wait_for WAITFORIT_RESULT=$? exit $WAITFORIT_RESULT else if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then wait_for_wrapper WAITFORIT_RESULT=$? else wait_for WAITFORIT_RESULT=$? fi fi if [[ $WAITFORIT_CLI != "" ]]; then if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" exit $WAITFORIT_RESULT fi exec "${WAITFORIT_CLI[@]}" else exit $WAITFORIT_RESULT fidocker-compose.ymlの編集
docker-compose.ymlversion: "3.7" services: web-back: container_name: python-backend env_file: ./backend/web-back/.env build: ./backend/web-back/. volumes: - ./backend/web-back:/code/ - static_volume:/code/staticfiles # <-- bind the static volume stdin_open: true tty: true command: gunicorn --bind :8000 config.wsgi:application networks: - backend_network environment: - CHOKIDAR_USEPOLLING=true - DJANGO_SETTINGS_MODULE=config.local_settings depends_on: - db backend-server: container_name: nginx_back build: context: ./backend/nginx/. dockerfile: Dockerfile.dev volumes: - static_volume:/code/staticfiles # <-- bind the static volume ports: - "8080:80" depends_on: - web-back networks: - backend_network db: build: ./mysql command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: todoList MYSQL_USER: user MYSQL_PASSWORD: password TZ: 'Asia/Tokyo' volumes: - ./mysql_volume:/var/lib/mysql - ./sql:/docker-entrypoint-initdb.d networks: - backend_network web-front: image: node:14.13.1 volumes: - ./frontend/web-front:/home/app/frontend ports: - 3000:3000 working_dir: /home/app/frontend command: [bash, -c, yarn upgrade --no-progress --network-timeout 1000000 && yarn run dev] networks: - frontend_network frontend-server: container_name: nginx_frontend build: context: ./frontend/nginx/. dockerfile: Dockerfile.dev ports: - "80:80" depends_on: - web-front networks: - frontend_network networks: backend_network: driver: bridge frontend_network: driver: bridge volumes: static_volume:これでファイルの用意はできました。
reactのプロジェクトの作成
docker-compose run --rm web-front sh -c "npx create-react-app ."web-frontはnode_modulesを除くと以下のようにプロジェクトができているはずです。
$ tree web-front -I node_modules web-front ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── README.md ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js └── yarn.lock 2 directories, 17 filesnext.jsのための準備
必要なモジュールを今のうちに入れておきましょう。
docker-compose run --rm web-front sh -c "yarn add next axios" docker-compose run --rm web-front sh -c "yarn add --dev typescript @types/react"
package.json
でdev
の項目を追加します。これがないとdev
が見つからないといってエラーになります。package.json"scripts": { "dev": "next dev", //追加 "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" },
pages
フォルダをsrc
の下に作って、テキトーなtypescriptファイルをおいてみます。next.jsではpagesの下にページを置くことがルールとなっています。pages/index.tsximport { FC } from 'react' export default (() => { return ( <div> hello world </div> ) }) as FCこれで
docker-compose up
してみましょう。
フロントエンドにアクセスするときは、localhost
だけで大丈夫です、ポート番号は必要ありません。hello world
と返されているのが見えるはずです。4. APIのデータを取得して表示
- index.tsxの編集
pages/index.tsximport React, { FC, useEffect, useState } from 'react' import axios, { AxiosInstance } from 'axios' type Todo = { id: string title: String body: String } export default (() => { const [todos, setTodo] = useState<Todo[]>([]) const getAPIData = async () => { let instance: AxiosInstance instance = axios.create({ baseURL: 'http://localhost:8080', }) try { const response = await instance.get('/api/') console.log(response?.data) const tododata = response?.data as Todo[] setTodo(tododata) } catch (error) { console.log(error) } } return ( <div> hello world <button onClick={getAPIData}>click</button> {todos.map((item) => ( <div key={item.id}> <h1>{item.title}</h1> <p>{item.body}</p> </div> ))} </div> ) }) as FC
localhost
にアクセスすると、以下のようにボタンが現れると思います。ボタンを押したらAPIを通してデータが取得されます。
CSSやらBootstrapなどを使っていないのでしょうもないものですが、一応フロントエンドとバックエンドで通信ができていることを確認できました!
とりあえずここまでにしておいて、今後K8sへのデプロイについての記事を書くかもしれません。
参考
ゼロからGKEにDjango+Reactをデプロイする(1)backendの開発 - Nginx + Django
wait-for-it.sh