Django使用心得(一) 善用migrations

elfgzp發表於2018-12-18

Django使用心得(一) 善用migrations

在使用和學習Django框架時,發現很多人包括我自己在對Django專案進行版本管理時,通常把migrations檔案新增到了.gitignore中。

筆者也一直有疑問這種做法是否正確,於是去檢視官方文件,找到以下這段。

原文:

You should think of migrations as a version control system for your database schema. makemigrations is responsible for packaging up your model changes into individual migration files - analogous to commits - and migrate is responsible for applying those to your database.

The migration files for each app live in a “migrations” directory inside of that app, and are designed to be committed to, and distributed as part of, its codebase. You should be making them once on your development machine and then running the same migrations on your colleagues’ machines, your staging machines, and eventually your production machines.

中文翻譯:

你可以想象 migrations 相當一個你的資料庫的一個版本控制系統。makemigrations 命令負責儲存你的模型變化到一個遷移檔案 - 和 commits 很類似 - 同時 migrate負責將改變提交到資料庫。

每個 app 的遷移檔案會儲存到每個相應 app 的“migrations”資料夾裡面,並且準備如何去執行它, 作為一個分散式程式碼庫。 每當在你的開發機器或是你同事的機器並且最終在你的生產機器上執行同樣的遷移,你應當再建立這些檔案。

根據官方文件的說法,不將migrations提交到倉庫的做法時錯誤的。

而且如果要使用django自帶的封裝好的TestCase進行單元測試,migrations也必須保留。

下一篇文章筆者也會介紹一下django中的TestCase的使用心得。

下面介紹一下,在專案中migrations的一些使用心得和遇到的一些問題。

利用migrations初始化資料

我們現在有一個Book的模型,我想在migrate之後初始化一些資料。

make_good_use_of_migrations/models.py view raw
class Book(models.Model):
    name = models.CharField(max_length=32)複製程式碼

例如:生成三本名稱分別為HamletTempestThe Little Prince的書。

在執行了python manage.py makemigrations之後migrations資料夾會生成0001_initial.py的檔案。

檔案中包含了Book這個模型初始化的一些程式碼。

在介紹如何利用migrations初始化資料時,先介紹一下migrations常用的兩個操作:

RunSQLRunPython

顧名思義分別是執行SQL語句Python函式

下面我用migrations中的RunPython來初始化資料。

  1. 在相應app下的migrations檔案新建0002_init_book_data.py migrations/.
    ​ ├── 0001_initial.py.
    ​ └── 0002_init_book_data.py.

  2. 然後增加Migration類繼承django.db.migrations.Migration,並在operations中增加需要執行的程式碼。

make_good_use_of_migrations/migrations/0002_init_book_data.py view raw
from django.db import migrations

"""
make_good_use_of_migrations 是App的名字
"""


def init_book_data(apps, schema_editor):
    Book = apps.get_model('make_good_use_of_migrations', 'Book')
    init_data = ['Hamlet', 'Tempest', 'The Little Prince']
    for name in init_data:
        book = Book(name=name)
        book.save()


class Migration(migrations.Migration):
    dependencies = [
        ('make_good_use_of_migrations', '0001_initial'),
    ]

    # 這裡要注意dependencies為上一次migrations的檔名稱

    operations = [
        migrations.RunPython(init_book_data)
    ]複製程式碼
  1. 執行python manage.py migrate,可以看到資料已經在資料庫中生成了。

利用migrations修復資料

我們常常遇到這種情況,例如我需要給Book模型增加一個外來鍵欄位,而且這個欄位不能為空,所以舊的資料就要進行處理修復,我們可以這樣處理。

  1. 先將需要增加的欄位null屬性設定為True,然後執行makemigrations
make_good_use_of_migrations/models.py view raw
class Author(models.Model):
    name = models.CharField(max_length=32)


class Book(models.Model):
    name = models.CharField(max_length=32)
    author = models.ForeignKey(to=Author, on_delete=models.CASCADE, null=False)複製程式碼
  1. 在相應app下的migrations檔案新建0004_fix_book_data.py migrations/.
    ├── 0001_initial.py.
    ├── 0002_init_book_data.py.
    ├── 0003_auto_20181204_0533.py.
    └── 0004_fix_book_data.py.
make_good_use_of_migrations/migrations/0004_fix_book_data.py view raw
from django.db import migrations


def fix_book_data(apps, schema_editor):
    Book = apps.get_model('make_good_use_of_migrations', 'Book')
    Author = apps.get_model('make_good_use_of_migrations', 'Author')
    for book in Book.objects.all():
        author, _ = Author.objects.get_or_create(name='%s author' % book.name)
        book.author = author
        book.save()


class Migration(migrations.Migration):
    dependencies = [
        ('make_good_use_of_migrations', '0003_auto_20181204_0533'),
    ]

    operations = [
        migrations.RunPython(fix_book_data)
    ]複製程式碼
  1. 最後再將Book模型中的author欄位屬性null設為False,並執行makemigrations。執行後會出現,

    You are trying to change the nullable field 'author' on book to non-nullable without a default; we can't do that (the database needs something to populate existing rows).
    Please select a fix:
     1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
     2) Ignore for now, and let me handle existing rows with NULL myself (e.g. because you added a RunPython or RunSQL operation to handle NULL values in a previous data migration)
     3) Quit, and let me add a default in models.py
    Select an option:
    複製程式碼

    這裡選擇第2項,意思是忽略該欄位已經為空的資料,使用RunPython或者RunSQL自行處理。

    選擇完成後在執行python manage.py migrate,會發現資料庫中的資料會按照我們的預期處理完成。

解決多人開發時migrations產生的衝突

為了模擬多人多分支開發,新建一個master-2的分支,並且版本在建立Author類之前,並且在Book模型中增加remark欄位。

model.py檔案中的內容如下:

make_good_use_of_migrations/models.py view raw
class Book(models.Model):
    name = models.CharField(max_length=32)
    remark = models.CharField(max_length=32, null=True)複製程式碼

migrations檔案目錄如下:

migrations/.
├── 0001_initial.py.
├── 0002_init_book_data.py.
└──0003_book_remark.py.

當我們把master-2的程式碼合併到master時,會發現migrations中出現了重複的編號0003並且他們共同依賴於0002_init_book_data

migrations/.
├── 0001_initial.py.
├── 0002_init_book_data.py.
├── 0003_auto_20181204_0533.py
├── 0003_book_remark.py
├── 0004_fix_book_data.py
└──0005_auto_20181204_0610.py.

這時候就需要用到命令:

python manage.py makemigrations --merge
複製程式碼

然後就會在migrations目錄生成一個0006_merge_20181204_0622.py檔案

make_good_use_of_migrations/migrations/0006_merge_20181204_0622.py view raw
from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ('make_good_use_of_migrations', '0005_auto_20181204_0610'),
        ('make_good_use_of_migrations', '0003_book_remark'),
    ]

    operations = [
    ]複製程式碼

這時候在執行python manage.py migrate就可以了。

使用migrations.RunPython需要注意的問題

在函式中是無法呼叫模型類的函式的

假設在Book模型中定義了兩個函式print_name和類函式print_class_name

make_good_use_of_migrations/models.py view raw
class Book(models.Model):
    name = models.CharField(max_length=32)
    author = models.ForeignKey(to=Author, on_delete=models.CASCADE, null=False)
    remark = models.CharField(max_length=32, null=True)

    def print_name(self):
        print(self.name)

    @classmethod
    def print_class_name(cls):
        print(cls.__name__)複製程式碼

migrations中是無法呼叫的,筆者也沒有仔細研究,推測是Book類初始化時只把欄位初始化了。

make_good_use_of_migrations/migrations/0004_fix_book_data.py view raw
from django.db import migrations


def fix_book_data(apps, schema_editor):
    Book = apps.get_model('make_good_use_of_migrations', 'Book')
    Author = apps.get_model('make_good_use_of_migrations', 'Author')
    for book in Book.objects.all():
        author, _ = Author.objects.get_or_create(name='%s author' % book.name)
        book.author = author
        """
        book.print_name()
        book.print_class_name()
        這樣呼叫會報錯
        """
        book.save()


class Migration(migrations.Migration):
    dependencies = [
        ('make_good_use_of_migrations', '0003_auto_20181204_0533'),
    ]

    operations = [
        migrations.RunPython(fix_book_data)
    ]複製程式碼

在函式中模型的類所重寫的save方法無效,包括繼承的save方法

migrations中所有重寫的save方法都不會執行,例如:

make_good_use_of_migrations/models.py view raw
class Book(models.Model):
    name = models.CharField(max_length=32)
    author = models.ForeignKey(to=Author, on_delete=models.CASCADE, null=False)
    remark = models.CharField(max_length=32, null=True)

    def print_name(self):
        print(self.name)

    @classmethod
    def print_class_name(cls):
        print(cls.__name__)

    def save(self, *args, **kwargs):
        if not self.remark:
            self.remark = 'This is a book.'複製程式碼

最後初始化生成的資料的remark欄位的值仍然為空。

在函式中模型註冊的所有signal無效

雖然給Book模型註冊了signal,但是在migrations中仍然不會起作用

make_good_use_of_migrations/models.py view raw
@receiver(pre_save, sender=Book)
def generate_book_remark(sender, instance, *args, **kwargs):
    print(instance)
    if not instance.remark:
        instance.remark = 'This is a book.'複製程式碼

不要將資料處理放到模型變更的migrations檔案中

在做資料修復或者生成初始化資料時,不要將處理函式放到自動生成的變更或生成欄位、模型的migrations檔案中,例如:

make_good_use_of_migrations/migrations/0005_auto_20181204_0610.py view raw
class Migration(migrations.Migration):
    dependencies = [
        ('make_good_use_of_migrations', '0004_fix_book_data'),
    ]

    operations = [
        migrations.AlterField(
            model_name='book',
            name='author',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
                                    to='make_good_use_of_migrations.Author'),
        ),
        """
        migrations.RunPython(xxx) 不要把資料處理放到模型變更中
        """
    ]複製程式碼

不要放在一起的主要原因是,當RunPython中函式的處理邏輯一旦出現異常無法向下執行,

django_migrations將不會記錄這一次處理,但是表結構的變更已經執行了!

這也是Django migrations做的不好的地方,正確應該是出現異常需要做資料庫回滾。

一旦出現這種情況,只能手動將migrations的名稱如0005_auto_20181204_0610,寫入到資料庫表django_migrations中,然後將RunPython中的邏輯單獨剝離出來。

總結

以上就是筆者在專案中使用Django框架的migrations的心得,下一篇會介紹Django框架的TestCase

本文的原始碼會放到github上,github.com/elfgzp/djan…

本人部落格原文地址:elfgzp.cn/2018/12/04/…


相關文章