質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Django

DjangoはPythonで書かれた、オープンソースウェブアプリケーションのフレームワークです。複雑なデータベースを扱うウェブサイトを開発する際に必要な労力を減らす為にデザインされました。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

2回答

533閲覧

Django リレーション(一対多 → 一対多)について教えてください。

tepin712

総合スコア20

Django

DjangoはPythonで書かれた、オープンソースウェブアプリケーションのフレームワークです。複雑なデータベースを扱うウェブサイトを開発する際に必要な労力を減らす為にデザインされました。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

0クリップ

投稿2023/03/19 02:37

編集2023/03/27 12:33

実現したいこと

Djangoでアプリ開発をしている初学者です。
複数のリレーションを組みたいのですが
エラーが発生してしまうため、解決方法をご指導ください。

どうぞよろしくお願いします。

前提

リレーションの詳細
User(1)対 Corp(多)
Corp(1)対 Staff(多)

Djangoアプリケーションを
「accounts」「corp」に分けて作成しています。

app
|-accounts
|-corp
|-app

発生している問題・エラーメッセージ

staffを参照するリンク(staff_list)を叩くと以下のエラーが発生します。

AttributeError at /corp/1/staff-list/
'WSGIRequest' object has no attribute 'corp'

該当のソースコード

accounts/models.py

python

1from django.contrib.auth.models import AbstractUser 2 3 4class CustomUser(AbstractUser): 5 6 class Meta: 7 verbose_name_plural = "CustomUser" 8

corp/models.py

python

1from accounts.models import CustomUser 2from django.db import models 3 4 5class Corp(models.Model): 6 7 user = models.ForeignKey(CustomUser, verbose_name='ユーザー', on_delete=models.PROTECT) 8 9 corp_name = models.CharField( 10 verbose_name='会社名', 11 max_length=50, 12 ) 13 corp_name_kana = models.CharField( 14 verbose_name='会社名カナ', 15 max_length=100, 16 ) 17 18 class Meta: 19 verbose_name_plural = 'Corp' 20 21 def __str__(self): 22 return self.corp_name 23 24 25class Staff(models.Model): 26 27 corp = models.ForeignKey(Corp, verbose_name='会社名', on_delete=models.PROTECT) 28 29 last_name = models.CharField( 30 verbose_name='姓', 31 max_length=10, 32 ) 33 34 first_name = models.CharField( 35 verbose_name='名', 36 max_length=10, 37 ) 38 39 class Meta: 40 verbose_name_plural = 'Staff' 41 42 def __str__(self): 43 return self.last_name 44

追記)corp/ulrs.py

python

1#corp/ulrs.py 2# (省略) 3 path('corp-list/', views.CorpListView.as_view(), name="corp_list"), 4 path('corp/<int:pk>/', views.CorpView.as_view(), name="corp"), 5 6 path('corp/<int:pk>/staff-list/', views.StaffListView.as_view(), name='staff_list'),

追記)corp/views.py

python

1from .models import Corp, Staff 2from django.views import generic 3 4#CorpListViewは正常に取得できる 5class CorpListView(generic.ListView): 6 model = Corp 7 template_name = 'corp_list.html' 8 paginate_by = 5 9 10 def get_queryset(self): 11 corps = Corp.objects.filter(user=self.request.user).order_by('corp_name') 12 return corps 13 14#CorpViewも正常に表示できる 15class CorpView(generic.DetailView): 16 model = Corp 17 template_name = 'corp.html' 18 19 20#StaffListViewはエラーになってしまう 21class StaffListView(generic.ListView): 22 model = Staff 23 template_name = "staff_list.html" 24 paginate_by = 10 25 26 def get_queryset(self): 27 staff_list = Staff.objects.filter(corp=self.request.corp).order_by('last_name') 28 return staff_list

staff/templates/staff_list.html

html

1<!-- 省略 --> 2<a href="{% url 'staff:staff_list' %}">従業員情報</a> 3<!-- 省略 -->

試したこと

マイグレーションをやり直したりデータベースの確認、
インターネットで同様の解説を検索など

補足情報(FW/ツールのバージョンなど)

Django 4.1.7
Python 3.10
PostgreSQL 15.1

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

dameo

2023/03/19 06:54 編集

staff_listの取得方法が分かりませんが、 Staff.objects.filter(corp__user__username='hoge') みたいな感じで、CustomUserのusernameが'hoge'であるようなStaffのQuerySetが取得できます。 QuerySetはqueryプロパティを取得してstrにするとSQLを見れます。なので str(Staff.objects.filter(corp__user__username='hoge').query) とかすると、SQLを見る事ができます。joinしてるかどうかはそこで分かると思います。
tepin712

2023/03/20 05:24

コメントありがとうございます。 ご教授いただいた内容をviewsに書くと次のエラーになってしまいます。 NoReverseMatch at /corp/staff_list/ Reverse for 'corp' with arguments '('',)' not found. 1 pattern(s) tried: ['corp/(?P<pk>[0-9]+)/$']
dameo

2023/03/20 05:31

ちゃんと行ったことを全部載せてもらえますか?あと、ここでなく質問文に書いてください。 そしてviewではなくdjango shellを使用した操作で明快にしてください。
guest

回答2

0

自己解決

以下の方法で解決できました。

staff_list.htmlのリンクでcorp pkを埋め込む

staff/templates/staff_list.html

1 <div class="post-preview"> 2 <a href="{% url 'corp:staff_list' corp.pk %}" class="btn btn-primary float-right">従業員情報</a> 3 </div>

viewsで .filter(corp__pk=self.kwargs["pk"])にてデータを取得

corp/views.py

1class StaffListView(LoginRequiredMixin, OnlyYouMixin, generic.ListView, Corp): 2 model = Staff 3 template_name = "staff_list.html" 4 5 def get_queryset(self): 6 staff_list = Staff.objects.filter(corp__pk=self.kwargs["pk"]) 7 return staff_list

このように修正したところ、取得できるようになりました。
なかなか情報がなかったので、同様のエラーが発生している
どなたかのお役にたてば幸いです。

投稿2023/04/04 11:58

tepin712

総合スコア20

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

dameo

2023/04/04 12:49 編集

ここで普通と違うのはfilterの使い方だけですよね。私の回答があれば難しいことは何もないと思いますが、ほんの少しても役に立つようにしたいのであれば、まずはテンプレート、次にurl.pyやpkの指定方法など、動く形で具体的に載せないといけませんね。あなたがどういう画面を作りたいのかが誰にも分からないからです。djangoは特にうまく動かない理由が分かりにくいフレームで、エラーから原因にも到達しにくいので、全コードを載せる、動くモノを載せるのがとても大事です。役に立ちたいならね。 あなたの問題は、まずリレーションがない形でのviewとテンプレートの使い方を分かっていなかった点。 加えてリレーションがある形でのmodelの使い方をまるで理解していなかった点の2点でした。 そこにアプローチしてることと、原因の明記、対策の明記が必須で、理解してもらうためには最小限の現象再現コードが必要です。 もう少し早く理解できるといいですね。
tepin712

2023/04/04 13:09

今後の参考にさせていただきます
guest

0

回答ではありません。

質問者さんはview+テンプレートの使い方が分からないようなので、タイトルにあるリレーションとは関係ない部分に引っかかっているようです。なので今回の質問は考慮せず、現在タイトルにある「Django リレーション(一対多 → 一対多)について教えてください。」に対して回答します。

モデル抜粋

質問者さんのモデルは必要以上に複雑なので、単純化しました。

mysite/myapp/models.py

python

1from django.db import models 2from django.contrib.auth.models import AbstractUser 3class CustomUser(AbstractUser): 4 def __str__(self): 5 return f'{self.username}({self.first_name} {self.last_name})' 6class Corp(models.Model): 7 user = models.ForeignKey(CustomUser, on_delete=models.PROTECT) 8 name = models.CharField(max_length=50) 9 def __str__(self): 10 return self.name 11class Staff(models.Model): 12 corp = models.ForeignKey(Corp, on_delete=models.PROTECT) 13 name = models.CharField(max_length=10) 14 def __str__(self): 15 return self.name

名前やメンバ自体は削除・変更していますが骨格自体は変えていません。

クエリセット

viewからmodelを参照する際は、Modelオブジェクトを元にQuerySetを作成して、データベースから値を取得します。詳細は以下に記載があります。
https://docs.djangoproject.com/en/4.1/ref/models/querysets/

例えば、CustomUserのusernameが'user1'であるユーザーのCustomUser-Corp-Staffリストの取得は以下のようになります。

mysite/queryset_check_script.py

python

1from myapp.models import CustomUser, Corp, Staff 2queryset_user1 = Staff.objects.filter(corp__user__username='user1').values_list('corp__user__username', 'corp__name', 'name') 3print(queryset_user1) 4print(queryset_user1.query)

表の中身は以下のとおりです(本回答ではsqlite3を使っています)。

sql

1sqlite> select * from myapp_customuser; 2id password last_login is_superuser username first_name last_name email is_staff is_active date_joined 3---------- ---------- ---------- ------------ ---------- ---------- ---------- ---------- ---------- ---------- -------------------------- 41 0 user1 0 1 2023-03-20 21:34:33.274055 52 0 user2 0 1 2023-03-20 21:34:33.274081 6sqlite> select * from myapp_corp; 7id name user_id 8---------- ---------- ---------- 91 corp1 1 102 corp2 2 113 corp3 1 12sqlite> select * from myapp_staff; 13id name corp_id 14---------- ---------- ---------- 151 staff1 1 162 staff2 2 173 staff3 3 184 staff4 2 19sqlite>

この表のデータを先のスクリプトを使い、django shellから流すと以下のように出力されます。

bash

1$ python manage.py shell <queryset_check_script.py 2<QuerySet [('user1', 'corp1', 'staff1'), ('user1', 'corp3', 'staff3')]> 3SELECT "myapp_customuser"."username", "myapp_corp"."name", "myapp_staff"."name" FROM "myapp_staff" INNER JOIN "myapp_corp" ON ("myapp_staff"."corp_id" = "myapp_corp"."id") INNER JOIN "myapp_customuser" ON ("myapp_corp"."user_id" = "myapp_customuser"."id") WHERE "myapp_customuser"."username" = user1 4$

環境構築スクリプト

python

1import os, venv, subprocess 2ENV = 'env' 3if os.name == 'posix': 4 BIN = 'bin' 5else: 6 BIN = 'Scripts' 7builder = venv.EnvBuilder(with_pip=True) 8builder.create(ENV) 9cwd = os.getcwd() 10python = os.path.join(cwd, ENV, BIN, 'python') 11os.environ['VIRTUAL_ENV'] = os.path.join(cwd, ENV) 12os.environ['PATH'] = os.path.join(cwd, ENV, BIN) + os.pathsep + os.environ['PATH'] 13p = subprocess.run([python, '-m', 'pip', 'install', '--upgrade', 'pip', 'setuptools']) 14p = subprocess.run([python, '-m', 'pip', 'install', 'django==4.1.7', 'whatthepatch==1.0.4']) 15with open('generated.py', 'wt', encoding='utf8') as f: 16 f.write("""\ 17import subprocess, os 18p = subprocess.run('django-admin startproject mysite', shell=True) 19os.chdir('mysite') 20p = subprocess.run('python manage.py startapp myapp', shell=True) 21p = subprocess.run('python ../patch.py ../patch.diff', shell=True) 22p = subprocess.run('python manage.py makemigrations', shell=True) 23p = subprocess.run('python manage.py migrate', shell=True) 24p = subprocess.run('python manage.py shell <insert_script.py', shell=True) 25p = subprocess.run('python manage.py shell <queryset_check_script.py', shell=True) 26""") 27with open('patch.diff', 'wt', encoding='utf8') as f: 28 f.write("""\ 29diff --git a/insert_script.py b/insert_script.py 30new file mode 100644 31index 0000000..c47446e 32--- /dev/null 33+++ b/insert_script.py 34@@ -0,0 +1,7 @@ 35+from myapp.models import CustomUser, Corp, Staff 36+users = [CustomUser(username=f'user{i+1}') for i in range(2)] 37+corps = [Corp(name='corp1', user=users[0]), Corp(name='corp2', user=users[1]), Corp(name='corp3', user=users[0])] 38+staffs = [Staff(name='staff1', corp=corps[0]), Staff(name='staff2', corp=corps[1]), Staff(name='staff3', corp=corps[2]), Staff(name='staff4', corp=corps[1])] 39+for l in [users, corps, staffs]: 40+ for o in l: 41+ o.save() 42diff --git a/myapp/models.py b/myapp/models.py 43index 71a8362..aa03eab 100644 44--- a/myapp/models.py 45+++ b/myapp/models.py 46@@ -1,3 +1,16 @@ 47 from django.db import models 48+from django.contrib.auth.models import AbstractUser 49+class CustomUser(AbstractUser): 50+ def __str__(self): 51+ return f'{self.username}({self.first_name} {self.last_name})' 52+class Corp(models.Model): 53+ user = models.ForeignKey(CustomUser, on_delete=models.PROTECT) 54+ name = models.CharField(max_length=50) 55+ def __str__(self): 56+ return self.name 57+class Staff(models.Model): 58+ corp = models.ForeignKey(Corp, on_delete=models.PROTECT) 59+ name = models.CharField(max_length=10) 60+ def __str__(self): 61+ return self.name 62 63-# Create your models here. 64diff --git a/myapp/urls.py b/myapp/urls.py 65new file mode 100644 66index 0000000..4c9189f 67--- /dev/null 68+++ b/myapp/urls.py 69@@ -0,0 +1,8 @@ 70+from django.urls import path 71+ 72+from . import views 73+ 74+urlpatterns = [ 75+ path('', views.index, name='index'), 76+] 77+ 78diff --git a/myapp/views.py b/myapp/views.py 79index 91ea44a..59a44a4 100644 80--- a/myapp/views.py 81+++ b/myapp/views.py 82@@ -1,3 +1,4 @@ 83 from django.shortcuts import render 84- 85-# Create your views here. 86+from .models import CustomUser, Corp, CustomUser 87+def index(): 88+ pass 89diff --git a/mysite/settings.py b/mysite/settings.py 90index 19d4403..80db486 100644 91--- a/mysite/settings.py 92+++ b/mysite/settings.py 93@@ -31,6 +31,7 @@ ALLOWED_HOSTS = [] 94 # Application definition 95 96 INSTALLED_APPS = [ 97+ 'myapp.apps.MyappConfig', 98 'django.contrib.admin', 99 'django.contrib.auth', 100 'django.contrib.contenttypes', 101@@ -103,9 +104,9 @@ AUTH_PASSWORD_VALIDATORS = [ 102 # Internationalization 103 # https://docs.djangoproject.com/en/4.1/topics/i18n/ 104 105-LANGUAGE_CODE = 'en-us' 106+LANGUAGE_CODE = 'ja' 107 108-TIME_ZONE = 'UTC' 109+TIME_ZONE = 'Asia/Tokyo' 110 111 USE_I18N = True 112 113@@ -121,3 +122,5 @@ STATIC_URL = 'static/' 114 # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field 115 116 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 117+ 118+AUTH_USER_MODEL = "myapp.CustomUser" 119diff --git a/mysite/urls.py b/mysite/urls.py 120index 8bc0cd4..7229155 100644 121--- a/mysite/urls.py 122+++ b/mysite/urls.py 123@@ -14,8 +14,9 @@ Including another URLconf 124 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 125 \"\"\" 126 from django.contrib import admin 127-from django.urls import path 128+from django.urls import path, include 129 130 urlpatterns = [ 131+ path('myapp/', include('myapp.urls')), 132 path('admin/', admin.site.urls), 133 ] 134diff --git a/queryset_check_script.py b/queryset_check_script.py 135new file mode 100644 136index 0000000..25edd00 137--- /dev/null 138+++ b/queryset_check_script.py 139@@ -0,0 +1,4 @@ 140+from myapp.models import CustomUser, Corp, Staff 141+queryset_user1 = Staff.objects.filter(corp__user__username='user1').values_list('corp__user__username', 'corp__name', 'name') 142+print(queryset_user1) 143+print(queryset_user1.query) 144""") 145with open('patch.py', 'wt', encoding='utf8') as f: 146 f.write("""\ 147import whatthepatch, os, sys 148with open(sys.argv[1], 'rt', encoding='utf8') as fp: 149 for diff in whatthepatch.parse_patch(fp): 150 header = diff.header 151 if header.new_path == '/dev/null': 152 os.remove(header.old_path) 153 else: 154 if header.old_path == '/dev/null': 155 old = [] 156 else: 157 with open(header.old_path, 'rt', encoding='utf8') as fo: 158 old = [line.rstrip(os.linesep) for line in fo] 159 new = whatthepatch.apply_diff(diff, old) 160 try: 161 dir_path = os.path.dirname(header.new_path) 162 if dir_path == '': 163 dir_path = '.' 164 os.makedirs(dir_path) 165 except FileExistsError: 166 pass 167 with open(header.new_path, 'wt', encoding='utf8') as fn: 168 for line in new: 169 print(line, file=fn) 170""") 171p = subprocess.run([python, 'generated.py'])

使い方は空のディレクトリで上記スクリプトを流すだけです。

中身はおよそ以下のとおりになっています。

  1. venvを使いカレントディレクトリにenvという名前で仮想環境を構築
  2. 仮想環境の中に入る
  3. generated.py, patch.diff, patch.pyをカレントに出力
  4. generated.pyを実行してpipを更新してdjangoとパッチ用のツールをインストール
  5. djangoのプロジェクト(mysite)を作成し、その中にアプリ(myapp)を作成
  6. patch.pyとpatch.diffを使ってアプリ用の実装を生成
  7. dbをmigrate(初回)
  8. django shellとmysite/insert_script.pyを使用し、サンプルデータを作成
  9. django shellとmysite/queryset_check_script.pyを使用し、クエリを発行
  10. 仮想環境から出る

投稿2023/03/20 22:50

dameo

総合スコア943

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

tepin712

2023/03/27 12:37

コメントありがとうございます。ただ当方のプログラミングの誤っている箇所をご指摘いただきたいです。 データベースには正しくデータがあるのですが、どこのプログラムに問題があるため、エラーが発生しているのかを知りたいです。
dameo

2023/03/27 12:43

私の回答はあなたのためのものではありません。 あなたの質問のタイトルにある内容を検索して来た人に対するものです。 どうしてもあなたのコードに対する回答が欲しいのなら、現象を再現できる「最小限の」全コードをご用意ください。余計なものが入っていたら見ないし、ファイルが足りずに動かないのなら見ません。多分そういう作業をしているうちに自分で気づく類の間違いです。他人に聞く暇があったら手を動かしましょう。
tepin712

2023/03/28 15:34

こちらも手を動かしたいのですがわからないので質問しております。 ご指摘いただければ不足のファイル情報は追加しますし、修正もします。 私の質問に対する回答が同様のエラーに苦戦している初学者の参考にもなると思います。 当質問に対する回答では無いのであれば申し訳ありませんが他でお願いします。 ご厚意には感謝しますが私のような初学者が質問しづらくなると思いますので これ以上攻撃的なコメントを記入するのはお止めください。
dameo

2023/03/28 15:41

私の回答はあなたのためのものではありません。 あなたの質問のタイトルにある内容を検索して来た人に対するものです。 どうしてもあなたのコードに対する回答が欲しいのなら、現象を再現できる「最小限の」全コードをご用意ください。余計なものが入っていたら見ないし、ファイルが足りずに動かないのなら見ません。多分そういう作業をしているうちに自分で気づく類の間違いです。他人に聞く暇があったら手を動かしましょう。 初心者かどうかは関係ありませんし、攻撃的なコメントはしていません。手を動かしましょう。
tepin712

2023/03/30 17:47

「他人に聞く暇があったら手を動かしましょう。」 https://teratail.com/tour プログラミングに関して、わからないことがあれば是非teratailで質問してください。とあります。 質問者の問題解決以外はコメントはお控えください。
dameo

2023/03/30 17:51

ほんとに何もしてないんですね。コメントしてる暇があったら手を動かしましょう。 私はあなたが私の解答欄にコメントしてるから返してるだけですよ。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問