[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

DjangoのあるModelを参照しているModelの一覧を取得する

ここのところDjangoを触り続けていてなかなか楽しいです。触っている中であるModelのオブジェクトを1つに統合する処理を書かなければいけなくなりました。そんなときに影響を受けるModelの洗い出しをどうするか調べたので残しておきます。

もうちょっと詳しく

こんな感じのモデルを例にします。

from django.db import models


class Product(models.Model):

    name = models.CharField(verbose_name='商品名', max_length=100)
    price = models.IntegerField(verbose_name='価格')


class ProductOrder(models.Model):

    product = models.ForeignKey('Product', verbose_name='注文商品', related_name='orderd_product')
    amount = models.IntegerField(verbose_name='注文数')
    order_date = models.DateField(verbose_name='注文日', auto_now=True)


class ProductMakerName(models.Model):

    product = models.ForeignKey('Product', verbose_name='商品', related_name='makerd_product')
    maker_name = models.CharField(verbose_name='作成会社名', max_length=100)

Productが中心にあり、ProductOrderProductMakerNameがそれを参照する関係にあります。

この時、例えばProductで本来同じであるはずの製品が何故か結構前から2個登録されており、そのままそれぞれにProductOrderが紐付いてしまっていたとします。この2つになってしまったProductを1つに統合し、ProductOrder等も統合しなければいけないケースがあったとします。

統合するとどちらかのオブジェクトは消えるわけですが、それを参照しているオブジェクトについては統合先のオブジェクトにリレーションを貼り直さなければいけません。そのためには、Productをどのモデルが参照しているかの一覧が欲しくなります。

どうするのか

1.7より前のDjangoではModel._meta.user._meta.get_all_related_objects() で関連するモデルを取得できたようですが、廃止されたようです。以下のドキュメントにget_all_related_objects相当の結果を取得するためのコード例が示されていました。

https://docs.djangoproject.com/en/2.0/ref/models/meta/#migrating-from-the-old-api

これらの例をまとめると、こんな感じの処理で関連するModelが取れるようです。

def get_all_related_models(target_object):
    """
    被参照関係にあるモデルクラスの一覧を戻す
    :param model target_object: 捜索対象
    :return: 被参照関係にあるモデルクラスのリスト
    :rtype: list[class]
    """

    related_list = [
        f.related_model for f in target_object._meta.get_fields()
        if (f.one_to_many or f.one_to_one or f.many_to_many) and f.auto_created and not f.concrete
    ]

    return related_list

対象のModelの全列をget_fields()で取得し、関連を持っているかを確認した上でそのrelated_modelを参照することで関連するModelの一覧を列挙できるようです。

実際に実行してみます。

n [6]: prodct = Product.objects.get(pk=1)

In [7]:

In [7]: def get_all_related_models(target_object):
   ...:     related_list = [
   ...:         f.related_model for f in target_object._meta.get_fields()
   ...:         if (f.one_to_many or f.one_to_one or f.many_to_many) and f.auto_created and not f.concrete
   ...:     ]
   ...:
   ...:     return related_list
   ...:

In [8]: get_all_related_models(prodct)
Out[8]: [app1.models.ProductOrder, app1.models.ProductMakerName]

期待どおりに[app1.models.ProductOrder, app1.models.ProductMakerName]という形で依存関係を持っているModelの一覧が期待通り取得できています。あとは、ProductOrderProductMakerName側に対して対象のProductオブジェクトを参照しているオブジェクトを取得し、更新すれば完了です。

ProductOrder.objects.filter(product=product).update(product=merged_product)

実際はProductOrder側等でユニーク制約等を考慮してすでにあるエントリは更新ではなく削除するなどの対応が必要になることもあると思いますが、概ねこの流れで良いと思います。