« ^ »

Djangoのマイグレーションファイルを効率良くスカッシュする

2023/1/21 更新
約 4分 で読める

Djangoはデータベースのスキーマ更新のために、マイグレーションファイルを作成し管理する。 manage.pymakemigrations というサブコマンドを提供しており、マイグレーションファイルとモデル定義を元にスキーマの変更を検知し、必要なマイグレーションファイルを生成する。開発中にモデル定義を変更した場合、次のコマンドでマイグレーションファイルを生成し、それを適応することで、データベースのスキーマ変更を行う。

  1. python manage.py makemigrations
  2. python manage.py migrate

複数のトピックの開発が平行して進む場合、このマイグレーションファイルが、ブランチによって分岐してしまうことがある。例えばログイン機能とカート機能を実装しているEコマースサイトを考える。ログイン機能とカート機能はそれぞれ異なるブランチで開発しており、それぞれにモデル定義の更新が必要になった。ログイン機能ブランチとカート機能ブランチは、それぞれにマイグレーションファイルを生成した。Djangoのマイグレーションファイルには依存関係が記述されている。その依存関係には他のDjangoアプリケーションのマイグレーションも記述されるため、他のDjangoアプリケーションに依存するマイグレーションファイルが増えると、依存関係も複雑になる。


Main Branch
     |
     |
+----+----+
| Commit  |
+----+----+
     |
     +-----------------+-------------------+
     |                 |                   |
+----+----+            |Login              |Cart
| Commit  |            |Branch             |Banch
+----+----+            |                   |
     |                 |                   |
                  +----+----+         +----+----+
                  | Commit  |         | Commit  |<- Add
                  +----+----+         +----+----+   a migration
                       |                   |
                       |                   |
                  +----+----+
                  | Commit  |<- Add
                  +---------+   a migration
                       |
                       |
                  +----+----+
                  | Commit  |<- Add
                  +---------+   a migration
                       |
                       |
                  +----+----+
                  | Commit  |<- Add
                  +---------+   a migration

Djangoはこういった問題のために squashmigrations を提供している。 squashmigrations は指定した複数のマイグレーションファイルを1つにまとめる。ただしスカッシュ対象のマイグレーションに依存するマイグレーションが他にあったとしても、スカッシュできるのであれば実行し、依存関係の設定を組替えることはしない。そのため依存関係グラフが破綻する事がある。大きなコードベースの場合、この辻褄合わせの作業が結構大変だ。そこでスカッシュ対象に依存しているマイグレーションがどこかに存在するのか、どこからどこまでをスカッシュするのか、スカッシュしたファイルの後始末などの操作を簡略化する事にした。

マイグレーションファイルはmigrationsディレクトリ配下に 0002_xxx.py のようなファイル名で保存されている。Emacsにはdiredという良いファイラがあるので、そこでスカッシュ対象のファイルを選択できるように拡張することにした。またスカッシュ対象マイグレーションが所属するDjangoアプリケーションに依存したマイグレーションファイルを検索するコマンドも実装した。

(defcustom django-migration-squash-python-command "python" "")
(defcustom django-migration-module-directory "." "")
(defcustom django-migration-squash-database-name "testing" "")
(defvar django-migration-squash-app-migration-alist nil)
(defvar django-migration-squash-buffer-name "*DJANGO SQUASH*")
(defvar django-migration-squash-retval nil)
(setq django-migration-dependencies-buffer-name "*DJANGO MIGRATION DEPS*")
(setq django-migration-dependencies-buffer-name-temp "*TESTING*")

(defun django-migration-squash-convert-to-app-migration-alist (files)
  (interactive)
  (let* ((last-file (car (seq-reverse files))))
    `((migration-begin . ,(if (eq 1 (seq-length files))
			      "" (car (seq-reverse (string-split (car files) "/")))))
      (migration-end . ,(car (seq-reverse (string-split last-file "/"))))
      (app-label . ,(nth 2 (seq-reverse (string-split last-file "/")))))))

(defun django-migration-squash-build-squash-command (app-migration-alist)
  (format "%s manage.py squashmigrations --noinput %s %s %s"
	  django-migration-squash-python-command
	  (cdr (assoc 'app-label app-migration-alist))
	  (file-name-base (cdr (assoc 'migration-begin app-migration-alist)))
	  (file-name-base (cdr (assoc 'migration-end app-migration-alist)))))

;;;###autoload
(defun django-migration-create-database ()
  (interactive)
  (sql-send-string
   (format "DROP DATABASE IF EXISTS %s;" django-migration-squash-database-name))

  (sql-send-string
   (format "CREATE DATABASE %s DEFAULT CHARACTER SET utf8mb4;" django-migration-squash-database-name)))

;;;###autoload
(defun django-migration-select ()
  (interactive)
  (setq django-migration-squash-app-migration-alist
	(django-migration-squash-convert-to-app-migration-alist
	 (dired-get-marked-files))))

;;;###autoload
(defun django-migration-squash ()
  (interactive)
  (let ((vterm-shell (format "%s manage.py squashmigrations --noinput %s %s %s"
			     django-migration-squash-python-command
			     (cdr (assoc 'app-label django-migration-squash-app-migration-alist))
			     (file-name-base (cdr (assoc 'migration-begin django-migration-squash-app-migration-alist)))
			     (file-name-base (cdr (assoc 'migration-end django-migration-squash-app-migration-alist)))))
	(vterm-buffer-name django-migration-squash-buffer-name)
	(vterm-kill-buffer-on-exit nil))
    (vterm)))


;;;###autoload
(defun django-migration-delete-squash ()
  (interactive)
  (let ((vterm-shell (format "%s manage.py delete_squashed_migrations --noinput %s"
			     django-migration-squash-python-command
			     (cdr (assoc 'app-label django-migration-squash-app-migration-alist))))
	(vterm-buffer-name django-migration-squash-buffer-name)
	(vterm-kill-buffer-on-exit nil))
    (vterm)))

(defun django-migration-dependencies-format-buffer ()
  (interactive)
  (with-current-buffer (get-buffer-create django-migration-dependencies-buffer-name)
    (insert (with-current-buffer
		(get-buffer django-migration-dependencies-buffer-name-temp)
	      (buffer-substring-no-properties (point-min) (point-max))))
    (replace-regexp ".*\(\'" "" nil (point-min) (point-max))
    (sort-lines nil (point-min) (point-max))
    (delete-duplicate-lines (point-min) (point-max))))

;;;###autoload
(defun django-migration-dependencies ()
  (interactive)
  (let ((vterm-shell (format "grep \"('%s', '0\" %s/*/migrations/*.py"
			     (cdr (assoc 'app-label django-migration-squash-app-migration-alist))
			     django-migration-module-directory))
	(vterm-buffer-name django-migration-dependencies-buffer-name-temp)
	(vterm-kill-buffer-on-exit nil)
	(vterm-exit-functions nil))
    (vterm)))

(provide 'django-migration)
https://github.com/TakesxiSximada/emacs.d/blob/main/lisp/django-migration.el

diredmigrations ディレクトリを表示し m キーでスカッシュするマイグレーションファイルを選択する。 M-x django-migration-select を実行し、選択したマイグレーションファイルをスカッシュ対象として登録する。 M-x django-migration-dependencies を実行すると、スカッシュ対象のアプリケーションに依存するマイグレーションファイルの一覧を確認できる。依存されているマイグレーションを上手く避けつつ、 M-x django-migration-select を選択し直す。

スカッシュ対象を確定できたら、 M-x django-migration-squash を実行し、スカッシュを行う。これによりスカッシュされたマイグレーションファイルが生成される。ただし古いマイグレーションファイルは削除しない。もし django-extentions をインストールしていて、古いマイグレーションファイルを削除したい場合は、 M-x django-migration-delete-squash を実行することで、スカッシュにより必要のなくなったマイグレーションファイルを削除できる。


しむどん三度無視 により 2023/1/19 に投稿、2023/1/21 に最終更新
« ^ »