[Django] Migrations vỡ lòng - Part 2

taiphuong
6 min readJun 1, 2019

Ở phần trước, như bạn đã biết migration là công cụ hữu ích và mạnh mẽ. Vậy ở phần này chúng ta đi sâu hơn 1 tí nhé.

Setup nhanh một Project Django

Xuyên suốt series này mìnhsẽ dùng Django để làm 1 example về Tracker Bitcoin. Mình sẽ sử dụng virtual environment trên macOS X

$ python3 -m venv env # install virtual environment
$ source env/bin/activate # active virtual environment.
(env) $ pip install "Django==2.1.*"
...
Successfully installed Django-2.1.3

Bây giờ bạn đã có 1 môi trường ảo python và tiếp tục tạo các project, app

$ django-admin.py startproject bitcoin_tracker
$ cd bitcoin_tracker
$ python manage.py startapp historical_data

Bạn sẽ có được cấu trúc thư mục như sau, mình có chú thích cho các bạn dễ hiểu

bitcoin_tracker/
|
├── bitcoin_tracker/ # chứa file giúp bạn setting cho dự án
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
|
├── historical_data/ # đây là 1 app của dự án.
│ ├── __init__.py
│ ├── admin.py # file dùng để khai báo trên giao diện admin.
│ ├── apps.py
│ ├── migrations/ # đây là file mình cần quan tâm.
│ │ └──__init__.py
| |
│ ├── models.py # file chứa models.
│ ├── tests.py # file này chắc ai cũng biết.
│ └── views.py # file này là view. còn tuỳ vào cách bạn sử dụng.
|
└── manage.py

Chúng ta bắt đầu với việc tạo 1 model.

#file: historical_data/models.py
class PriceHistory(models.Model):
date = models.DateTimeField(auto_now_add=True)
price = models.DecimalField(max_digits=7, decimal_places=2)
volume = models.PositiveIntegerField()

Cũng đừng quên rằng bạn phải khai báo app nào sẽ cài đặt như sau:

#file: bitcoin_tracker/settings.py
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'historical_data',
]
...

Nếu bạn không cài đặt việc kết nối tới l loại db nào khác thì mặc định sẽ dùng SQLite.

Tạo migrations

Sau khi tạo model, việc tiếp theo là migrations theo các command sau:

$ python manage.py makemigrations historical_data#việc makemigrations  
Migrations for 'historical_data':
historical_data/migrations/0001_initial.py
- Create model PriceHistory

khi bạn chỉ định app nào sẽ được migrations thì hay gọi tên nó, nếu không có giá trị nào migrations hiểu sẽ tất cả app sẽ được tạo migrations.

bitcoin_tracker/
|
├── bitcoin_tracker/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
|
├── historical_data/
│ ├── migrations/
│ │ ├── 0001_initial.py # file mới được tạo
│ │ └── __init__.py
| |
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
|
├── db.sqlite3
└── manage.py

Kiểm tra bằng dbshell cho SQLite như sau:

$ python manage.py dbshell
SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite> .tables
sqlite>

Hiện tại db đang rỗng. Vì chưa apply phần migrations này vào db.

Applying Migrations.

Sau khi tạo migrations, nhưng chưa apply bất kì sự thay đổi nào db vì vậy follow theo đoạn code bên dưới.

$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, historical_data, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying historical_data.0001_initial... OK
Applying sessions.0001_initial... OK

Có nhiều thứ đang diễn ra ở đây. Đây là những phần migrations được apply thành công. Nhưng chúng đến từ đâu?. Chúng đến từ settings trong INSTALLED_APPS.

Kiểm tra xem:

$ python manage.py dbshell
SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite> .tables
auth_group django_admin_log
auth_group_permissions django_content_type
auth_permission django_migrations
auth_user django_session
auth_user_groups historical_data_pricehistory
auth_user_user_permissions
sqlite>

Bây giờ bạn thấy có nhiều table chưa. Tên của nó được tạo từ tên app và tên bảng. Dùng .schema để xem bạn đã có những gì.

sqlite> .schema --indent historical_data_pricehistory
CREATE TABLE IF NOT EXISTS "historical_data_pricehistory"(
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"date" datetime NOT NULL,
"price" decimal NOT NULL,
"volume" integer unsigned NOT NULL
);

Giờ bạn migrate lần nữa.

$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, historical_data, sessions
Running migrations:
No migrations to apply.

Không có gì cả! Django nhớ cái nào đã được applied rồi và bạn không cần phải chạy lại nó.

Changing Models

Khi bạn thay đổi bất kì dữ liệu trên model, db table sẽ thay đổi theo. Nếu định nghĩa model của bạn không trùng khớp với db schema, thì đa phần bạn sẽ gặp lỗi: django.db.utils.OperationalError.

Test thay đổi trên model như sau:

class PriceHistory(models.Model):
date = models.DateTimeField(auto_now_add=True)
price = models.DecimalField(max_digits=7, decimal_places=2)
volume = models.DecimalField(max_digits=7, decimal_places=3)

Thử tạo migrations bằng lệnh makemigrations.

$ python manage.py makemigrations
Migrations for 'historical_data':
historical_data/migrations/0002_auto_20181112_1950.py
- Alter field volume on pricehistory

Tiếp tục migrate để câu lệnh thực thi vào db.

$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, historical_data, sessions
Running migrations:
Applying historical_data.0002_auto_20181112_1950... OK

Kiểm tra tại đây bằng dbshell

$ python manage.py dbshell
SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite> .schema --indent historical_data_pricehistory
CREATE TABLE IF NOT EXISTS "historical_data_pricehistory" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"date" datetime NOT NULL,
"price" decimal NOT NULL,
"volume" decimal NOT NULL. #thay đổi ở field này nè.
);

Listing Out Migrations

Nếu bạn muốn biết toàn bộ dự án có bao nhiêu migrations, follow it.

$ ./manage.py showmigrations
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
[X] 0003_logentry_add_action_flag_choices
auth
[X] 0001_initial
[X] 0002_alter_permission_name_max_length
[X] 0003_alter_user_email_max_length
[X] 0004_alter_user_username_opts
[X] 0005_alter_user_last_login_null
[X] 0006_require_contenttypes_0002
[X] 0007_alter_validators_add_error_messages
[X] 0008_alter_user_username_max_length
[X] 0009_alter_user_last_name_max_length
contenttypes
[X] 0001_initial
[X] 0002_remove_content_type_name
historical_data
[X] 0001_initial
[X] 0002_auto_20181112_1950
sessions
[X] 0001_initial

Dấu X là thể hiện phần đã được apply migrations. Ở ví dụ này, bạn có thể thấy được phần dự án từ khi bạn bắt đầu trên nền code sẵn có hoặc làm việc với team khi mà bạn không phải là người duy nhất thêm migrations.

Unapplying Migrations.

Giờ bạn đã biết cách thay đổi schema db bằng cách tạo và apply migrations. Tuy nhiên còn vài điểm cần lưu ý là khi bạn muốn undo, switch back về lại schema db trước đó. Bởi vì:

  • Bạn cần test migrations của đồng nghiệp.
  • Nhận ra rằng sự thay đổi của bạn là chưa hợp lý.
  • Làm việc với nhiều features với nhiều sự thay đổi song song.
  • Cần khôi phục lại bảng backup đã tạo trước đó.

May thay, migrations có thể hỗ trợ bạn. Trong nhiều trường hợp, sự ảnh hưởng của migrations có thể được undo bởi bạn chưa apply migrations.

Để không apply migrations, bạn phải gọi migrate với tên của app và tên của migration trước migration mà bạn không apply.

Nếu bạn muốn revert migration 0002_auto_20181112_1950 trong app historical_data, bạn phải pass 0001_initial như là một tham số.

$ python manage.py migrate historical_data 0001_initial
Operations to perform:
Target specific migration: 0001_initial, from historical_data
Running migrations:
Rendering model states... DONE
Unapplying historical_data.0002_auto_20181112_1950... OK

Migration vừa rồi đã revert, và không được apply. Việc unapply không có nghĩa là bạn remove file migration của nó. Tiếp theo bạn chạy lệnh migrate, thì phần migration sẽ chạy lại lần nữa.

Chú ý: Đừng quá bối rối với việc này, khi mà bạn dùng trên trình IDE yêu thích của bạn. Không phải tất cả hoạt động trên db có thể được revert hoàn toàn, nếu bạn chỉ remove 1 field từ model, hãy tạo migration và apply nó. Django sẽ remove tương ứng từ db. Việc unapply này sẽ tạo lại cột những sẽ không thay đổi dữ liệu được lưu ở cột đó.

Naming Migrations

Bạn có thể thay đổi tên migrations, Django tự động đặt tên cho migrations theo dạng timestamp. Nếu bạn không thích hay dùng arg là name

Một ví dụ:

$ rm historical_data/migrations/0002_auto_20181112_1950.py$ ./manage.py makemigrations historical_data --name switch_to_decimals

Đây là toàn bộ phần cơ bản của bài migrations vỡ lòng.

Phần tiếp theo sẽ đào sâu hơn về Migrations.

--

--