Django1.7から、models.pyの分割方法に関する変更が入ってた

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

 

変更の確認

リリースノートおよびチケットに情報がありました。

 
公式ドキュメントの記述も見てみます。

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.pymodels/__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.pypublication_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で参照しているパターンも書きましたが、こちらも問題なく動作しました。