1. 1:1관계

OneToOneField()

 

2. 1:N 관계

ForeignKey()

 

3. M:N 관계

ManyToManyField()

 

 

1:N 관계

ForeignKey(to, on_delete)

Django 모델에서 관계형 데이터베이스의 외래키를 담당한다.

2개의 필수 위치 인자가 필요하다.

 

on

참조하는 model class

 

on_delete

외래키가 참조하는 객체가 사라졌을 때 외래키를 가진 객체를 어떻게 처리할지를 정의

데이터 무결성을 위해 중요한 설정이다.

on_delete 옵션 값 

CASCADE : 부모객체(참조된 객체)가 삭제됐을 때 이를 참조하는 객체도 삭제된다.

그 외 PROTECT, SET_NULL, SET_DEFAULT ...등 여러 옵션이 존재한다.

 

 

* 데이터 무결성

데이터의 정확성, 일관성을 유지하고 보증하는 것

개체 무결성(Entity), 참조 무결성(Reference), 범위 무결성(Domain)

 

 

 

class Article(models.Model):
    title = models.CharField(max_length=10)
    content = models.TextField()
    
    
class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE)

외래키 필드는 ForeignKey클래스를 작성하는 위치와 관계없이 필드의 마지막에 작성된다.

ForeignKey() 클래스의 인스턴스 이름은 참조하는 모델 클래스 이름의 소문자형, 단수형으로 작성하는 것을 권장한다.

Article 클래스를 참조하기 때문에 article 이라는 이름을 만들었다.

 

 

모델에 변동사항이 생겼기 때문에 migration 진행해야 한다.

python manage.py makemigrations
python manage.py migrate

 

 

 

장고에서 models.py으로 설계도를 구성하고 migrations를 만들었다.

0002_comment.py가 새로 생성되었다. 

 

python manage.py sqlmigrate articles 0002


BEGIN;
--
-- Create model Comment
--
CREATE TABLE "articles_comment" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
"content" text NOT NULL, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL, 
"article_id" bigint NOT NULL REFERENCES "articles_article" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "articles_comment_article_id_59ff1409" ON "articles_comment" ("article_id");
COMMIT;

sqlmigrate 명령어를 실행하면 해당 migrations를 SQL문으로 확인 할 수 있다.

해당 migrations 설계도가 테이블을 생성한다는 것을 알 수 있다.

 

 

 

파이썬으로 작성된 migrations.py를 DB(SQL)과 소통하기 위해서 django ORM이 이용된다. 

 

ORM : object relational mapping(객체 관계 매핑)

Object(객체)는 말 그대로 OOP에서 사용되는 객체 그 자체, Relational(관계)는관계형 데이터베이스를 의미한다.

 객체와 관계형DB를 매핑해주는 개념이다.

원래 DB를 쓰려면 SQL 쿼리가 필요하다. 그러나 ORM이 있으면 파이썬 문법만으로도 DB를 다룰 수 있다.

SQL문법을 파이썬 객체로 맵핑해 파이썬 객체의 인스턴스, 메소드를 통해 구현된다.

ORM을 사용하게 되면 따로 SQL문을 작성할 필요없이 객체를 통해 간접적으로 데이터베이스를 조작할 수 있다.

 

 

 

 

 

migrate 후 comment 모델 클래스로 인해 생성된 테이블을 확인한다.

FK 모델 필드로 인해 작성된 컬럼 이름이 article_id이다.

만약 FK 인스턴스를 article이 아닌 abc로 작성했다면 abc_id가 될 것이다. 

-> 모델 관계 파악을 위해 단수형, 소문자로 만들자!

 

 

 

 

 python manage.py shell_plus

shell_plus를 실행해서 댓글을 생성해보도록 한다.

 

 

In [1]: comment = Comment()

In [2]: comment.content="first comment"

In [3]: comment.save()

IntegrityError: NOT NULL constraint failed: articles_comment.article_id

NOT NULL 오류가 발생한다. article을 참조하는 article_id가 NULL이기 때문이다.

 

 

In [4]: article = Article.objects.create(title='title', content='hello')

In [5]: article.pk
Out[5]: 1

In [6]: comment.article=article

In [7]: comment.save()

article 인스턴스를 만들어주고 comment.article에 만들어준 인스턴스 자체를 대입하면 된다.

comment.article_id=article.pk도 가능하지만, 권장하지 않는다. 객체를 집어넣는 것이 권장된다.

 

 

객체를 넣어주어도, 객체의 pk값을 article_id로 저장하게 된다.

 

 

 

In [8]: comment.article_id
Out[8]: 1

In [9]: comment.article.pk
Out[9]: 1

결과는 동일하지만 article_id는 필드의 값에서 가져온 것이고, article.pk는 참조 인스턴스의 pk를 가져온 것이다.

 

 

 

In [12]: comment.content
Out[12]: 'first comment'

In [13]: comment.article.content
Out[13]: 'hello'

댓글으로 게시글의 내용도 확인할 수 있다.

 

 

 

In [15]: comment = Comment(content = 'second', article=article)

In [16]: comment.save()

In [17]: comment.pk
Out[17]: 2

동일한 article 객체를 이용해서 comment객체를 새로 생성했다.

 

 

같은 article을 참조하고 있다. = 같은 게시글의 댓글이다.

 

 

 

 

Realted manager

N:1 혹은 M:N 관계에서 사용가능한 문맥(CONTEXT)으로 Django는 모델간 N:1, M:N 관계가 설정되면 역참조할 때 사용할 수 있는 manager를 생성한다.

모델 생성시 objects라는 매니저를 통해 queryset api를 사용했던 것 처럼 related manager를 통해 queryset api를 사용할 수 있게 된다.

 

 

 

1:N에서 N은 FK로 1쪽을 참조하고 있었다. 하지만, 1쪽에서는 아무런 변화가 없기때문에 관계를 확인하기 위해서는 역참조를 해야한다.

 

역참조

나를 참조하는 테이블 (나를 외래키로 지정한) 을 참조하는 것

본인을 외래키로 참조중인 다른 테이블에 접근하는 것이다.

N:1 관계에서 1이 N을 참조하는 상황

 

article.comment_set.method()

article 모델이 comment모델을 역참조할때 사용하는 매니저이다.

article 객체에는 comment와의 article.comment 형식으로는 댓글 객체를 참조할 수 없다. article 클래스에는 comment에 대한 어떠한 정보도 없기 때문이다.

django에서 comment_set 매니저를 자동으로 생성하기 때문에 댓글 객체를 참조할 수 있다.

 

 

 

In [18]: article = Article.objects.get(pk=1)

In [19]: article.pk
Out[19]: 1

In [20]: article.comment_set.all()
Out[20]: <QuerySet [<Comment: first comment>, <Comment: second>]>

1번 게시글에 작성된 모든 댓글을 확인했다.

 

 

In [23]: comments= article.comment_set.all()

In [24]: for comment in comments:
    ...:    print(comment.content)
    ...:
first comment
second

이렇게 응용할 수 있다.

 

 

 

class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comments')

class 생성에서 FK의 옵션 중에 related_name을 설정하면 model_set manager 매니저 이름을 변경할 수 있다. 기존 comment_set 대신에 comments를 이용하면 되고, comment_set은 더이상 사용할 수 없다.

작성 후 migration 과정이 필요하다.

related_name을 반드시 작성해야하는 경우도 있다.

 

 

 

 

def comments_create(request, pk):
    article = Article.objects.get(pk=pk)
    comment_form = CommentForm(request.POST)
    if comment_form.is_valid():
        comment = comment_form.save(commit=False) #저장하지않고 인스턴스만 줌
        comment.article = article
        comment.save()
    return redirect('articles:detail', article.pk)

views.py이다.

save(commit=False)으로 저장하지 않고 인스턴스만 준다. 인스턴스 comment.article에 article 객체를 넣어준다. 이것이 없으면 article_id가 존재하지 않아 NULL 오류가 발생한다.

 

 

save(commit=False)

생성하고 아직 데이터베이스에 저장되지 않은 인스턴스를 반환

저장하기 전에 객체에 대한 사용자 지정 처리를 수행할 때 유용하다.

 

 

@require_safe
def detail(request, pk):
    article = Article.objects.get(pk=pk)
    comment_form = CommentForm()
    comments = article.comment_set.all()
    context = {
        'article': article,
        'comment_form': comment_form,
        'comments': comments,
    }
    return render(request, 'articles/detail.html', context)

views.py

게시글에서 댓글을 표시하기 위해서 article.comment_set.all()으로 모든 댓글을 가져온다.

 

 

<ul>
    {% for comment in comments %}
      <li>{{comment.content}} </li>
     {% end for %}
</ul>

detail.html

가져온 comments를  for문을 이용해 하나씩 댓글 목록을 조회한다.

 

 

 

 

댓글 개수 출력은 

1. DTL filter - length

2. Queryset API - count

두가지 방법이 있다. 

{{comment|length}}
{{article.comment_set.all|length}}
{{comments.count}}
{{article.comment_set.count}}

 

 

  <h4>댓글 목록</h4>
  {% if comments %}
    <p>{{ comments|length }}개의 댓글이 있습니다.</p>
  {% endif %}
  <ul>
    {% for comment in comments %}
      <li>
        {{ comment.content }}
          <form action="{% url 'articles:comments_delete' article.pk comment.pk %}" method="POST">
            {% csrf_token %}
            <input type="submit" value="DELETE">
          </form>
      </li>
    {% empty %}
      <li>댓글이 없음</li>
    {% endfor %}
  </ul>

for - empth - endfor 구조로 댓글이 없을 때의 내용을 다르게 사용할 수 있다.