IT/WEB

[Django] 튜토리얼 ③ : Django 모델 만들기

개발자 두더지 2020. 9. 8. 17:28
728x90

 지난 포스팅을 통해 본격적인 앱 만들기의 기본 세팅을 완료했습니다. 지난 포스팅에 이어 계속해서 Django 공식 튜토리얼을 토대로 참고 자료로 내용을 덧붙여 포스팅하도록 하겠습니다. 

 

모델 만들기


 이제 모델을 정의해 보자. 장고는 모델과 데이터베이스를 연결해서 데이터베이스에 영구적으로 데이터를 저장하거나 불러오게 되는데 이를 ORM(Object - Relation Mapping)이라고 한다. 

#polls/models.py

from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

 모델은 클래스로 작성되는데, 각 모델은 django.db.models.Model 을 하위 클래스로하는 클래스로 표현된다. 각 모델에는 여러 개의 클래스 변수가 있다. 각각의 클래스 변수는 모델의 데이터 베이스 필드로 표현된다.

 데이터베이스의 각 필드는 Field 클래스의 인스턴스로 표현된다.

■ CharField ;  문자 자료형 필드 표현

■ DataTimeField ; 날짜와 시간(datetime) 자료형 필드 표현

 각각의 Field 인스턴스의 이름(여기서는 question_text 또는 pub_date 등)은 기계에게 친숙한 형식으로 지정했다. 이 필드명는 python코드에서도 사용되는 동시에 데이터베이스 입장에서는 컬럼명으로 사용된다.

  여기서 몇몇 Field 클래스들에 필수 입력 조건을 설정하였다. 위의 코드의 CharField의 경우 max_length를 입력해야 하도록 하였다. 이것은 데이터 베이스 스키마에서만 필요한 것이 아니라 값을 검증할 때도 사용되는데 곧 확인할 수 있을 것이다. 

 여기서 사용된 Forignkey를 이용한 관계설정을 통해 각각의 Choice가 하나의 Question에 관계된다는 것을 장고에게 알려주고 있다. 이러한 문법에 대해서는 다른 포스팅으로 조금 더 자세히 다루고자 한다. 장고는 다대일(many-to-one), 다대대(many-to-many), 일대일(one-to-one)과 같은 모든 일반 데이터베이스 관계를 지원한다.

 

모델 활성화


위의 간략한 모델 코드는 장고에게 엄청난 양의 정보를 전달한다. 장고는 그 정보를 이용해 다음과 같은 일을 한다.

■ 앱의 데이터베이스 스키마 생성(CREATE TABLE문)

■ Question과 Choice 객체에 접근하기 위한 Python 데이터베이스 접근 API 생성

 그러나 지금 당장은 프로젝트에게 polls 앱이 설치되어 있다는 것을 알려야한다. 앱을 프로젝트에 설치되어 있다는 것을 알리기 위해 mysite/setting.py의 INSTALLED_APPS를 수정해주면 된다.  

# mysite/settings.py

INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

 이제 장고는 polls앱이 존재한다는 것을 알게 됐다. 이후 cmd창을 열어 프로젝트(여기에서는 mystie) 디렉토리로 이동한 후 아래의 명령어로 장고에게 새로운 모델을 만들었다는 것을 알려준다.

python manage.py makemigrations polls

 문제가 없다면 아래와 같은 커맨드 라인을 확인할 수 있을 것이다.

Migrations for 'polls':
  polls/migrations/0001_initial.py
    - Create model Question
    - Create model Choice

 makemigration을 실행시키는 것으로 모델(데이터 베이스 스키마)을 변경 혹은 신규 작성했다는 사실과 이 변경사항을 migration으로 저장시키고 싶다는 것을 장고에게 알려주는 것이 된다. 

 migration이 내부적으로 어떤 문장을 실행하는지 살펴보고 싶다면, cmd창에서 아래의 명령어를 실행시켜 볼 수 있다.

python manage.py sqlmigrate polls 0001

  가독성을 위해 결과물을 다듬은 출력물은 다음과 같다.

BEGIN;
--
-- Create model Question
--
CREATE TABLE "polls_question" (
    "id" serial NOT NULL PRIMARY KEY,
    "question_text" varchar(200) NOT NULL,
    "pub_date" timestamp with time zone NOT NULL
);
--
-- Create model Choice
--
CREATE TABLE "polls_choice" (
    "id" serial NOT NULL PRIMARY KEY,
    "choice_text" varchar(200) NOT NULL,
    "votes" integer NOT NULL,
    "question_id" integer NOT NULL
);
ALTER TABLE "polls_choice"
  ADD CONSTRAINT "polls_choice_question_id_c5b4b260_fk_polls_question_id"
    FOREIGN KEY ("question_id")
    REFERENCES "polls_question" ("id")
    DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id");

COMMIT;

이러한 출력결과에 따른 주의항목은 아래와 같고, 참고하길 바란다.

- 위의 출력결과는 사용한 데이터 베이스인 PostgreSQL의 문법을 따른 것이며, 데이터 베이스에 따라 조금씩 다를 수 있다.

- 테이블 명은 어플리케이션의 이름(polls)와 모델의 소문자 표기의 question와 choice를 합친 형태로 자동 생성된다 (이와 같은 작동은 오버라이드(override)로 수정할 수 있다).

- primary key,ID는 자동적으로 추가된다 (이와 같은 작동은 오버라이드(override)로 수정할 수 있다).

- 편의상, 장고는 Foreign key(외부키)의 필드명에 "_id'를 추가한다 (이와 같은 작동은 오버라이드(override)로 수정할 수 있다).

Foreign key(외부키) 관계는 Foreign key제약으로명확화된다.DEFERRABLE에 대해서는 걱정할 필요없다. 이것은 단지 PostgreSQL에서 외부키를 트랙잭션 종료까지 강제하지 않도록하기 위해 전달하고 있을 뿐이다.

- sqlmigrate 명령어는 실제로 데이터베이스에 마이그레이션을 실행하지 않는다. 단지 장고가 필요로 하는 SQL이 무엇인가를 화면에 표시할 뿐이다. 이것을 통해 장고가 무엇을 하려고 하는가 확인하거나, 데이터 베이스 관리자에게 변경을 위한 SQL스크립트 요청 등에 도움이 된다.

 이제 migrate 명령어를 cmd창에서 실행시켜 데이터베이스 모델과 관련된 테이블을 생성하자.

python manage.py migrate

 migrate 명령어는 아직 적용되지 않은 마이그레이션을 모두 수집해 실행하며, 이 과정을 통해 모델의 변경 사항과 데이터 베이스의 스키마가 동기화된다. 

 모델의 신규 작성, 변경의 세 단계를 꼭 기억하길 바란다. 

① model.py에서 모델을 작성-변경한다.

② python manage.py makemigrations을 통해 이 작성-변경사항에 대한 마이그레이션을 만든다. 

③ python manage.py migrate 명령어를 통해 데이터베이스에 적용한다. 

 

Django API이용하기


Python Shell을 이용할 것이다. Python shell을 작동시키기 위해 cmd창에서 다음과 같은 명령을 입력한다. 

python manage.py shell

( Shell 입력을 종료하고 싶다면 quit()명령어를 입력하면 된다. )

 Shell에 진입하였다면 여러 명령어를 이용하여 API를 탐색해보자.

>>> from polls.models import Choice, Question  # Import the model classes we just wrote.

# No questions are in the system yet.
>>> Question.objects.all()
<QuerySet []>

# Create a new Question.
# Support for time zones is enabled in the default settings file, so
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
# instead of datetime.datetime.now() and it will do the right thing.
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())

# Save the object into the database. You have to call save() explicitly.
>>> q.save()

# Now it has an ID.
>>> q.id
1

# Access model field values via Python attributes.
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)

# Change values by changing the attributes, then calling save().
>>> q.question_text = "What's up?"
>>> q.save()

# objects.all() displays all the questions in the database.
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>

 마지막 출력 결과인 <QuerySet [<Question: Question object (1)>]>는 좋은 객체 표현법이 아니므로 polls/models.py 파일에 __str__()메소드를 Question과 Choice에 추가해보자. __str__() 메소드를 추가하는 것은 객체의 표현을 대화식 프롬프트에서 편하게 보려는 이유 말고도, 장고가 자동으로 생성하는 관리 사이트에서도 객체의 표현이 사용되기 때문이다. 

 __str__() 추가한 전체 코드는 아래와 같다. 

# polls/models.py

from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
    def __str__(self):
        return self.question_text


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)
    def __str__(self):
        return self.choice_text

 그리고 여기서 커스텀 메소드를 하나 더 추가해보자. 

# polls/models.py

import datetime

from django.db import models
from django.utils import timezone


class Question(models.Model):
    # ...
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

■ import datetime ; Python의 표준 모듈인 datetime모듈

■ from django.utils import timezone ; Django의 시간대 관련 유틸리티인 django.utils.timezone 참조

 변경 사항을 저장하고, cmd 창에서 python manage.py shell을 다시 실행해보자.

>>> from polls.models import Choice, Question

# Make sure our __str__() addition worked.
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>

# Django provides a rich database lookup API that's entirely driven by
# keyword arguments.
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>

# Get the question that was published this year.
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>

# Request an ID that doesn't exist, this will raise an exception.
>>> Question.objects.get(id=2)
Traceback (most recent call last):
    ...
DoesNotExist: Question matching query does not exist.

# Lookup by a primary key is the most common case, so Django provides a
# shortcut for primary-key exact lookups.
# The following is identical to Question.objects.get(id=1).
>>> Question.objects.get(pk=1)
<Question: What's up?>

# Make sure our custom method worked.
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True

# Give the Question a couple of Choices. The create call constructs a new
# Choice object, does the INSERT statement, adds the choice to the set
# of available choices and returns the new Choice object. Django creates
# a set to hold the "other side" of a ForeignKey relation
# (e.g. a question's choice) which can be accessed via the API.
>>> q = Question.objects.get(pk=1)

# Display any choices from the related object set -- none so far.
>>> q.choice_set.all()
<QuerySet []>

# Create three choices.
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)

# Choice objects have API access to their related Question objects.
>>> c.question
<Question: What's up?>

# And vice versa: Question objects get access to Choice objects.
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3

# The API automatically follows relationships as far as you need.
# Use double underscores to separate relationships.
# This works as many levels deep as you want; there's no limit.
# Find all Choices for any question whose pub_date is in this year
# (reusing the 'current_year' variable we created above).
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

# Let's delete one of the choices. Use delete() for that.
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()

참고자료

wayhome25.github.io/django/2017/02/28/django-03-lotto-project-3/

728x90