Mastering Django Core: Chapter 4 Models

웹사이트들은 보통 데이터베이스(DB)를 이용합니다. Django 역시 데이터베이스를 이용할 수 있습니다.

View에서 DB Query를 하는 기본적인 방법

SQL query를 지원하는 파이썬 라이브러리를 사용해서 DB query를 할 수도 있습니다:

from django.shortcuts import render
import MySQLdb

def book_list(request):
    db = MySQLdb.connect(user='me', db='mydb',  passwd='secret', host='localhost')
    cursor = db.cursor()
    cursor.execute('SELECT name FROM books ORDER BY name')
    names = [row[0] for row in cursor.fetchall()]
    db.close()
    return render(request, 'book_list.html', {'names': names})

하지만 이 방법은 몇가지 문제점들이 있습니다:

  1. 중요한 DB 연결 정보가 소스 코드에 직접 입력 되어있음 (hard code)

  2. Boilerplate 코드: DB query를 할 때마다 DB에 연결하고 cursor를 만드는 등의 절차를 진행해야 함

  3. DB의 추상화가 되지 않음: DB 플랫폼의 종류 (MySQL, PostgreSQL)에 따라 문법이 다르기 때문에 코드를 재 작성해야 함

Django는 이런 문제점들을 해결해주는 DB 레이어를 제공하고 있습니다.

DB 설정

Django 프로젝트를 만들 때 자동으로 만들어진 settings.py를 보면 DB 세팅 정보가 저장되어 있습니다.

# Database
#...
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
  • ENGINE은 Django에 사용되는 DB 플랫폼의 타입을 설정함 (위에서는 sqlite3가 설정됨)

  • NAME은 DB의 이름을 설정함 (e.g. ‘NAME’: ‘mydb’)

Project VS App

Django에 대해 더 설명하기 전에 용어를 명확히 집고 넘어갑시다.

  • 프로젝트는 Django 앱들과 그 앱들의 설정을 포함한 인스턴스를 지칭함 (프로젝트 유일한 필수조건은 설정파일임)

  • 앱은 Django의 기능들을 포함하고 있는 포터블한 파이썬 패키지로, 보통 model과 view들을 포함하고 있음 (다양한 프로젝트에서 재사용 가능함)

프로젝트가 존재하기 위해서 앱을 꼭 생성할 필요는 없지만, 만약 Django의 DB layer (model)을 사용하려면 필수적으로 앱이 있어야 합니다. 하지만 프로젝트가 매우 간단하지 않다면 나중에 기능들을 다른 프로젝트에서 재사용할 수 있기 때문에 앱을 생성하는 것을 권장합니다.

Models

Django의 model은 (SQL의 SQL CREATE TABLE 구문과 같이) DB의 데이터를 파이썬 코드로 표현된 설명입니다. Django는 model을 이용해서 SQL 구문을 실행시키며, (SQL만으로는 불가능한) 데이터를 하이 레벨로 표현할 수 있게 하였습니다.

Django가 SQL을 사용하지 않고 파이썬으로 DB 레이어를 표현한 이유들은 아래와 같습니다:

  • Django가 데이터의 레이아웃을 알기 위해서는 두가지 방법이 있음: 1. 데이터의 구조를 파이썬으로 표현하거나 2. 런타임에 DB introspection(메타데이터 조사)을 실행해야 함

    Introspection이 DB 테이블의 메타데이터가 (SQL 쪽) 한 장소에만 머물러 있기 때문에 더 좋아보이지만, (introspection을 DB에 request할 때 하든 웹서버든 처음 시작할때 하든) 런타임에 intropsection을 하는 것은 오버헤드를 발생시킵니다 - Django는 프레임워크의 오버헤드를 최소화 하는 디자인 철학을 갖고 개발되었습니다. 또한 몇몇 DB플랫폼들은 정밀하고 완벽한 intropsection을 위한 메타데이터를 모두 저장할 수 없으며 (예: email 주소나 URL 등의 데이터) 데이터를 하이 레벨 컨셉으로 표현하기 위한 메타데이터 역시 모두 지원하지 않습니다.

  • 모든 코드를 한가지 언어(파이썬)만으로 구현함에 따라 개발자의 머리가 context switch를 하지 않아도 됨

  • 데이터 모델을 코드에 저장함으로써 model이 쉽게 버전관리 될 수 있음 (데이터 레이아웃의 변화를 쉽게 확인할 수 있음)

  • SQL은 DB 플랫폼들이 일관성이 없음 (예: MySQL과 PostgreSQL은 문법이 틀림)

DB 레이어를 파이썬으로 작성한 방법의 단점은 DB와의 싱크가 맞지 않을 수 있다는 점입니다. 하지만 Django는 이러한 문제를 해결하기 위한 방법을 갖고 있으며 기존에 (다른 프레임워크랑) 사용하던 데이터도 이동할 수 있는 방법을 갖고 있습니다.

Model에 데이터 레이아웃 정의하기

model에 대해 더 설명하기 위해서 책/작가/출판사 데이터 레이아웃을 사용할 것이며, 이에 대한 컨셉은 아래와 같습니다:

  • 작가는 성, 이름, email 주소를 갖고 있음

  • 출판사는 이름, 도로주소, 도시, 주, 국가 그리고 웹사이트 주소를 갖고 있음

  • 책은 제목과 출판일을 갖고 있으며, 한명 이상의 작가와 (many to many 관계) 하나의 출판사(one to many 관계 - 출판사의 foreign key)를 갖고있음

먼저 books라고 불리울 app을 만들면,

python manage.py startapp books

아래와 같은 파일 구조가 생성됩니다:

mysite/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        wsgi.py    
    books/
        /migrations
        __init__.py
        admin.py
        models.py
        tests.py
        views.py

‘books/models.py’에 model을 생성합니다.

from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()

class Author(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField()

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()

model 클래스는 DB와 상호작용을 하는 기능들이 포함된 django.db.models.Model의 서브클래스로 만듭니다. 덕분에 Django는 위의 코드를 이용해서 자동으로 CREATE TABLE과 같은 구문을 생성합니다.

하나의 model은 하나의 DB 테이블을 표현하고 있으며, 각 model attribute는 DB table의 열을 표현합니다. attribute의 이름은 DB table 열의 이름이 되고 field의 타입은 DB 열이 갖고 있을 수 있는 타입을 정의합니다 (예: CharField -> varchar).

보통 Django는 하나의 클래스당 하나의 DB 테이블을 생성하지만, many to many 관계에서는 추가적인 ‘join’ 테이블을 생성해서 관계를 처리합니다.

또한, Django에 따로 정의하지 않으면, 자동으로 하나씩 증가하는 정수의 primary key (id)가 각 model에 생성됩니다.

Model 적용

model을 정의했으니 이제 DB에 테이블을 만들어 봅시다.

먼저 books 앱을 Django Project에 등록시켜 이 model들을 활성화 시킵시다. 설정파일 ‘settings.py’의 INSTALLED_APPS 항목에 앱의 이름을 추가해주면 앱이 등록됩니다.

INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'books',    # Add the books app
)

DB 테이블을 생성하기 전에 아래의 명령어를 사용해서 에러가 없는지 확인할 수 있습니다 (몇가지 정적(static)검사를 통해 Django 프로젝트가 문제 없는지 확인하는 명령어).

python manage.py check

이제 아래의 명령어를 실행시킵니다 (아래의 명령어는 model이 바뀔 때 마다 실행되어야 합니다).

python manage.py makemigrations books

migration은 Django가 model의 변화를 저장하게 합니다. 현재의 경우에는 ‘0001_initial.py’ 파일이 books앱 ‘migrations’ 폴더 안에 생성이 됩니다. migration 파일이 생성되면 아래의 명령어를 통해 어떤 SQL 구문들이 실행될 것인지 확인할 수 있습니다.

python manage.py sqlmigrate books 0001

이 명령어는 아래의 결과물을 보여줍니다 (자동으로 SQL 플랫폼에 맞춰 보여주며 아래는 Postgre 문법으로 작성되었습니다):

BEGIN;

CREATE TABLE "books_author" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "first_name" varchar(30) NOT NULL,
    "last_name" varchar(40) NOT NULL,
    "email" varchar(254) NOT NULL
);
CREATE TABLE "books_book" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "title" varchar(100) NOT NULL,
    "publication_date" date NOT NULL
);
CREATE TABLE "books_book_authors" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "book_id" integer NOT NULL REFERENCES "books_book" ("id"),
    "author_id" integer NOT NULL REFERENCES "books_author" ("id"),
    UNIQUE ("book_id", "author_id")
);
CREATE TABLE "books_publisher" (
Chapter 4: Models 94
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "name" varchar(30) NOT NULL,
    "address" varchar(50) NOT NULL,
    "city" varchar(60) NOT NULL,
    "state_province" varchar(30) NOT NULL,
    "country" varchar(50) NOT NULL,
    "website" varchar(200) NOT NULL
);
CREATE TABLE "books_book__new" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "title" varchar(100) NOT NULL,
    "publication_date" date NOT NULL,
    "publisher_id" integer NOT NULL REFERENCES
    "books_publisher" ("id")
);

INSERT INTO "books_book__new" ("id", "publisher_id", "title",
"publication_date") SELECT "id", NULL, "title", "publication_date" FROM
"books_book";

DROP TABLE "books_book";

ALTER TABLE "books_book__new" RENAME TO "books_book";

CREATE INDEX "books_book_2604cbea" ON "books_book" ("publisher_id");

COMMIT;
  • 테이블 이름을 자동으로 앱 이름과 모델의 소문자 이름을 조합하여 만들어짐 (따로 정의할 수도 있음)

  • Django가 자동으로 primary key를 만들어주며, foreign key의 경우 테이블 이름과 ‘_id’를 합쳐서 이름을 생성함

  • foreign key 관계를 명시적으로 REFERENCE를 이용해서 표현함

이 SQL 구문들을 직접 복사 붙여넣기 해서 테이블을 만들수도 있지만 편의를 위해서 Django는 아래의 명령어를 지원합니다:

python manage.py migrate

기본적인 데이터 사용

>>> from books.models import Publisher
>>> p1 = Publisher(name='Apress', address='2855 Telegraph Avenue',
...     city='Berkeley', state_province='CA', country='U.S.A.',
...     website='http://www.apress.com/')
# Publisher object is not stored in the database until save is called
>>> p1.save()
>>> p2 = Publisher(name="O'Reilly", address='10 Fawcett St.',
...     city='Cambridge', state_province='MA', country='U.S.A.',
...     website='http://www.oreilly.com/')
>>> p2.save()
>>> publisher_list = Publisher.objects.all()  # get all Publisher objects
>>> publisher_list
[<Publisher: Publisher object>, <Publisher: Publisher object>]

DB의 출판사 테이블을 사용하기 위해서 model이 먼저 import 되어야 합니다. 그 후 기존의 객채를 다루듯 출판사 객체를 인스턴스화 시킵니다. 하지만 이 객체는 save()가 불리가 전까지 DB에 저장되지 않습니다 (SQL INSERTsave()가 불릴 때 실행됨).

출판사 객체를 DB에서 가져오고 사용하려면 Publisher.objects를 이용해서 model manger(테이블 레벨의 연산을 담당하는 객체)를 불러와야 합니다.

한 문장으로 객체를 생성하고 DB에 저장하려면 create()를 사용할 수도 있습니다.

# No save is required
>>> p1 = Publisher.objects.create(name='Apress', address='2855 Telegraph Avenue',
...     city='Berkeley', state_province='CA', country='U.S.A.',
...     website='http://www.apress.com/')

Model의 str 표현

그 전의 코드에서 볼 수 있다시피, 출판사 객체를 출력해도 도움이 되는 정보를 얻을 수 없습니다.

[<Publisher: Publisher object>, <Publisher: Publisher object>]

model의 __str__ method를 정의하면 더 도움이 되는 정보를 얻을 수 있게 됩니다.

from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()
    def __str__(self):
        return self.name

class Author(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField()
    def __str__(self):
        return u'%s %s' % (self.first_name, self.last_name)

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()
    def __str__(self):
        return self.title

__str__은 str 객체를 리턴해야하며 만약 리턴 타입이 str이 아니면 TypeError가 발생합니다.

이제 출판사 객체를 출력하면:

>>> publisher_list = Publisher.objects.all()
>>> publisher_list
[<Publisher: Apress>, <Publisher: O'Reilly>]

__str__ 예제에서 볼 수 있다 시피, Django model은 DB 테이블의 레이블 이상의 정보를 저장할 수 있습니다 - 객체가 해야할 기능을 정의할 수도 있습니다.

데이터 Insert/Update

전에 말했듯이 새로운 객체가 (save()를 이용하여) DB에 추가될 때, Django는 자동으로 객체에 id를 부여합니다. 따라서 만약 save()가 같은 객체에 한번 더 불린다면, Django는 INSERT 구문 대신 UPDATE를 실행합니다.

>>> p = Publisher(name='Apress',
...         address='2855 Telegraph Ave.',
...         city='Berkeley',
...         state_province='CA',
...         country='U.S.A.',
...         website='http://www.apress.com/')
>>> p.save()    # INSERT data
>>> p.name = 'Apress Publishing'
>>> p.save()    # UPDATE data

UPDATE 구문을 대략 SQL로 표현하면:

UPDATE books_publisher SET
    name = 'Apress Publishing',
    address = '2855 Telegraph Ave.',
    city = 'Berkeley',
    state_province = 'CA',
    country = 'U.S.A.',
    website = 'http://www.apress.com'
WHERE id = 52;

위에 보다시피, name 필드만 업데이트하지 않고, 모든 필드를 업데이트 하게 됩니다.

객체들 선택

모든 출판사 데이터는 전에 본 것처럼 all()을 이용해서 가져올 수 있습니다.

>>> Publisher.objects.all()
[<Publisher: Apress>, <Publisher: O'Reilly>]

이는 QuerySet 객체를 리턴하며 이는 DB의 행들을 표현하고 있습니다.

그리고 이를 대략 SQL으로 표현하면 아래와 같습니다:

SELECT id, name, address, city, state_province, country, website
FROM books_publisher;

Django는 ‘Zen of Python’에 명시된 아래의 글 처럼 *를 사용하기보다 모든 필드를 명시하고 있습니다:

“명시가 암시보다 낫다.”

데이터 필터링

데이터의 특정 행만 가져오기 위해서는 filter()를 사용할 수 있습니다:

>>> Publisher.objects.filter(name='Apress')
[<Publisher: Apress>]

키워드 인자를 사용해서 WHERE 구문으로 데이터를 필터링 할 수 있습니다. 그리고 여러개의 키워드 인자를 사용하게 되면 WHERE구분에 AND 조건으로 필터링을 할 수 있습니다.

>>> Publisher.objects.filter(country="U.S.A.",
state_province="CA")
[<Publisher: Apress>]

또한 필터링은 특수한 키워드와 attribute의 이름을 __를 이용해 합쳐서 다양한 검색을 할수도 있습니다 - contains (LIKE), icontains (대소문자를 구분하지 않는 LIKE), startswith, endswith, range 등.

>>> Publisher.objects.filter(name__contains="press")
[<Publisher: Apress>]

하나의 객체만 가져오기

filter()는 list와 비슷한 Queryset 오브젝트를 리턴히지만, 만약 하나의 객체만 가져오고 싶으면 get()을 이용할 수도 있습니다.

>>> Publisher.objects.get(name="Apress")
<Publisher: Apress>

단 이는 하나의 객체만 가져오는 기능임으로, 만약 조건이 0개 혹은 2개 이상의 객체를 만족하면 에러가 발생합니다.

# Multiple objects
>>> Publisher.objects.get(country="U.S.A.")
Traceback (most recent call last):
    ...
MultipleObjectsReturned: get() returned more than one Publisher -- it returned 2! Loo\
kup parameters were {'country': 'U.S.A.'}

# No object
>>> Publisher.objects.get(name="Penguin")
Traceback (most recent call last):
    ...
DoesNotExist: Publisher matching query does not exist.

DoesNotExist 에러는 model 클래스의 attribute이기 때문에 이를 이용해서 try except 구문을 작성할 수도 있습니다.

try:
    p = Publisher.objects.get(name='Apress')
except Publisher.DoesNotExist:
    print ("Apress isn't in the database yet.")
else:
    print ("Apress is in the database.")

데이터 정렬

객체들은 랜덤한 순서로 리턴됩니다. 따라서 order_by()를 이용해서 리턴되는 데이터를 정렬시킬 수 있습니다(ORDER BY SQL 구문).

>>> Publisher.objects.order_by("name")
[<Publisher: Apress>, <Publisher: O'Reilly>]

여려개의 인자를 사용해서 첫번째 필드의 순서가 같으면 그 다음 순서를 정해줄 수 있습니다.

>>> Publisher.objects.order_by("state_province", "address")
 [<Publisher: Apress>, <Publisher: O'Reilly>]

- (빼기)를 사용하게 되면 역순으로 정렬할 수 있습니다.

>>> Publisher.objects.order_by("-name")
[<Publisher: O'Reilly>, <Publisher: Apress>]

order_by() 구문을 매번 추가하기는 귀찮을 수 있기 때문에, model에 기본 정렬 순서를 정의하여 사용할 수 있습니다.

class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()

    def __str__(self):
        return self.name

    class Meta:
        ordering = ['name']
  • Meta 클래스를 사용하여 어느 model에서나 model 지정 옵션을 정의할 수 있음

검색 Chain

query 구문들이 Queryset을 리턴하기 때문에 query 구문들을 체인시킬 수 있습니다.

>>> Publisher.objects.filter(country="U.S.A.").order_by("-name")
[<Publisher: O'Reilly>, <Publisher: Apress>]

데이터 Slicing

list처럼 Queryset에 slicing를 이용할 수있습니다.

>>> Publisher.objects.order_by('name')[0]
<Publisher: Apress>
>>> Publisher.objects.order_by('name')[0:2]
[<Publisher: Apress>, <Publisher: O'Reilly>]
# Negative indexing is not working
>>> Publisher.objects.order_by('name')[-1]
Traceback (most recent call last):
  ...
AssertionError: Negative indexing is not supported.

하나의 구문으로 복수의 객체를 업데이트하기

save()는 변경하지 않은 필드를 포함해서 모든 필드든 업데이트 합니다. 대신 update()QuerySet 객체에 이용해서 변경된 필드만 업데이트 할 수 있습니다.specified.

>>> Publisher.objects.filter(id=52).update(name='Apress Publishing')

위의 코드를 대략 SQL로 표현하면 아래와 같습니다:

UPDATE books_publisher
SET name = 'Apress Publishing'
WHERE id = 52;

update()Queryset객체의 메서드이기 때문에 이를 이용해서 복수의 데이터를 한번에 업데이트 할 수도 있습니다:

# Update returns the number of rows updated
>>> Publisher.objects.all().update(country='USA')
2

Deleting Objects

DB 데이터를 없애려면 delete()를 사용할 수 있습니다.

>>> p = Publisher.objects.get(name="O'Reilly")
>>> p.delete()
>>> Publisher.objects.all()
[<Publisher: Apress Publishing>]
# Multiple deletion
>>> Publisher.objects.filter(country='USA').delete()
>>> Publisher.objects.all().delete()
>>> Publisher.objects.all()
[]