前置き
最近はR&Dで働く葛です。お久しぶりです。
小さなWebアプリを小さく初めて、試作品をたくさん作りたいと思っているのにあんまりフレームワークがないなと思っていました。
なんでもいいのでFrontend, Backend, Databaseをフルカスタマイズ可能で小さくサクッと始めるために今回作ってみました。
やったこと
- Frontend + Backend + Database のコンテナを立ててるDockerComposeを造りました。
- Frontendは Vite + React + TypeScriptです。(これは viteでプロジェクト建てただけ)
- Backendは Pythonの FastAPIです。
- Databaseは MySQLです。
環境
Windows11+WSL+Ubuntu 22.03です。
ディレクトリ構成
$ tree -L 2 . ├── backend │ ├── Dockerfile │ ├── README.md │ ├── __pycache__ │ ├── config │ ├── database │ ├── main.py │ ├── models │ ├── requirements.txt │ ├── routers │ └── services ├── docker-compose.yml ├── docs └── frontend ├── Dockerfile ├── README.md ├── index.html ├── node_modules ├── package-lock.json ├── package.json ├── public ├── src ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts
docker-compose.yml
以下はとりあえずローカルで適当に動かすためだけに作りました。SQLのパスワードなどを直書きしているので、実運用させたい場合は、環境変数化したりして適宜変更が必要だと思います。
services: backend: build: ./backend ports: - "8000:8000" depends_on: - db volumes: - ./backend:/app db: image: mysql:8.0 command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: sample_password MYSQL_USER: sample_user MYSQL_PASSWORD: sample_user_password frontend: build: ./frontend ports: - "5173:5173" volumes: - ./frontend:/app
説明
- frontendは port 5173でlistenさせています。
- backendは port 8000でlistenさせています。
- databaseはport 3306で自動的にlistenされていました。
- それぞれマウントさせて、Dockerコンテナ内での開発内容をローカルに反映させています。
Dockerfile
Frontend
# Use an official Node.js image FROM node:20 # Set the working directory in the container to /app WORKDIR /app # Add the current directory contents into the container at /app ADD . /app # Install any needed packages specified in package.json RUN npm install # Run npm start when the container launche # CMD ["npm", "run", "dev"] # 開発時に起動させるためだけのコマンド CMD [ "tail", "-f", "/dev/null" ]
説明
- 最初にプロジェクトだけをviteで作るために、VSCodeの機能でdev containerを立てました。そのなかで viteを使ってプロジェクトを立ち上げました。
- プロジェクトのソースコードだけを作るのはcontainer内でやる必要がなかったかも。
- プロジェクトの作ったら、とりあえずコンテナを閉じました。
- CMDで npm run devをしてしまうと、開発中にコンテナを閉じたり建てたりを繰り返す必要が出てきます。これは非常に手間です。
- そこで 最後の行を追加して、何もしない tail コマンドを実行させて待機させます。その間に vscodeの
attach container
を使って起動中のコンテナに入ることで開発ができます。これが便利でした。
- そこで 最後の行を追加して、何もしない tail コマンドを実行させて待機させます。その間に vscodeの
Backend
FROM python:3.11 WORKDIR /app COPY . /app/ RUN pip install --no-cache-dir -r requirements.txt # CMD [ "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload" ] CMD [ "tail", "-f", "/dev/null" ]
ほぼやっていることは一緒です。
あれ、COPY っているんだっけ?(また調べます)
DatabaseとBackendの連携について
おそらくここが鬼門だったと思います。
最初にフレームワークの説明をします。
├── backend │ ├── config : 固定値などを入れる │ ├── database : SQL Alchemey │ ├── main.py : エントリーポイント │ ├── models : PyDantic │ ├── requirements.txt │ ├── routers : エンドポイント │ └── services : 業務ロジック
- SQL ALchemyを利用してDatabaseを定義します。またSQLを操作します。
- FastAPIでのデータのやりとりはPyDanticを利用します。
- データの連結イメージは以下です
- Database <-[SQL Alchemy]-> FastAPI <-[PyDantic]-> Frontend
main.py
# main.py from fastapi import FastAPI from routers.index_router import index_router from routers.user_router import user_router from models.base import Base from database.database import engine app = FastAPI() @app.on_event("startup") async def startup_event(): Base.metadata.create_all(engine) app.include_router(index_router) app.include_router(user_router)
FASTAPI のエントリーポイントは上のように書きました。最初にFastAPIを初期化します。
database.py
# database.py from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker DATABASE_URL = "mysql://root:sample_password@db:3306" # Link Docker Compose ENVIROMENT VARIABLE DATABASE_NAME = "new_database" # DB名を指定せずに接続する engine = create_engine(f"{DATABASE_URL}") # DBがなければ作成する with engine.connect() as connection: connection.execute(text(f"CREATE DATABASE IF NOT EXISTS {DATABASE_NAME}")) # 切断する engine.dispose() # 新しく作成したDBに再接続する engine = create_engine(f"{DATABASE_URL}/{DATABASE_NAME}") # SessionLocalクラスを作成 SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
database.pyではDBの初期化やセッション、接続などの基本部分を書きました。
DATABASEとの接続する際はDocker Networkを使うのでそこに合わせて使います。
base.py
from sqlalchemy.ext.declarative import declarative_base Base = declarative_base()
スキーマ定義のベース定義です。これを継承して各スキーマを定義していきます。
database.user_schema.py
from sqlalchemy import Column, Integer, String from sqlalchemy.ext.declarative import declarative_base from models.base import Base class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) name = Column(String(50)) email = Column(String(50))
Userというテーブルを作ります。
models.user_model.py
from pydantic import BaseModel class UserBase(BaseModel): name: str email: str class UserCreate(UserBase): name: str email: str class UserRead(UserBase): id: int name: str email: str class UserUpdate(UserBase): id: int name: str email: str
UserクラスにそれぞれCRUDに対応したクラスを定義しました。
これを使ってEndpointを定義していきます。
routers.user_router.py
# user_router.py from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from models.user_model import UserCreate, UserRead, UserUpdate from database.database import SessionLocal from database.user_schema import User as UserSchema user_router = APIRouter() # 依存関係 def get_db(): db = SessionLocal() try: yield db finally: db.close() @user_router.post("/users/", response_model=UserCreate) def create_user(request_user: UserCreate, db: Session = Depends(get_db)): user_schema = UserSchema() user_schema.name = request_user.name user_schema.email = request_user.email db.add(user_schema) db.commit() db.refresh(user_schema) return user_schema @user_router.get("/users/{user_id}", response_model=UserRead) def read_user(user_id: int, db: Session = Depends(get_db)): user = db.query(UserSchema).filter(UserSchema.id == user_id).first() return user @user_router.put("/users/{user_id}") def update_user(user_id: int, user: UserUpdate, db: Session = Depends(get_db)): db_user = db.query(UserSchema).filter(UserSchema.id == user_id).first() db_user.name = user.name db_user.email = user.email db.commit() db.refresh(db_user) return db_user @user_router.delete("/users/{user_id}") def delete_user(user_id: int, db: Session = Depends(get_db)): user = db.query(UserSchema).filter(UserSchema.id == user_id).first() db.delete(user) db.commit() return {"message": "User deleted successfully"}
CRUDのエンドポイントを作ってみました。 それぞれ PyDanticで受け取ったデータをORMでSQL Alchemryに変換してDBとやりとりします。
以上で実装は終わりです。
CURLで動作確認してみる。
- ユーザーの作成(POST /users/):
curl -X POST "http://localhost:8000/users/" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"name\":\"string\",\"email\":\"user@example.com\"}"
- ユーザーの読み取り(GET /users/{user_id}):
curl -X GET "http://localhost:8000/users/1" -H "accept: application/json"
- ユーザーの更新(PUT /users/{user_id}):
curl -X PUT "http://localhost:8000/users/1" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"id\":1,\"name\":\"new_name\",\"email\":\"new_email@example.com\"}"
- ユーザーの削除(DELETE /users/{user_id}):
curl -X DELETE "http://localhost:8000/users/1" -H "accept: application/json"
DockerのSQLに入って確認してみる
MySQL Docker
docker compose run db mysql -h db -u root -psample_password
上は本来非推奨 パスワード直打ちだからです。
-h db
オプションを追加したことで、MySQLクライアントはDocker Network経由でMySQLサーバーに接続しようとしました。これは、Docker環境では各コンテナが独自のネットワーク空間を持っているため、localhostや127.0.0.1ではなく、サービス名(この場合はdb)を使用して他のコンテナに接続する必要があるからです。
あとは作成したDBを適宜確認すればOKです。
最後に
今回は上のようなものを作ってみました。
BACKENDとDBの骨子ができれば、あとはFrontendにUIを作って、JSONを描画させて、BACKENDに業務ロジックを実装するだけですね。
おそらくはもっと考慮すべき事項もあると思いますが、今回はこれくらいにします。
何か気になった指摘があればください。