同じターゲットへの2つの外部キーを持つ中間モデルを含む同様の問題を解決しました。これは私のシステムがどのように見えるかです:
class Node(models.Model):
receivers = models.ManyToManyField('self', through='Connection', related_name='senders', symmetrical=False)
class Connection(models.Model):
sender = models.ForeignKey(Node, related_name='outgoing')
receiver = models.ForeignKey(Node, related_name='incoming')
これは、中間モデルで同じターゲットに対して 2 つの外部キーを使用するための主な要件を示していると思います。つまり、モデルにはManyToManyField
、ターゲット'self'
(再帰的な ManyToMany) とthrough
中間モデルを指す属性を持つ が必要です。また、各外部キーに一意の を割り当てる必要があると思いますrelated_name
。このsymmetrical=False
引数は、再帰関係を一方向にしたい場合に適用されます。たとえば、Node1 は Node2 にシグナルを送信しますが、Node2 は必ずしも Node1 にシグナルを送信するとは限りません。symmetrical=False
再帰的な ManyToMany でカスタムの「スルー」モデルを使用するには、関係を定義する必要があります。カスタムの「スルー」モデルを使用して対称的な再帰的 ManyToMany を作成する場合は、ここでアドバイスを見つけることができます。
これらの相互関係はすべてかなり紛らわしいことがわかったので、コードが何をしているかを実際に捉える適切なモデル属性と related_name を選択するのにしばらく時間がかかりました。これがどのように機能するかを明確にするために、ノード オブジェクト N がある場合、N からデータを受信するか、N にデータを送信する他のノードのセットをそれぞれ呼び出すN.receivers.all()
か、返します。related_names を使用して、Connection オブジェクト自体をN.senders.all()
呼び出しN.outgoing.all()
たり、アクセスしたりします。N.incoming.all()
それにはまだあいまいさがsenders
ありreceivers
、ManyToManyField で交換することができ、コードは同様にうまく機能しますが、方向が逆になることに注意してください。「送信者」が実際に「受信者」に送信しているかどうか、またはその逆かどうかについてテストケースを確認することで、上記にたどり着きました。
あなたの場合、再帰的な ManyToManyField を User に直接追加する方法が明らかではないため、両方の外部キーを User にターゲットすると複雑になります。User モデルをカスタマイズする好ましい方法は、OneToOneField を介して User に接続されたプロキシを介して拡張することだと思います。これは、MembershipInfo を使用して Membership を拡張するのと同じように満足できないかもしれませんが、少なくとも User モデルにさらにカスタマイズを簡単に追加できるようになります。
したがって、あなたのシステムでは、次のようなことを試します(テストされていません):
class Member(models.Model):
user = models.OneToOneField(User, related_name='member')
recruiters = models.ManyToManyField('self', through = 'Membership', related_name = 'recruits', symmetrical=False)
other_custom_info = ...
class UserManagedGroup(Group):
leader = models.ForeignKey(Member, related_name='leaded_groups')
members = models.ManyToManyField(Member, through='Membership', related_name='managed_groups')
class Membership(models.Model):
member = models.ForeignKey(Member, related_name='memberships')
made_member_by = models.ForeignKey(Member, related_name='recruitments')
group = models.ForeignKey(UserManagedGroup, related_name='memberships')
date_added = ...
membership_justification = ...
Member1 が Member2 をリクルートしても、Member2 が Member1 をリクルートしたことを意味しないため、再帰フィールドは非対称である必要があります。関係をより明確に伝えるために、いくつかの属性を変更しました。ユーザー オブジェクトにアクセスする必要がある場合はいつでも Member.user にアクセスできるため、ユーザーを使用する場合はどこでもプロキシ メンバーを使用できます。これが意図したとおりに機能する場合、特定のメンバー M で次のことができるはずです。
M.recruiters.all() -> set of other members that have recruited M to groups
M.recruits.all() -> set of other members that M has recruited to groups
M.leaded_groups.all() -> set of groups M leads
M.managed_groups.all() -> set of groups of which M is a member
M.memberships.all() -> set of Membership objects in which M has been recruited
M.recruitments.all() -> set of Membership objects in which M has recruited someone
グループGの場合、
G.memberships.all() -> set of Memberships associated with the group
これは機能し、別の MembershipInfo モデルよりも「クリーンな」ソリューションを提供するはずだと思いますが、たとえば、再帰フィールドの方向をチェックして、採用担当者が新兵を募集していることを確認し、その逆ではないことを確認するなど、微調整が必要になる場合があります。
編集: Member モデルを User モデルにリンクするのを忘れていました。それは次のように行われます:
def create_member(member, instance, created, **kwargs):
if created:
member, created = Member.objects.get_or_create(user=instance)
post_save.connect(create_member, member=User)
create_member は Member のメソッドではなく、Member が定義された後に呼び出されることに注意してください。これにより、User が作成されるたびに Member オブジェクトが自動的に作成されます (Member フィールドを初期化せずにユーザーを追加する場合は、メンバー フィールドを null=True および/または blank=True に設定する必要がある場合があります)。