하지만 반대로, 역방향 참조를 수정하는 경우(다른 모델이 수정할 모델을 가리킬 때)에는 수정할 모델을 가리키는 모든 모델을 수정해야 합니다. 어디서 이 모델을 가리키는지 다 모른다고요? 장고 모델 클래스의 _meta.related_objects 속성에서 찾을 수 있습니다.
for related_object in Product._meta.related_objects:
print(
related_object.related_model.__name__,
related_object.remote_field.name,
related_object.get_accessor_name(),
sep='\t',
)
# => Purchase product purchases
# => Sell product sells
# => ProductStatistics product statistics
예제의 ERD
class ProductCategory(Model):
code = CharField(...)
class Product(Model):
category = Foreignkey(ProductCategory, related_name='products', ...)
name = CharField(...)
...
class Purchase(Model):
buyer = ForeignKey(User, related_name='purchases', ...)
product = ForeignKey(Product, related_name='purchases', ...)
...
class Sell(Model):
seller = ForeignKey(User, related_name='sells', ...)
product = ForeignKey(Product, related_name='sells', ...)
...
상품분류(ProductCategory)에 속한 상품(Product)들을 사용자(User)가 구매(Purcase)하거나 판매(Sell)할 수 있는 구조입니다. 모델의 세부 내용과 on_delete 같은 필수 매개변수 등은 생략했습니다. 예제를 간단히 하기 위한 것이니 신경쓰지 않으셔도 됩니다.
category_book = ProductCategory.objects.create(code='BOOK', ...)
category_magazine = ProductCategory.objects.create(code='MAGAZINE', ...)
Product.objects.create(category=category_book, name='월간 8퍼센트 소식 2020년 10월호', ...)
Product.objects.create(category=category_book, name='월간 8퍼센트 소식 2020년 11월호', ...)
상품 분류로 ‘BOOK’, ‘MAGAZINE’이 있고, BOOK으로 분류된 ‘월간 8퍼센트 소식지’라는 상품들이 입력되어 있습니다. 그런데 소식지 상품들의 상품분류가 잘못 입력되었다고 합니다. BOOK이 아니라 MAGAZINE이라는군요. 이걸 수정하는 건 어렵지 않습니다. 대상 상품들의 category 필드만 수정하면 됩니다.
Product.objects.filter(
category=category_book,
name__startswith='월간 8퍼센트 소식지',
).update(
category=category_magazine,
)
순방향 외래 키 참조를 수정하는 건 간단하군요.
product_1 = Product.objects.create(name='월간 8퍼센트 소식 2020년 12월호', ...)
product_2 = Product.objects.create(name='월간 8퍼센트 소식 2020년 12월호', ...)
Purchase.objects.create(product=product_1, ...)
Purchase.objects.create(product=product_2, ...)
Sell.objects.create(product=product_1, ...)
Sell.objects.create(product=product_2, ...)
...
데이터 정리를 하다가 product_1과 product_2 가 동일한 제품이라는 사실을 발견했습니다. 제품 담당자가 실수로 동일한 제품을 두 번 입력했던 것입니다.
데이터 이상이 발생한 상황이므로 수정해야 합니다. product_2는 삭제하고 product_1만 남기는 방식으로 둘을 합치기로 결정했습니다. product_2는 다른 모델이 참조하고 있기 때문에 그냥 삭제하면 안 됩니다. 먼저 product_2를 참조하는 모델들을 찾아 product_1로 수정해야 합니다.
Purchase.objects.filter(product=product_2).update(product=product_1)
Sell.objects.filter(product=product_2).update(product=product_1)
앞서 작성한 모델 정의 코드를 확인해보면 Product 모델을 참조하는 모델은 Purchase와 Sell 뿐입니다. 위 소스코드 같이 모델을 직접 찾아 수정하는 것도 한 방법입니다.
그런데 이 방법에는 잠재적인 위험이 있습니다. 과연 Product 모델을 참조하는 모델이 Purchase와 Sell 뿐이라고 확신할 수 있을까요? 장고 앱은 다른 장고 앱에서 가져와 함께 사용할 수도 있습니다. 그렇기 때문에 모델을 참조하는 부분이 같은 앱의 소스코드에 없더라도 잠재적으로 다른 곳에서 참조될 가능성이 있습니다. 따라서 이 경우처럼 역방향 참조를 수정할 때는 이 모델을 가리키는 역방향 외래 키를 모두 조회해야 합니다.
역방향 참조를 모두 찾아 수정하기
장고 모델 클래스에는 _meta 라는 속성이 정의되어 있습니다. 이 속성은 Options 클래스의 인스턴스로, 모델의 여러 가지 부가 정보가 정의되어 있는데 그 중에는 관계 정보도 있습니다. Options 인스턴스의 다양한 정보 중 related_objects 속성이 바로 모델의 관계를 담은 시퀀스입니다. for 문으로 확인해봅시다.
for related_object in Product._meta.related_objects:
print(related_object)
# => <ManyToOneRel: shopping.purchase>
# => <ManyToOneRel: shopping.sell>
# => <ManyToOneRel: statistics.productstatistics>
예제에서 작성한 모델은 shopping이라는 앱에 정의해 두었나 봅니다. shopping 앱의 Purchase 모델과 Sell 모델이 다대일 관계(ManyToOneRel)로 연결된 것을 확인할 수 있습니다. 그런데 statistics 앱에서도 Product 모델과 다대일 관계로 연결된 모델이 있나 보군요.
모델과 외래 키 필드의 이름을 정확하게 확인해봅시다.
for related_object in Product._meta.related_objects:
print(
related_object.related_model.__name__,
related_object.remote_field.name,
related_object.get_accessor_name(),
sep='\t',
)
# => Purchase product purchases
# => Sell product sells
# => ProductStatistics product statistics
위 코드에서 확인한 속성의 의미는 다음과 같습니다.
related_object.related_model: 참조한 모델
related_object.remote_field.name: 참조한 필드의 이름
related_object.get_accessor_name(): 역참조 모델의 역참조 필드의 이름
역참조 필드의 이름은 ForeignKey 필드를 정의하는 모델에서 related_name에 정의한 이름입니다. 직접 정의하지 않으면 모델 이름 뒤에 접미사 _set 을 붙인 형태로 정의됩니다. 예를 들어, Sell 모델의 product 필드에서는 sells, 라고 정의했는데, 직접 정의하지 않았다면 sell_set이 되었을 겁니다.
이를 활용해 Purchase 모델이 역참조하는 모든 외래 키를 수정할 수 있습니다.
from django.db.models.fields.reverse_related import ManyToOneRel
for related_object in Product._meta.related_objects:
if type(related_object) != ManyToOneRel:
continue
model = related_object.related_model
field_name = related_object.remote_field.name
model.objects.filter(
**{field_name: product_2},
).update(
**{field_name: product_1},
)
_meta.related_objects 에는 ManyToOneRel외에도 OneToOneRel, ManyToManyRel 등이 있습니다. 이것들은 수정하는 방식이 ForeignKey 와 약간 달라서 type(related_object)를 확인해서 제외했습니다. (그것들이 존재한다면 예제와 같이 그냥 넘어가는 게 아니라 알맞은 방법으로 수정하셔야 합니다.)
이걸로 Product 가 역참조하는 모델들을 빠짐없이 수정했습니다. 마지막으로 product_2 를 삭제하여 작업을 마무리하면 됩니다.
product2.delete()
SQL으로 역방향 참조를 확인하고 싶나요?
SQL으로도 역방향 참조를 확인할 수 있습니다. Dataedo의 문서(https://dataedo.com/kb/query/postgresql/list-foreign-keys)을 참고하세요.
참고 자료
장고 Options 클래스 소스코드: https://docs.djangoproject.com/en/2.2/_modules/django/db/models/options/