在使用和學習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
之後初始化一些資料。
class Book(models.Model):
name = models.CharField(max_length=32)複製程式碼
例如:生成三本名稱分別為Hamlet
、Tempest
、The Little Prince
的書。
在執行了python manage.py makemigrations
之後migrations
資料夾會生成0001_initial.py
的檔案。
檔案中包含了Book
這個模型初始化的一些程式碼。
在介紹如何利用migrations
初始化資料時,先介紹一下migrations
常用的兩個操作:
RunSQL
、RunPython
顧名思義分別是執行SQL語句
和Python函式
。
下面我用migrations
中的RunPython
來初始化資料。
在相應app下的
migrations
檔案新建0002_init_book_data.py
migrations/.
├── 0001_initial.py.
└── 0002_init_book_data.py.然後增加
Migration
類繼承django.db.migrations.Migration
,並在operations
中增加需要執行的程式碼。
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)
]複製程式碼
- 執行
python manage.py migrate
,可以看到資料已經在資料庫中生成了。
利用migrations修復資料
我們常常遇到這種情況,例如我需要給Book
模型增加一個外來鍵欄位,而且這個欄位不能為空,所以舊的資料就要進行處理修復,我們可以這樣處理。
- 先將需要增加的欄位
null
屬性設定為True
,然後執行makemigrations
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)複製程式碼
- 在相應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.
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)
]複製程式碼
最後再將
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
檔案中的內容如下:
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
檔案
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
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
類初始化時只把欄位初始化了。
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
方法都不會執行,例如:
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
中仍然不會起作用
@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
檔案中,例如:
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/…