【Django - パーマリンク変更】unique + NOT NULLなカラムをあとから追加する方法
2020年6月4日
Djangoでパーマリンク(記事のURL)を<int:pk>
から<slug:slug>
にあとから変更した時の話
どうも、テック備忘LOGのYuki(@tech_bibo_log)です!!
今回はDjangoで作成したブログのパーマリンクに関する記事です。
当ブログを運営しているうえでSEOの観点から分かりやすいURLにするため、
パーマリンクの変更を行いました。
当初のパーマリンク形式’/blog/post/<int:pk>
から、/blog/post/<slug:slug>
へ変更していきます。
また今回は、既に公開中の記事が存在する状態での変更となるので、同じ状況の方も安心です。
基本的に途中でパーマリンクを変更するのはSEO的にはおすすめされないそうですが、
運営前段の段階や、運営直後のPVがつかない段階で行うことをお勧めしておきます。
余談ですが、次の記事では当ブログを作成した時の手順について説明しています。
▽▽▽ Djangoでブログを構築した時のお話し ▽▽▽
2020/5/11
Django + Nginx + uWSGIな環境を構築する!「Hello, World」公開まで。
Djangoでブログを構築した際の実際の手順をコマンドを交えて分かりやすく紹介しています。
yuki-yamashita.site/blog/post/django-blog/
では早速やっていきましょう!!
前提:
1.既に公開中の記事が存在する(公開記事がなくても参考になります)
[ 目次 (開く) ]
Postモデルにunique + NOT NULLなカラムを後から追加して発生したエラー
記事のパーマリンクにスラッグを含ませたいので、<slug:slug>
を扱えるようモデルにカラムを追加しました。
... title = models.CharField(max_length=255) slug = models.SlugField(max_length=30, unique=True) content = MDTextField() description = models.TextField(blank=True) ...
そしてmakemigrations
を実行してみると...既存データに設定されるデフォルト値を指定するよう求められます。
# python3 manage.py makemigrations blog
You are trying to add a non-nullable field 'slug' to product 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) Quit, and let me add a default in models.py
デフォルト値に123
をセットしたところ...
マイグレーションファイルは作成されました。
Select an option: 1 Please enter the default value now, as valid Python The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now Type 'exit' to exit this prompt >>> 123 Migrations for 'blog': blog/migrations/0010_post_slug.py - Add field slug to post
このまま勢いよくmigrate
を実行します。
すると既存の記事データすべてに対して同じ"123"というデフォルト値を入れようとしているので、エラーUNIQUE constraint failed:
が発生して「重複してますよ」と怒られてしまいます。
今回はこの問題を解決していきます。
# python3 manage.py migrate blog Operations to perform: Apply all migrations: blog Running migrations: Applying blog.0010_post_slug...Traceback (most recent call last): ... ... File "/usr/local/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 328, in execute return Database.Cursor.execute(self, query, params) sqlite3.IntegrityError: UNIQUE constraint failed: blog_post.post_slug
今回の環境:
1. VPS(CentOS8)
2. Nginx(1.16.1)
3. Django(3.0.2)
補足:
分かりやすいように、基本コマンドを叩く際はフルパスで記載します。
※一部を除く
現状のMigrationsを確認
まず最初に、Modelを変更したときのMigrations処理が残っていないか確認しましょう。
# python3 manage.py makemigrations
No changes detected
『No changes detected』が表示されていればModelの修正はDBに適用されています。
まだ適用していないMigrationsが残っている場合は先に実行しておきましょう。
マイグレーションの確認が終われば作業の準備は完了です。
Djangoのモデルの管理、マイグレーション操作の方法については次の記事で取り扱っています。
間違えてモデルの変更をDBに適用してしまった場合や、以前の変更を元に戻したい場合は参考にしてみてくださいね。
▽▽▽ マイグレーション操作について知りたい時はこちら ▽▽▽
Postモデルにslugを追加
今回は上記のマイグレーション操作の記事でマイグレーション関係はロールバックとマイグレーションファイルの削除などを行ってエラーがでる前の状態に戻した状態で始めます。
ただし、UNIQUE constraint failed
が出ている状態のマイグレーションファイルを編集する形でも問題ないので、その場合は一つ飛ばしして#2
から始めてください。
1.null=Trueに変更しよう!
最初に、モデルを変更していきます。
まずは下記2点の変更を加えます。
1:unique=True
をnull=True
に変更
2:デフォルト値に123
を指定
... title = models.CharField(max_length=255) slug = models.SlugField(max_length=30, null=True, default=123) ← ここを変更 content = MDTextField() description = models.TextField(blank=True) ...
そして、
makemigtations
を実行。
python3 manage.py makemigrations blog
Migrations for 'blog': blog/migrations/0010_post_slug.py - Add field slug to post
2.既存データにslugのデータを追加する関数処理を追加しよう!
先ほど作成したマイグレーションファイルを開きます。
このままでは既存のデータにslugの値が入らないので、関数set_default_slug
を作成して
各記事のタイトル名をslugのデフォルトとして入れることにします。
# Generated by Django 3.0.2 on 2020-xx-xx xx:xx
from django.db import migrations, models
+def set_default_slug(apps, schema_editor): ← これを追加 + post_model = apps.get_model('blog', 'Post') + for row in post_model.objects.all(): + row.slug = '{}'.format(row.title) + row.save()
class Migration(migrations.Migration):
dependencies = [ ('blog', '0009_category_parent'), ]
operations = [ migrations.AddField( model_name='post', name='slug', field=models.SlugField(default='123', max_length=30,null=True
), preserve_default=False, ), + migrations.RunPython(set_default_slug
, reverse_code=migrations.RunPython.noop), ← これを追加 ]
それでは、slugを
null=True
からunique=True
戻す処理を追加しましょう。
# Generated by Django 3.0.2 on 2020-xx-xx xx:xx
from django.db import migrations, models
+def set_default_slug(apps, schema_editor): post_model = apps.get_model('blog', 'Post') for row in post_model.objects.all(): row.slug = '{}'.format(row.title) row.save()
class Migration(migrations.Migration):
dependencies = [ ('blog', '0009_category_parent'), ]
operations = [ migrations.AddField( model_name='post', name='slug2', field=models.SlugField(default='123', max_length=30, null=True), preserve_default=False, ), migrations.RunPython(set_default_slug, reverse_code=migrations.RunPython.noop),
+ migrations.AlterField( ← slugカラムを修正 + model_name='post', + name='slug2', + field=models.CharField(max_length=30,unique=True
,null=False
,blank=False
), ← NOT NULLなユニーク制約を追加 + preserve_default=False, + ), ]
これで全て完了です。
それではmigrate
を実行しましょう。
# python3 manage.py migrate blog
Operations to perform: Apply all migrations: blog Running migrations: Applying blog.0010_post_slug... OK
成功ですね!お疲れ様でした。
まとめ
今回やった作業を簡単にまとめると下記の2つになります。
一旦nullを許容するようにしておいて既存データに一意なデータを入れてあげて、最後にNOT NULL + uniqueに戻しています。
1:モデルを修正
⇒ unique=True
をnull=True
に変更
⇒ makemigrations
を実行してマイグレーションファイルを生成
2:マイグレーションファイルを修正
⇒ 既存データに対して、新規のslug
カラムにtitle
カラムのデータを挿入する関数set_default_slug
を作成
⇒ slug
カラムをnull=True
からunique=True
に変更(戻す)ように修正
⇒ migrate
を実行
これでもう後からNOT NULL + uniqueなカラムを追加するときも詰まることなくなりました!良かった...(笑)
以上、ありがとうございました。