Django
WebAPI
SQL

DjangoでDBに初期データをインポートする時の自分がハマった点


こんにちわ

PythonやDjangoがわからないオタクのhmdです、よろしくお願いします。

DRF(Django RESTfull Framework)理解したい


TL;DR


  • DRFでRESTfullAPIを作る時にDBに初期データをjsonからインポートしたかった

  • ググってもそれっぽい方法が出てこなかったのでModel周りの機能を利用してDjangoのshellから叩いた

  • 色々引っかかって時間を取られたので備忘録


何がしたかったの?

ある理由でDRFを書こうとしているんですが、その前段階のモデル定義の時点で初期データを突っ込んでおきたくなったので、その方法を調べていました。

調べた結果、manage.pyにloaddataという機能がある事を知りましたが、どうやらそれはすでに稼働しているDBから新しい環境にデータを移動させる時のインポートの方法らしく、路頭に迷ってしまいました。

埒が明かないのでpython manage.py shellをして、shell内でModelAPIを叩くことにしたという話です。


引っかかったところ


  • SQLite3の使い方

  • ManyToManyフィールドとsaveメソッド

以下、とてもとても見苦しいコードが並びます。ご了承ください。


SQLite3がわからん


DataGrip&PyCharmで.tablesができない

この通りです。

.tablesと打っても実行時にNothing to runとなりました。

Twitterでキレてた呟いたところ、DataGripのヘルパーアカウントから「困ってんの?これしてみ」と言われましたが、その通りやってもなんかうまくいかず、最終的にTerminalからsqliteコマンドを叩いて利用することで解決しました。

これはキレてたところを公式に諌められる僕.png

☝️これはキレてたところを公式に諌められる僕

流石にtable一覧が見えないと不便極まりないのでは?ってなったんですけど、もし僕のやり方が違って、正しいやり方があるなら教えていただきたいです(一応select * fromの置換で一覧できたけどどちらにしろ不便...)


.open FILENAMEを忘れがち

その通りです。

論外ですよね...


sqlite3

SQLite version 3.24.0 2018-06-04 14:10:15

Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite>

ここに書いてある事をよく読みましょう(自戒)


ManyToManyFieldとsave()

ここからが本題です


ManyToManyFieldとは

DjangoのModelAPIで多対多を記述したい時に使います。

migrateした時にデータベース上では2モデル分のテーブル+その間の関係を記すテーブルに変換されてくれて便利です。


どんなModelを書いたか


models.py

from django.db import models

class Author(models.Model):
name = models.CharField(max_length=100)

class Tag(models.Model):
name = models.CharField(max_length=100)

class Article(models.Model):
title = models.CharField(max_length=500)
author = models.ForeignKey(Author, related_name='articles', on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag, related_name='articles', blank=True)


簡単に説明するとQiitaのようなブログシステムを模したモデルです。

Article(記事)にはAuthor(著者)とタイトルが存在し、Tag(タグ)がついています。

さて、ここで一記事に対してタグがいくつか設定されていて、かつ、1つのタグが設定されている記事はいくつか存在しています。

つまり、いくつかの記事といくつかのタグが関係している状態=ManyToMany(多対多)です。

こういう時、DjangoではManyToManyFieldを利用します(たぶん他のフレームワークでもほとんど同じ名前)


どのようにデータのインポートを行なったか

先にも書いた通り、shellからModelAPIを叩きました。

上のmodels.pyをマイグレーションした時点で


  • {{app_name}}_article

  • {{app_name}}_article_tags

  • {{app_name}}_author

  • {{app_name}}_tag

というテーブル群がDB上に生成されるので、そこにデータを流し込んでいきます。


1. リレーションのないモデルを生成しsave()

Article以外にはリレーション関連のフィールド(ForeignKey, ManyToMany)を設定したモデルはないので、そこを先に生成します。

from {{app_name}} import Author, Tag

# Author Model
for author in authors:
a = Author(name=author)
a.save()

# Tag Model
for tag in tags:
t = Tag(name=tag)
t.save()

authors, tagsは初期データ群からauthorデータとtagデータだけを重複無しで取り出したリストです。

モデルをデータベースに記録するにはインスタンスを生成した後にsave()を実行する必要があります。shellでデータベースの中身をいじる時やviewでModelAPIを直接いじる時には注意が必要です。


2. リレーションのあるモデルを生成しsave()(しかし罠にはまる)

上と同様にArticleを生成します。ただし、リレーションの部分は、実際に対応するモデルのオブジェクトをgetしてきてコンストラクタに突っ込む必要があります。

for key in datas.keys():

author = datas[key]['author']
title = datas[key]['title']
article = Article(
author=Author.objects.get(name=author),
title=title
)
for tag in datas[key]['tags']:
article.tags.add(Tag.objects.get(name=tag))
article.save()

datasは初期データの記録されたJSONファイルを辞書型変数にロードしたものです。

JSONに記録されていた記事情報を1つずつfor文で回し、必要なデータを取得した後Articleのインスタンスを生成しています。

この時点でTagのデータは結び付けられていないんですが、そのあとのfor文でarticle.tags.addというメソッドを実行する時に、データに対応するTagのオブジェクトをゲットしてきて追加しています。

これで本来はやりたいことができると思っていたのですが、まさかのエラー発生。

ValueError: "<Article: Article object (None)>" needs to have a value for field "id" before this many-to-many relationship can be used.

エラーメッセージでググってから初めて知りましたが、どうやらManyToManyFieldを利用するモデルは、実際にManyToManyに指定されたモデルのオブジェクトをaddする前に保存されていないといけないらしいです。エラーメッセージ自体は、まだArticleのオブジェクトが保存されていないので結び付けたいArticleのオブジェクトにidがないよ〜って感じに理解しました。

というわけで、addの前に一度save()をちゃんとしましょう。

for key in datas.keys():

author = datas[key]['author']
title = datas[key]['title']
article = Article(
author=Author.objects.get(name=author),
title=title
)

# 今回ハマったやつ
article.save()

for tag in datas[key]['tags']:
article.tags.add(Tag.objects.get(name=tag))
article.save()

これで無事にデータがインポートできてました。


まとめ

ManyToManyFieldを利用してモデルの結び付けを行う時にはsave()を忘れずに行おう(自戒)


おわりに

この間、 #kosenconf というイベントに参加して大阪のさくらインターネット本社に行ってきたんですが、梅田駅の地下で食べたパンケーキが本当に美味しくてホームシック的な症状が起きてます。なんとかDjangoを頑張っていきたいですわオホホ

3月に東京である次回の #kosenconf にも参加して、Djangoのネタについて登壇しようと思っているので、会場で僕と握手しましょう。

本編とは関係ないですが、僕は「自分がググった時にQiitaやhatenablogでわかりやすい記事が検索できなかった時かつ、自分がそこそこ困った時」にQiitaに記事を投げるようにしています。

初学者ガーからのマサカリが怖いですが、この記事が他人のためになれば嬉しく思っています。

何か問題があればコメント欄かTwitterまで伝えてくださると嬉しいです。

Pythonわからん