ShiftanのDjangoを用いたデータベース構築
こんにちは!Nineeenという学生開発チームの代表をしている古谷洸樹です。
今回はShiftanのデータベース構築がある程度終了したのでデータベースの中身について記事にしていきたいと思います。
自分たちの開発のアウトプットとして記事にしますが、実例として誰かの参考になっていただければ幸いです。
基本情報
Shiftanとは飲食店を想定した無料のシフト作成アプリ
開発環境はDockerでコンテナ化をしています。バックエンドをPythonのDjango、フロントエンドをReactでデータベースはMySQLです。
学生開発チームNineteenで学生5人で開発中
Shiftanのデータベース設計
さっそくですが、まずはデータベース設計のER図をご覧ください。
バイトのシフト作成アプリケーションということでデータベースは少し複雑になっています。今からは、ER図の一つ一つのテーブルについて解説をしていきながらまとめていきたいと思います。もしよければこのER図を見ながら読み進めていただきたいと思います。
データベース設計において自然キーではなく、すべて代理キーを採用しています。理由としてはただでさえややこしい設計なので少しでもSQLを簡潔にしたいからです。
Userテーブル
まずは、Userテーブルです。まず、Userには2種類のアカウントがあります。店長アカウントとアルバイトアカウントです。それぞれアカウントを登録するときの画面が分かれています。
これがShiftanのアカウント作成ページです。左が店長で右がアルバイトです。どちらのフォームから登録したかによってテーブルのマネージャーフラグがboolで切り替わります。店舗アカウントでは一緒にStoreテーブルの情報も入力してもらいます。
FKの店舗IDはSET_NULLにしておき、アカウント登録終了後にアルバイトの人は店舗登録を行います。
ログインはメールアドレスとパスワードを使うのでメールアドレスはuniqueにしてあります。
Storeテーブル
StoreテーブルではStoreIDをuniqueで作ります。
後々にStoreIDでアルバイトが店舗登録を行うからです。
Groupテーブル
Groupテーブルでは主にバイトのポジション分けなどで使用します。色は変更できるようになっているのでcolor要素があります。
Tmp_Work_Scheduleテーブル
これはバイトが募集がかけられている期間に対してのシフト希望を保存するテーブルです。
Shift_Rangeテーブル
このテーブルは店長がシフト希望の募集をかけるときにいつからいつまでのシフトを募集するかが保存されます。
Work_Scheduleテーブル
このテーブルには実際に店長が作成したシフトが入ります。
画面はまだ実装していません。
Schedule_Templateテーブル
これはデプロイ後の追加機能としてユーザーがよく入力する日時をテンプレートとして保存し、クリック一つで反映できるように準備しています。
Authorityテーブル
ER図右上の二つは店長とバイトの権限を分けるのに使おうと思っていましたが、Userテーブルのマネージャーフラグがあれば必要ないように感じたのでここでは書いていますが、実際には実装していません。
もし、Authorityテーブルで分けたほうが良い場合はご指摘いただけると幸いです。
models.pyの中身
次は実際のDjangoデータベースを構築した際のmodels.pyについてみていこうと思います。
from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.core.mail import send_mail
from django.utils import timezone
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.utils.translation import gettext_lazy as _ # 多言語対応するために使用されている関数
# Create your models here.
class UserManager(BaseUserManager): # emailは必須項目なので、emailがからの場合は例外が発生するように設定
use_in_migrations = True
# 通常ユーザー作成用のメソッド
def _create_user(self, username, email, password, **extra_fields):
if not email:
raise ValueError('Emailを入力して下さい')
email = self.normalize_email(email)
username = self.model.normalize_username(username)
user = self.model(username=username, email=email, **extra_fields)
user.set_password(password)
user.save(using=self.db) # 実際にユーザーを作成
return user
def create_user(self, username, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, username, email, password, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('is_staff=Trueである必要があります。')
if extra_fields.get('is_superuser') is not True:
raise ValueError('is_superuser=Trueである必要があります。')
return self._create_user(username, email, password, **extra_fields)
class Store(models.Model):
store_name = models.CharField("店舗名",max_length=100, unique=True, null=False, blank=False)
address = models.CharField("住所",max_length=100, null=False, blank=False)
phone = models.CharField("電話番号", max_length=15, null=False, blank=False)
store_ID = models.SlugField("店舗ID",max_length=50, unique=True, null=False, blank=False)
class Group(models.Model):
group_name = models.CharField("グループ名",max_length=50, null=False, blank=False)
color = models.SlugField("グループカラー",max_length=100, null=False, blank=False)
class User(AbstractBaseUser, PermissionsMixin):
username_validator = UnicodeUsernameValidator() # validatorとは入力チェック
username = models.CharField(_("username"), max_length=50, validators=[username_validator], blank=True)
email = models.EmailField(_("email_address"), unique=True) # emailでのログインとする
is_staff = models.BooleanField(_("staff status"), default=False) # 管理画面のアクセス可否
is_active = models.BooleanField(_("active"), default=True) # ログインの可否
is_manager = models.BooleanField("manager", default=False) # 店長かどうか
date_joined = models.DateTimeField(_("date joined"), default=timezone.now) # アカウントの作成日時
last_name = models.CharField("名字", max_length=50, null=False, blank=False)
first_name = models.CharField("名前", max_length=50, null=False, blank=False)
phone = models.CharField("電話番号", max_length=15, null=False, blank=False)
store_FK = models.ForeignKey(Store, on_delete=models.SET_NULL, null=True)
group_FK = models.ForeignKey(Group, on_delete=models.SET_NULL, null=True)
objects = UserManager() # views.pyでUserモデルの情報を取得する際などで利用
USERNAME_FIELD = "email" # ここをemailにすることでメールアドレスでのログインが可能になる
EMAIL_FIELD = "email"
REQUIRED_FIELDS = ['username']
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
def email_user(self, subject, message, from_email=None, **kwargs):
send_mail(subject, message, from_email, [self.email], **kwargs)
# class Authority(models.Model):
# authority_name = models.CharField("権限名",max_length=50)
# class User_Authority(models.Model):
# user_FK = models.ForeignKey(User, on_delete=models.DO_NOTHING)
# authority_FK = models.ForeignKey(Authority, on_delete=models.DO_NOTHING)
# authority = models.BooleanField() #初期値False
class Tmp_Work_Schedule(models.Model):
start_time = models.DateTimeField("シフト希望開始時間",auto_now=False, auto_now_add=False)
stop_time = models.DateTimeField("シフト希望終了時間",auto_now=False, auto_now_add=False)
create_time = models.DateTimeField("シフト希望提出時間",auto_now=True, auto_now_add=False)
update_time = models.DateTimeField("シフト希望更新時間",auto_now=False, auto_now_add=True)
user_FK = models.ForeignKey(User, on_delete=models.CASCADE)
class Work_Schedule(models.Model):
start_time = models.DateTimeField("バイト開始時間",auto_now=False, auto_now_add=False)
stop_time = models.DateTimeField("バイト終了時間",auto_now=False, auto_now_add=False)
create_time = models.DateTimeField("シフト希望提出時間",auto_now=True, auto_now_add=False)
update_time = models.DateTimeField("シフト希望更新時間",auto_now=False, auto_now_add=True)
user_FK = models.ForeignKey(User, on_delete=models.CASCADE)
class Shift_Range(models.Model):
shift_name = models.CharField("シフト名",max_length=100, null=False, blank=False)
start_date = models.DateField("募集開始日",auto_now=False, auto_now_add=False)
stop_date = models.DateField("募集終了日",auto_now=False, auto_now_add=False)
create_time = models.DateTimeField("シフト作成時間",auto_now=True, auto_now_add=False)
update_time = models.DateTimeField("シフト更新時間",auto_now=False, auto_now_add=True)
class Schedule_Template(models.Model):
start_time = models.DateTimeField("シフトテンプレ開始時間",auto_now=False, auto_now_add=False)
stop_time = models.DateTimeField("シフトテンプレ終了時間",auto_now=False, auto_now_add=False)
user_FK = models.ForeignKey(User, on_delete=models.CASCADE)
まず、userモデルについてですが、DjangoのAbstractBaseUserを継承して作成しました。参考にしたサイトはこちらです。
カスタムUserモデルではユーザーIDを通じて認証を行うのですが、ShiftanではユーザーIDはなく、メールアドレスで認証を行うのでそこを変更するために少し難しい方法でユーザーモデルを作成しました。
正直この部分は自信がないのであまり鵜吞みにしないようにお願いします。
(もし間違っていたらご指摘いただけると幸いです。)
私が行ったことは上の記事とほとんど同じですので、そちらをご覧ください。
ここでは外部キーについて触れていこうと思います。最初、外部キーは結構わけが分からなかったのですが、リレーションが1対多のものに関しては基本的に必要という理解をしました。外部キーの決め方については今後また詳しく記事にしてみたいと思います。
ShiftanではUserモデルが一番かなめになっているのでUserの中の外部キーは基本on.deleteをSET_NULLにしています。これでグループが消去されたりしてもUserが消えることはなくなります。
Tmp_Work_ScheduleとWork_Scheduleに関してはUserIDの外部キーはon.deleteをDO_NOTHINGとしてあります。これは、ユーザーが消えたときに今まで作成したシフトからも一緒に名前やデータが消えてしまうと困るからです。
他はすべてon.deleteをCASCADEとしてあります。
migrateした後
mysql> show tables
-> ;
+-------------------------------+
| Tables_in_django-db |
+-------------------------------+
| auth_group |
| auth_group_permissions |
| auth_permission |
| django_admin_log |
| django_content_type |
| django_migrations |
| django_session |
| shiftan_group |
| shiftan_schedule_template |
| shiftan_shift_range |
| shiftan_store |
| shiftan_tmp_work_schedule |
| shiftan_user |
| shiftan_user_groups |
| shiftan_user_user_permissions |
| shiftan_work_schedule |
+-------------------------------+
これがmigrateした後の実際のテーブルです。一応ですが、テーブルはすべてそろっていたので安心しました。しかし、管理画面の内容を見ると若干不安になります。それはUserモデルにはいくつもの項目があるのに管理画面には表示されていないという事です。
入力するのはメールアドレスとパスワードです。
これが実際の画面です。
ここでユーザー追加したのちに電話番号とか姓、名を入力する画面が現れると思ったのですが実際にはユーザー名の入力しかでてきませんでした。
ここはもう少し調べる必要があるなと思いました。
最後に
今回の記事は自分の行ったことの記録のようなものであり、文章も拙く、まだまだ理解不足な部分がたくさんあると思います。なので、間違いを見つけたり、理解が進み次第この記事を更新していこうと思います。
Nineteenの活動に興味がわいたという方はフォローやスキをいただけると幸いです。
ここまで読んでくださりありがとうございました。