ForeignKey, OneToOneField, ManyToManyField

최동혁·2022년 12월 6일
0

Django

목록 보기
7/11

ForeignKey, OneToOneField, ManyToManyField

RDBMS에서의 관계 예시

1 : N 관계 → models.ForeignKey로 표현

1명의 유저(User)가 쓰는 다수의 포스팅(Post)

1명의 유저(User)가 쓰는 다수의 댓글(Comment)

1개의 포스팅(Post)에 다수의 댓글(Comment)

1 : N 이면, N 측에 외래키를 심는다.

1 : 1 관계 → models.OneToOneField로 표현

1명의 유저(User)는 1개의 프로필(Profile)

M : N 관계 → models.ManyToManyField로 표현

1개의 포스팅(Post)에는 다수의 태그(Tag)

1개의 태그(Tag)에는 다수의 포스팅(Post)

ForeignKey

1 : N 관계에서 N측에 명시

ex) Post : Comment, User : Post, User : Comment,

ForeignKey(to, on_delete)

to : 대상모델

클래스를 직접 지정하거나,

클래스명을 문자열로 지정. 자기 참조는 “self” 지정

on_delete : Record 삭제 시 Rule → https://docs.djangoproject.com/en/2.1/ref/models/fields/#django.db.models.ForeignKey.on_delete

CASCADE : FK로 참조하는 다른 모델의 Record도 삭제 (장고 1.X에서의 디폴트값)

PROTECT : ProtectedError (IntegrityError 상속) 를 발생시키며, 삭제 방지

SET_NULL : null로 대체. 필드에 null = True 옵션 필수.

SET_DEFAULT : 디폴트 값으로 대체. 필드에 디폴트 값 지정 필수.

SET : 대체할 값이나 함수 지정. 함수의 경우 호출하여 리턴값을 사용

DO_NOTHING : 어떠한 액션 X. DB에 따라 오류가 발생할 수도 있다.

예시

class Post(models.Model):
	message = models.TextField()
	photo = models.ImageField(blank = True, upload_to = 'instagram/post/%Y/%m/%d')
	is_public = models.BooleanField(defualt = False, verbose_name = '공개여부')
	created_at = models.DateTimeField(auto_now_add = True)
	updated_at = models.DateTimeField(auto_now = True)

	def __str__(self):
		return self.message

	class Meta:
		ordering = ['-id']

class Comment(models.Model):
	post = models.ForeignKey(Post, on_delete = models.CASCADE)
	# post = models.ForeignKey('Post', on_delete = models.CASCADE)
	# post = models.ForeignKey('instagram.Post', on_delete = models.CASCADE)
	# 다 가능
	message = models.TextField()
	created_at = models.DateTimeField(auto_now_add = True)
	updated_at = models.DateTimeField(auto_now = True)
  • 1개의 Post에 여러개의 Comment
  • 1 : N 관계이다.
  • Comment가 N이여서 Comment 모델에 외래키를 심어준다.
  • 대상은 Post 모델로 잡아준다.
  • 보통 외래키를 심어주면 DB에 해당 필드명은 위에 정해준것처럼 post가 아닌 post_id로 저장이 된다.
    • post_id는 해당 Post 모델에 해당하는 pk 값이다.

ForeignKey 내부 접근

class Post(models.Model):
	message = models.TextField()
	photo = models.ImageField(blank = True, upload_to = 'instagram/post/%Y/%m/%d')
	is_public = models.BooleanField(defualt = False, verbose_name = '공개여부')
	created_at = models.DateTimeField(auto_now_add = True)
	updated_at = models.DateTimeField(auto_now = True)

	def __str__(self):
		return self.message

	class Meta:
		ordering = ['-id']

class Comment(models.Model):
	post = models.ForeignKey(Post, on_delete = models.CASCADE)
	# post = models.ForeignKey('Post', on_delete = models.CASCADE)
	# post = models.ForeignKey('instagram.Post', on_delete = models.CASCADE)
	# 다 가능
	message = models.TextField()
	created_at = models.DateTimeField(auto_now_add = True)
	updated_at = models.DateTimeField(auto_now = True)
  1. 댓글 작성

  2. 쿼리셋 확인

    >>> from instagram.models import Post, Comment
    
    >>> comment = Comment.objects.first()
    >>> comment
    <Comment: Comment object (1)>
    • first()는 get()과 같이 쿼리셋이 아닌 하나의 객체를 반환해준다.
    • 전체 내용을 보고 싶다면 str(comment.query)로 볼 수 있다.

    이 상태에서 comment와 외래키로 연결된 post를 보고 싶다면?

    >>> Post.objects.get(pk=comment.post_id)
    4
    • 해당 댓글이 달린 게시글이 4의 pk 값을 가지고 있다는 것을 볼 수 있다.
    • 이렇게 post_id로 접근해도 되지만 다른 방식도 된다.
    >>> comment.post
    <Post: 네 번째 포스팅>
    • Comment 모델에 post 필드가 있다.
    • post로 접근을 하면 Post.objects.get(pk=comment.post_id) 이 동작을 내부적으로 수행해서 post에 할당해준다.
    • 결국 쿼리셋은 SELECT가 2번 이루어진 결과를 볼 수 있다.
    • Comment 모델을 SELECT하고, Post 모델을 SELECT 하는 것이다.

    Post에 속한 댓글 얻어오는 법

    >>> post = Post.objects.first()
    post
    <Post: 네 번째 포스팅>
    
    >>> comment.objects.filter(post_id=4)
    <QuerySet [<Comment: Comment object (1)>, <Comment: Comment object (2)>]>
    
    >>> comment.objects.filter(post__id=4)
    <QuerySet [<Comment: Comment object (1)>, <Comment: Comment object (2)>]>
    
    >>> comment.objects.filter(post=post)
    <QuerySet [<Comment: Comment object (1)>, <Comment: Comment object (2)>]>
    
    >>> post.comment_set.all()
    <QuerySet [<Comment: Comment object (1)>, <Comment: Comment object (2)>]>

    4번째 post의 댓글 얻어오는 방법

    1. post_id = 4
      1. Comment의 직접적인 필드 접근으로 얻어오기
    2. post__id = 4
      1. 실제 post 측에 있는 id
    3. post = post
      1. comment.objects.filter(post=post) 와 post.comment_set.all() 는 똑같다!
      2. 이것은 reverse_name 접근이다.
      3. 바로 다음 글에서 확인할 수 있다.

reverse 접근 시의 속성명 : 디폴트 → “모델명소문자_set”

Post : Comment (1 : N)

Post 측에서는 Comment와 다르게 참조할 이름이 없다.

왜냐면 FK는 N에 해당하는 모델에 심어주기 때문이다.

그래서 이런 경우 1에 해당하는 Post 측에는 “모델명소문자_set” 이라는게 생긴다!

>>> from instagram.models import Post, Comment

>>> post = Post.objects.first()
  • post.comment_set.all() ↔ Comment.objects.filter(post=post)

reverse_name 이름 충돌이 발생한다면?

reverse_name 디폴트 명은 앱이름 고려 X, 모델명만 고려

다음의 경우, user.post_set 이름에 대한 충돌

blog앱 Post모델, author = FK(User)

shop앱 Post모델, author = FK(User)

이름이 충돌이 날 때, makemigrations 명령이 실패

이름 충돌 피하기

  1. 어느 한 쪽의 FK에 대해, reverse_name을 포기 → related_name = “+”
    1. +로 쓰게 되면 reverse_name을 사용안하겠다는 소리임.
  2. 어느 한 쪽의 (혹은 모두) FK의 reverse_name을 변경
    1. ex) FK(User, …., related_name = “blog_post_set”)
    2. ex) FK(User, …., related_name = “shop_post_set”)

OneToOneField

1 : 1 관계에서 어느 쪽이라도 가능

User : Profile

ForeignKey(unique=True)와 유사하지만, reverse 차이

User : Profile를 FK로 지정한다면 → profile.user_set.first() → user

만약 1 : N의 관계에서 1측인 User가 없다면 None 반환

User : Profile를 O2O로 지정한다면 → profile.user → user

만약 1 : 1의 관계에서 1측인 User가 없다면 DoesNotExist() 예외 발생

OneToOneField(to, on_delete)

class Profile(models.Model):
	user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete = models.CASCADE)
	address = models.CharField(max_lenght = 100)
	zipcode = models.CharField(max_lenght = 6)
  • 1 : 1 관계이기 때문에 이미 서로 지정이 되어 있는 상태에서 다른 것이 참조하면 validation 에러 발생

reverse 접근 시의 속성명 : 디폴트 → 모델명소문자

class Profile(models.Model):
	user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete = models.CASCADE)
	address = models.CharField(max_lenght = 100)
	zipcode = models.CharField(max_lenght = 6)
>>> from accounts.models import Profile
>>> from django.contrib.auth import get_user_model

>>> User = get_user_model()
>>> profile.user
>>> user.profile
  • from django.contrib.auth import get_user_model는 현재 활성화된 User의 정보를 얻을 수 있는 function이다.

ManyToManyField

M : N 관계에서 어느 쪽이라도 필드 지정 가능

ManyToManyField(to, blank = False)

방법 1

class Post(models.Model):
	tag_set = models.ManyToManyField('Tag', blank = True)

class Article(models.Model):
	tag_set = models.ManyToManyField('Tag', blank = True)

class Tag(models.Model):
	name = models.CharField(max_lenght = 100, unique = True)

방법2

class Post(models.Model):
	...

class Article(models.Model):
	...

class Tag(models.Model):
	name = models.CharField(max_lenght = 100, unique = True)
	post_set= models.ManyToManyField('Post', blank = True)
	article_set = models.ManyToManyField('Article', blank = True)
  • M2M을 선언하면 장고가 임의로 데이터 베이스에 중간 테이블인 post_article_set을 생성해줌.
    • 그 안에는 post_id와 article_id 필드가 있음.
>>> from instagram.models import Post, Tag

>>> post = Post.objects.first()
>>> post.tag_set.all()
<QuerySet [<Tag: 파이썬>]>

>>> tag = Tag.objects.first()
>>> tag.post_set.all()
<QuerySet [<Post: 네 번째 포스팅>]>
  • related_name에는 add라는 함수가 있다.
  • 예를 들어
    >>> Tag.objects.create(name='추가태그')
    >>> Tag.objects.all()
    <QuerySet [<Tag: 파이썬>, <Tag: 추가태그>]>
    
    >>> tag = Tag.objects.get(name='추가태그')
    >>> post.tag_set.add(tag)
    
    >>> post.tag_set.all()
    <QuerySet [<Tag: 파이썬>, <Tag: 추가태그>]>
    
profile
항상 성장하는 개발자 최동혁입니다.

0개의 댓글