Djangoでmodel.pyを分割しようと思い、以下の記事を読みました。記事のDjangoバージョンは1.4でしたが、1.9でも上記の方法で動作しました。
Djangoのアプリケーションでmodelsモジュールを複数ファイルに分割する - 偏った言語信者の垂れ流し
ただ、各ModelにMeta.app_label
を書くのが手間だったので調べてみたところ、Django1.7にて変更が入り、Meta.app_label
は不要になっていました。
その時に調べた内容などをメモとして残しておきます。
環境
また、今回は動作検証をテストコードで行ったため、以下のライブラリも使っています。
- pytest-django 2.9.1
- factory-boy 2.7.0
変更の確認
リリースノートおよびチケットに情報がありました。
- App-loading refactor - Django 1.7 release notes | Django documentation | Django
- #4470 (Separate models.py in multiple files) – Django
- #25127 (document how to define models in multiple modules) – Django
公式ドキュメントの記述も見てみます。
Django1.6
If a model exists outside of the standard models.py (for instance, if the app’s models are in submodules of myapp.models), the model must define which app it is part of:
Options.app_label - Model Meta options — Django 1.6.12.dev20160216120443 documentation
Django1.7
If a model exists outside of the standard locations (models.py or a models package in an app), the model must define which app it is part of:
Options.app_label - Model Meta options | Django documentation | Django
Django1.9
If a model is defined outside of an application in INSTALLED_APPS, it must declare which app it belongs to:
Options.app_label - Model Meta options | Django documentation | Django
Django1.9でさらに記述が変更されていたので、リリースノートを見てみます。
All models need to be defined inside an installed application or declare an explicit app_label. Furthermore, it isn’t possible to import them before their application is loaded. In particular, it isn’t possible to import models inside the root package of an application.
Features removed in 1.9 - Django 1.9 release notes | Django documentation | Django
Django1.9のInitialization processも確認してみます。
You must define or import all models in your application’s models.py or models/init.py. Otherwise, the application registry may not be fully populated at this point, which could cause the ORM to malfunction.
Initialization process - Applications | Django documentation | Django
models.py
やmodels/__init__.py
にあるModelがロードされるようです。
以上より、models.pyを分割した場合でも、各ModelのMetaクラスでapp_label
を書く必要はなさそうです。
app_label無しで実装・確認
実際になくても動作するのかを試してみます。
アプリの作成と必要なファイルの追加
D:\Sandbox\Django_separate_model_file-sample>virtualenv -p c:\python35-32\python.exe env D:\Sandbox\Django_separate_model_file-sample>env\Scripts\activate (env) D:\Sandbox\Django_separate_model_file-sample>pip install django pytest-django factory-boy (env) D:\Sandbox\Django_separate_model_file-sample>django-admin startproject myproject . (env) D:\Sandbox\Django_separate_model_file-sample>mkdir apps\myapp (env) D:\Sandbox\Django_separate_model_file-sample>python manage.py startapp myapp apps/myapp # Model用のディレクトリを作成 (env) D:\Sandbox\Django_separate_model_file-sample>mkdir apps\myapp\models # デフォルトで作成された models.py を削除 (env) D:\dev\sandbox\django_multi_models>del apps\myapp\models.py # 必要なModelの空ファイルを作成 (env) D:\dev\sandbox\django_multi_models>mkdir apps\myapp\models (env) D:\dev\sandbox\django_multi_models>type nul > apps\myapp\models\__init__.py (env) D:\dev\sandbox\django_multi_models>type nul > apps\myapp\models\author_model.py (env) D:\dev\sandbox\django_multi_models>type nul > apps\myapp\models\publication_model.py
ここまでのディレクトリ構成は以下の通りです。
D:\Sandbox\Django_separate_model_file-sample ├── env\ (virtualenv環境) │ └── ... ├── apps\ │ ├── myapp\ │ │ └── models\ │ │ ├─ __init__.py │ │ ├─ author_model.py │ │ └─ publication_model.py │ └── ... ├── myproject\ │ ├── settings.py │ └── ... ...
以下のようなModelをauthor_model.py
とpublication_model.py
へ分割することを試してみます。
from django.db import models class Publication(models.Model): title = models.CharField(max_length=30) class Author(models.Model): name = models.CharField(max_length=100) publications = models.ManyToManyField(Publication)
Model
publication_model.py
from django.db import models class Publication(models.Model): title = models.CharField(max_length=30)
author_model.py
from django.db import models from .publication_model import Publication class Author(models.Model): name = models.CharField(max_length=100) publications = models.ManyToManyField(Publication)
__init__.py
両方のModelをロードするため、__init__.py
も実装します。
__init__.py
from apps.myapp.models.author_model import * from apps.myapp.models.publication_model import *
なお、from .author_model import *
のように相対importで実装すると、migrateはできるものの、実行時に
RuntimeError: Model class myapp.models.publication_model.Publication doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS.
というエラーが出ます。
マイグレーション
makemigrationsやmigrationを実行してみたところ、問題なく認識・実行されました。
(env) D:\Sandbox\Django_separate_model_file-sample>python manage.py makemigrations Migrations for 'myapp': 0001_initial.py: - Create model Author - Create model Publication - Add field publications to author (env) D:\Sandbox\Django_separate_model_file-sample>python manage.py migrate Operations to perform: Apply all migrations: myapp, auth, sessions, contenttypes, admin Running migrations: Rendering model states... DONE ... Applying myapp.0001_initial... OK ...
動作テスト
今回はテストコードを書いて、問題なく動作するかを確認します。
factory-boyを使って、ModelのFactoryを作ります。
myapp\tests\factories.py
import factory from ..models import Publication, Author class PublicationFactory(factory.django.DjangoModelFactory): title = factory.Sequence(lambda n: "Title #%s" % n) class Meta: model = Publication class AuthorFactory(factory.django.DjangoModelFactory): name = factory.Sequence(lambda n: "Name #%s" % n) class Meta: model = Author @factory.post_generation def publications(self, create, extracted, **kwargs): if not create: return if extracted: for publication in extracted: self.publications.add(publication)
Factoryを使ったテストコードを書きます。
myapp\tests\test_models.py
from django.test import TestCase from .factories import PublicationFactory, AuthorFactory class Test_SeparateModelFile(TestCase): def test_CreateModelByFactory(self): pub1 = PublicationFactory() pub2 = PublicationFactory() pub3 = PublicationFactory() a = AuthorFactory.create( publications=(pub1, pub2, pub3) ) assert a.name == 'Name #0' assert a.publications.all().count() == 3 assert a.publications.all()[0].title == 'Title #0'
実行してみると、テストは問題なく通りました。
(env) D:\dev\sandbox\django_multi_models>py.test ... apps\myapp\tests\test_models.py . ========================== 1 passed in 2.13 seconds ===========================
以上より、Model.app_label
がなくても動作することが確認できました。
ソースコード
GitHubに上げておきました。
thinkAmi-sandbox/Django_separate_model_file-sample
なお、GitHubの方は、別アプリのModel(Affiliation)をManyToManyで参照しているパターンも書きましたが、こちらも問題なく動作しました。