장고의 명령어 중, migrate, makemigrations 와 같은 명령어가 있습니다. 이러한 명령어는 models.py에 정의된 모델의 생성/변경 내역을 히스토리 관리데이터베이스에 적용 등과 같은 기능을 제공하여 손쉽게 데이터베이스의 구조를 바꿀 수 있습니다.

Migration 관련 명령어

# 마이그레이션 파일 생성
$ python manage.py makemigrations <app-name>

# 마이그레이션 적용
$ python manage.py migrate <app-name>

# 마이그레이션 적용 현황
$ python manage.py showmigrations <app-name>

# 지정 마이그레이션의 SQL 내역
 python manage.py sqlmigrate <app-name> <migration-name>

makemigrations

$ python manage.py makemigrations test

Migrations for 'test':
  test/migrations/0001_initial.py:
    - Create model Choice
    - Create model Question
    - Add field question to choice


makemigrations을 실행하면 모델을 변경시킨 사실 또는 새로 생성한 모델들과 같은 변경사항을 migrations로 저장하고자 Django에게 알려줍니다. migration은 Django가 모델의 변경사항을 저장하는 방법으로써, 디스크상의 파일로 존재합니다. 원한다면, <app-name>/migrations/0001_initial.py 파일로 저장된 새 모델에 대한 migration을 읽어볼 수 있습니다. 또 수동으로 Django의 변경점을 조정하고 싶을 때 직접 변경할 수 있습니다.


migration들을 실행시켜주고, 자동으로 데이터베이스 스키마를 관리해주는 migrate 명령어가 있습니다. 이 명령을 알아보기 전에 migration이 내부적으로 어떤 SQL 문장을 실행하는지 살펴봅시다. sqlmigrate 명령은 migration 이름을 인수로 받아, 실행하는 SQL 문장을 보여줍니다.

migrate

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, test, sessions
Running migrations:
  Rendering model states... DONE
  Applying test.0001_initial... OK


적용되지 않은생성/변경된 migrations들의 파일을 데이터베이스에 적용합니다. 이때 Django는 django_migrations 테이블을 두고 마이그레이션 적용 여부를 추적합니다. 이를 사용하면 데이터베이스를 직접 접근하지 않고도 모델의 반복적인 변경을 가능하게 해줍니다. 이처럼 마이그레이션을 만드는 명령과 적용하는 명령이 분리된 것은 버전 관리 시스템에 마이그레이션을 커밋하고 앱과 함께 출시할 수 있도록 하기 위해서라고 장고 공식 문서에 나와 있습니다.

모델의 생성/변경을 적용하는 단계는 아래와 같습니다.

  1. models.py 추가 및 변경
  2. python3 manage.py makemigrations를 실행해 변경사항에 대한 마이그레이션 파일 생성
  3. python3 manage.py migrate를 실행해 변경사항을 데이터베이스에 적용

showmigration

현재 적용된 마이그레이션 파일을 보여줍니다. 

$ python manage.py showmigrations

auth
[ ] 0001_initial
[ ] 0002_alter_permission_name_max_length
[ ] 0003_alter_user_email_max_length
[ ] 0004_alter_user_username_opts
[ ] 0005_alter_user_last_login_null
[ ] 0006_require_contenttypes_0002
[ ] 0007_alter_validators_add_error_messages
[ ] 0008_alter_user_username_max_length
contenttypes
[ ] 0001_initial
[ ] 0002_remove_content_type_name
sessions
[ ] 0001_initial


sqlmigrate

생성된 migrations파일들이 어떤 sql 문장을 실행하는지 보여줍니다.

$ python manage.py sqlmigrate test 0001

BEGIN;
--
-- Create model Choice
--
CREATE TABLE "test_temp" (
"id" serial NOT NULL PRIMARY KEY,
"name" varchar(200) NOT NULL
);
--
-- Create model Question
--
CREATE TABLE "test_temp2" (
"id" serial NOT NULL PRIMARY KEY,
"name" varchar(200) NOT NULL
);
--
-- Add field question to choice
--
ALTER TABLE "test_temp" ADD COLUMN "temp2_id" integer NOT NULL;
ALTER TABLE "test_temp" ALTER COLUMN "temp2_id" DROP DEFAULT;
CREATE INDEX "test_temp_7aa0f6ee" ON "test_temp" ("temp2_id");
ALTER TABLE "test_temp"
ADD CONSTRAINT "test_temp_temp2_id_246c99a640fbbd72_fk_test_temp2_id"
FOREIGN KEY ("temp2_id")
REFERENCES "test_temp2" ("id")
DEFERRABLE INITIALLY DEFERRED;

COMMIT;

만들어진 migrations의 sql은 아래와 같은 규칙이 있습니다.

  1. 테이블의 이름은 앱의 이름과 모델의 이름(소문자)가 조합되어 자동으로 생성. 위의 예시는 test 앱과 temp, temp2 모델명이 합쳐진 케이스
  2. 만약 pk가 지정되어 있지 않으면 자동으로 id라는 컬럼을 생성하고 pk로 지정
  3. 관례적으로 Djanngo는 외래키 컬럼의 마지막에 _id를 자동으로 추가
  4. sqlmigrate 명령어를 실행해도 실제로 데이터베이스의 마이그레이션을 실행하지 않음.


Django FilterSet 라이브러리는 GET 요청을 받고 쿼리 파라미터를 제어하는 기능을 제공합니다.

아래는 기본적으로 filterset을 사용한 예제입니다.

http://127.0.0.1:8000/test?name=kim


class NameFilter(filters.FilterSet):
    name = django_filters.CharFilter(
        name='name', lookup_expr='icontains'
    )


만약 위와 같은 구조가 아닌 여러 컬럼에 접근해야 한다면 아래와 같이 method를 지정하여 처리를 하도록 합니다.

http://127.0.0.1:8000/test?name=kim


class NameFilter(filters.FilterSet):
    name = django_filters.CharFilter(
        method='custom_name_filter'
    )

    def custom_name_filter(self, queryset, value, *args):
        return queryset.filter(name=args[0], nickname=args[0])


또는 아래와 같이 하나의 필드에 여러 값을 받은 다음 처리할 수 있습니다.

http://127.0.0.1:8000/test?multi=kim,5123



class MultiFilter(filters.FilterSet):
    multi = django_filters.CharFilter(
        method='custom_multi_filter'
    )

    def custom_multi_filter(self, queryset, value, *args):
        temp = args[0].split(',')
        name_list = []
        no_list = []
        
        for data in temp:
            if data.isdigit():
                no_list.append(int(data))
            else:
                name_list.append(data)

        return queryset.filter(name__in=name_list, no__in=no_list)


1. 기본 세팅

엘라스틱 서치 설치 : https://www.elastic.co/kr/downloads/elasticsearch

한글 형태소 분석기 노리 설치: https://www.elastic.co/kr/blog/nori-the-official-elasticsearch-plugin-for-korean-language-analysis

파이썬3 엘라스틱서치 패키지 설치: https://elasticsearch-py.readthedocs.io/en/master/

2. 파이썬 Elastic Search 세팅

아래는 위에서 설치 받은 한글 형태소 분석기인 nori를 특정 필드에 적용도록 하는 세팅 방법 입니다.

from elasticsearch import Elasticsearch

# es 실행 기본포트: 9200, 기본 ip: 루프백 아이피 - 127.0.0.1 (localhost)
es = Elasticsearch()

# test-index 인덱스가 이미 사용하고 있을 시, 삭제
es.indices.delete(index='test-index', ignore=[400, 404])
body = {
    "settings": {
        "index": {
            "analysis": {
				
                "tokenizer": {
                    "nori_tokenizer": {
                        "type": "nori_tokenizer",
                    },
                },
                "analyzer": {
					# nori 분석기 설정
                    "nori_korean": {
                        "type": "custom",
                        "tokenizer": "nori_tokenizer"
                    },
                }
            }
        }
    },
    "mappings": {
        "goods": {
            "properties": {
                "name": {
                    "type": "text",
					# name에 nori 형태소 분석기 설정
                    "analyzer": "nori_korean",
                },
                "description": {
                    "type": "text",
                },
                "name_eng": {
                    "type": "text"
                },
                "pid": {
                    "type": "integer"
                },
            }
        }
    }
}
es.indices.create(index='test-index', body=body)
# 테스트 데이터
goods = [
    {
        "name": '주름원피스',
        "name_eng": "Pleated dress",
        "pid": 0,
        "description": '주름진 원피스'
    }
]
for i, data in enumerate(goods):
    res = es.index(index="test-index", doc_type='goods', body=data, id=i)
    print(res['result'])


3. 검색하기

query = {
    "query": {
        "bool": {
            "should": [
                {
                    "match": {
                        "name": {
                            "query": "주름", "boost": 3
                        }
                    }
                }
            ]
        }
    }
}

res = es.search(index="test-index", body=query)


Django 모델에서 Manager는 데이터베이스와 상호 작용하는 인터페이스입니다. 기본적으로 Manager는 Model.objects 속성을 통해 사용할 수 있습니다. Django 모델마다 기본적으로 사용되는 기본 관리자는 django.db.models.Manager입니다.

from django.db import models

class DocumentManager(models.Manager):
    def pdfs(self):
        return self.filter(file_type='pdf')

    def smaller_than(self, size):
        return self.filter(size__lt=size)

class Document(models.Model):
    name = models.CharField(max_length=30)
    size = models.PositiveIntegerField(default=0)
    file_type = models.CharField(max_length=10, blank=True)

    objects = DocumentManager()


위 예시에서 filter_type='pdf'를 선언하는 방법은 아래와 같습니다. 또한 호출 후, filter()와 order_by()같이 추가적으로 연결하여 사용할 수 있습니다.

Document.objects.pdfs()


Document.objects.pdfs().filter(name='test')
Document.objects.pdfs().order_by('name')


하지만 Manager에 선언한 다른 메소드를 호출하고자 하면 오류가 발생합니다.

Document.objects.pdfs().smaller_than(1000)


# AttributeError: 'QuerySet' object has no attribute 'smaller_than'


이 문제를 해결하는 것은 Custom QuerySet을 선언하는 것입니다.

class DocumentQuerySet(models.QuerySet):
    def pdfs(self):
        return self.filter(file_type='pdf')

    def smaller_than(self, size):
        return self.filter(size__lt=size)

class DocumentManager(models.Manager):
    def get_queryset(self):
        return DocumentQuerySet(self.model, using=self._db)  # 중요

    def pdfs(self):
        return self.get_queryset().pdfs()

    def smaller_than(self, size):
        return self.get_queryset().smaller_than(size)

class Document(models.Model):
    name = models.CharField(max_length=30)
    size = models.PositiveIntegerField(default=0)
    file_type = models.CharField(max_length=10, blank=True)

    objects = DocumentManager()




Document.objects.pdfs().smaller_than(1000).exclude(name='Article').order_by('name')


만약 Custom QuerySet만 정의하고자 하면 아래와 같이 선언하면 됩니다.

class DocumentQuerySet(models.QuerySet):
    def pdfs(self):
        return self.filter(file_type='pdf')

    def smaller_than(self, size):
        return self.filter(size__lt=size)

class Document(models.Model):
    name = models.CharField(max_length=30)
    size = models.PositiveIntegerField(default=0)
    file_type = models.CharField(max_length=10, blank=True)

    objects = DocumentQuerySet.as_manager()


pdfs()만 호출할 수 있는 것은 물론, smaller_than() 또한 연결하여 호출할 수 있습니다.

Document.objects.pdfs().smaller_than(1000)


models.py 안에 조회 쿼리를 넣을 수 있지만 코드가 커진다면 Manager와 QuerySet을 managers.py라는 다른 모듈에 유지하는 것을 추천합니다. Custom Manager와 Custom QuerySet을 사용하는 이점은 공통적으로 사용되는 쿼리를 공통 함수로 정의할 수 있고 실제 동작을 숨길 수 있습니다.

ORM을 조인하고 싶을 때 N:1의 관계나 N:N의 관계일 경우, prefetch_related()를 사용하게 됩니다. 

모델 정의

예시를 들기 위해 아래와 같이 모델과 모델간 관계를 정의합니다.

from django.db import models

class Topping(models.Model):
    name = models.CharField(max_length=30)

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)
	vegetarian = models.BooleanField()

    def __str__(self):
        return "%s (%s)" % (
            self.name,
            ", ".join(topping.name for topping in self.toppings.all()),
        )


class Restaurant(models.Model):
    pizzas = models.ManyToManyField(Pizza, related_name='restaurants')
    best_pizza = models.ForeignKey(Pizza, related_name='championed_by', on_delete=models.CASCADE)

Prefetch

만약 첫 번째 레스토랑에서 채식주의자가 먹을 수 있는 피자를 조회하는 쿼리는 아래와 같습니다.

queryset = Pizza.objects.filter(vegetarian=True)


restaurants = Restaurant.objects.prefetch_related(Prefetch('pizzas', queryset=queryset))
vegetarian_pizzas = restaurants[0].pizzas.all()


만약 위와 같은 queryset이 다른 조인 쿼리에도 사용된다면 해당 쿼리가 실행될 때마다 새로 조회를 하므로 중복조회가 발생됩니다. 이때, Prefetch()에서 제공하는 to_attr을 사용하여 쿼리를 메모리에 저장하여 효율적으로 사용할 수 있습니다.

queryset = Pizza.objects.filter(vegetarian=True)


restaurants = Restaurant.objects.prefetch_related(Prefetch('pizzas', queryset=queryset, to_attr='vegetarian_pizzas'))
vegetarian_pizzas = restaurants[0].vegetarian_pizzas


to_attr에 저장되는 Prefetch()의 데이터 크기가 너무 크지 않다면, 메모리에 올려 재사용성을 늘리는 것이 효율적입니다.

문자열로 되어 있는 시간을 Datetime 객체로 변경하는 방법은 많이 찾아 볼 수 있습니다. 여기서는 기존에 존재하는 방법과 추가 설치 모듈로 좀 더 간편하게 사용하는 방법을 설명하겠습니다.

1. Datetime

파이썬에 내장되어 있는 Datetime 패키지를 사용하여 아래와 같이 변환을 할 수 있습니다.

import datetime

date_time_str = '2018-06-29 08:15:27.243860'  
date_time_obj = datetime.datetime.strptime(date_time_str, '%Y-%m-%d %H:%M:%S.%f')

print('Date-time:', date_time_obj)


# Date-time: 2018-06-29 08:15:27.243860


흔히 사용하는 포맷은 ISO 8601으로  YYYY-MM-DDTHH:MM:SS.mmmmmm 와 같이 표현하고 있습니다. 이와 같은 포맷만 들어온다면 그대로 사용해도 무방하지만 아래와 같이 여러 타입이 존재한다면 고려할 사항이 많아집니다.

"Jun 28 2018 at 7:40AM" -> "%b %d %Y at %I:%M%p"
"September 18, 2017, 22:19:55" -> "%B %d, %Y, %H:%M:%S"
"Sun,05/12/99,12:30PM" -> "%a,%d/%m/%y,%I:%M%p"
"Mon, 21 March, 2015" -> "%a, %d %B, %Y"
"2018-03-12T10:12:45Z" -> "%Y-%m-%dT%H:%M:%SZ"

timezone 지정

위처럼 문자열을 datetime 객체로 변환하거나 현재 날짜+시간을 호출하면 현재 로컬시간을 기준으로 호출이 됩니다.

now = datetime.datetime.now()
print(now)


# 2019-02-02 15:54:07.244639


여기서 구한 현재 시간을 뉴욕시간대로 변경하고자 하면 아래와 같이 복잡한 절차를 거치게 됩니다. 또한 현재 시간이 뉴욕시간대로 변환되지 않고 timezone만 추가된 형태를 확인할 수 있습니다.

import datetime as dt
import pytz

date_time_obj = dt.datetime.now()

print('now:', date_time_obj)

timezone = pytz.timezone('America/New_York')
timezone_date_time_obj = timezone.localize(date_time_obj)

print(timezone_date_time_obj)
print(timezone_date_time_obj.tzinfo)


# now: 2019-02-02 15:56:15.197103
# 2019-02-02 15:56:15.197103-05:00
# America/New_York


datetime 패키지로만 충분히 변환하거나 시간대를 바꿀 수 있지만 다른 모듈을 설치하면 좀 더 간편하게 사용할 수 있습니다.

2. dateutil

dateutil 모듈을 설치하여 사용하면 문자열의 시간 포맷을 걱정할 필요가 없습니다.

from dateutil.parser import parse

date_array = [
    '2018-06-29 08:15:27.243860',
    'Jun 28 2018  7:40AM',
    'Jun 28 2018 at 7:40AM',
    'September 18, 2017, 22:19:55',
    'Sun, 05/12/1999, 12:30PM',
    'Mon, 21 March, 2015',
    '2018-03-12T10:12:45Z',
    '2018-06-29 17:08:00.586525+00:00',
    '2018-06-29 17:08:00.586525+05:00',
    'Tuesday , 6th September, 2017 at 4:30pm'
]

for date in date_array:
    print('Parsing: ' + date)
    dt = parse(date)
    print(dt.date())
    print(dt.time())
    print(dt.tzinfo)
    print()



# Parsing: 2018-06-29 08:15:27.243860
# 2018-06-29
# 08:15:27.243860
# None

# Parsing: Jun 28 2018  7:40AM
# 2018-06-28
# 07:40:00
# None

# Parsing: Jun 28 2018 at 7:40AM
# 2018-06-28
# 07:40:00
# None

# Parsing: September 18, 2017, 22:19:55
# 2017-09-18
# 22:19:55
# None

# Parsing: Sun, 05/12/1999, 12:30PM
# 1999-05-12
# 12:30:00
# None

# Parsing: Mon, 21 March, 2015
# 2015-03-21
# 00:00:00
# None

# Parsing: 2018-03-12T10:12:45Z
# 2018-03-12
# 10:12:45
# tzutc()

# Parsing: 2018-06-29 17:08:00.586525+00:00
# 2018-06-29
# 17:08:00.586525
# tzutc()

# Parsing: 2018-06-29 17:08:00.586525+05:00
# 2018-06-29
# 17:08:00.586525
# tzoffset(None, 18000)

# Parsing: Tuesday , 6th September, 2017 at 4:30pm
# 2017-09-06
# 16:30:00
# None


위 예시처럼 시간포맷을 알 필요가 없다는 장점이 있지만 timezone을 지정하지 않으면 로컬 시간을 바라보고 있는 것을 확인할 수 있습니다.

3. maya

마야 또한 위의 dateutil 모듈과 같이 시간 포맷에 상관없이 변환할 수 있습니다.

import maya

date_array = [  
    '2018-06-29 08:15:27.243860',
    'Jun 28 2018  7:40AM',
    'Jun 28 2018 at 7:40AM',
    'September 18, 2017, 22:19:55',
    'Sun, 05/12/1999, 12:30PM',
    'Mon, 21 March, 2015',
    '2018-03-12T10:12:45Z',
    '2018-06-29 17:08:00.586525+00:00',
    '2018-06-29 17:08:00.586525+05:00',
    'Tuesday , 6th September, 2017 at 4:30pm'
]

for date in date_array:  
    print('Parsing: ' + date)
    dt = maya.parse(date).datetime()
    print(dt)
    print(dt.date())
    print(dt.time())
    print(dt.tzinfo)
    print()


# Parsing: 2018-06-29 08:15:27.243860
# 2018-06-29 08:15:27.243860+00:00
# 2018-06-29
# 08:15:27.243860
# UTC

# Parsing: Jun 28 2018  7:40AM
# 2018-06-28 07:40:00+00:00
# 2018-06-28
# 07:40:00
# UTC

# Parsing: Jun 28 2018 at 7:40AM
# 2018-06-28 07:40:00+00:00
# 2018-06-28
# 07:40:00
# UTC

# Parsing: September 18, 2017, 22:19:55
# 2017-09-18 22:19:55+00:00
# 2017-09-18
# 22:19:55
# UTC

# Parsing: Sun, 05/12/1999, 12:30PM
# 1999-05-12 12:30:00+00:00
# 1999-05-12
# 12:30:00
# UTC

# Parsing: Mon, 21 March, 2015
# 2015-03-21 00:00:00+00:00
# 2015-03-21
# 00:00:00
# UTC

# Parsing: 2018-03-12T10:12:45Z
# 2018-03-12 10:12:45+00:00
# 2018-03-12
# 10:12:45
# UTC

# Parsing: 2018-06-29 17:08:00.586525+00:00
# 2018-06-29 17:08:00.586525+00:00
# 2018-06-29
# 17:08:00.586525
# UTC

# Parsing: 2018-06-29 17:08:00.586525+05:00
# 2018-06-29 12:08:00.586525+00:00
# 2018-06-29
# 12:08:00.586525
# UTC

# Parsing: Tuesday , 6th September, 2017 at 4:30pm
# 2017-09-06 16:30:00+00:00
# 2017-09-06
# 16:30:00
# UTC


dateutil과 다르게 maya는 로컬시간을 바라보지 않고 기본적으로 UTC로 지정합니다.

dateutil과 maya의 차이점

두 모듈은 비슷해 보이지만 timezone에 대해 아주 큰 차이가 있습니다.

import maya
import dateutil.parser

date_array = [  
    '2018-06-29 17:08:00.586525+05:00',
    '2018-06-29 17:08:00.586525',
]


for date in date_array:  
    print('Parsing: ' + date)
    maya_dt = maya.parse(date).datetime()
    dateutil_dt = dateutil.parser.parse(date)

    print('maya: ')
    print(maya_dt)
    print(maya_dt.tzinfo)

    print('dateutil: ')
    print(dateutil_dt)
    print(dateutil_dt.tzinfo)

    print()


# Parsing: 2018-06-29 17:08:00.586525+05:00
# maya: 
# 2018-06-29 12:08:00.586525+00:00
# UTC


# dateutil: 
# 2018-06-29 17:08:00.586525+05:00
# tzoffset(None, 18000)

# Parsing: 2018-06-29 17:08:00.586525
# maya: 
# 2018-06-29 17:08:00.586525+00:00
# UTC


# dateutil: 
# 2018-06-29 17:08:00.586525
# None


maya의 경우는 문자열에 존재하는 +05:00을 계산하여 timezone을 UTC로 세팅합니다. 위의 경우 17시에 +05:00가 존재하여 5시간을 뺀 후, timezone을 UTC라고 보여주고 있습니다. 반면 dateutil은 시간계산을 하지 않고 +05:00을 저장하게 됩니다. 

또한 timezone을 변경하거나 세팅할 때도 큰 차이가 존재합니다.

import maya
import dateutil.parser
import pytz
import dateutil

date = '2018-06-29 17:08:00.586525'

print('maya: ')
maya_dt = maya.parse(date)
print('parsing: ', maya_dt.datetime())
maya_dt = maya_dt.datetime(to_timezone='America/New_York', naive=False)


print('(timezone) America/New_York: ', maya_dt)
print(maya_dt.tzinfo)

print('------------------')

print('dateutil: ')
dateutil_dt = dateutil.parser.parse(date)
print('parsing: ', dateutil_dt)

timezone = pytz.timezone('America/New_York')
dateutil_dt = timezone.localize(dateutil_dt)

print('(timezone) America/New_York: ', dateutil_dt)
print(timezone)




# maya: 
# parsing:  2018-06-29 17:08:00.586525+00:00
# (timezone) America/New_York:  2018-06-29 13:08:00.586525-04:00
# America/New_York
# ------------------
# dateutil: 
# parsing:  2018-06-29 17:08:00.586525
# (timezone) America/New_York:  2018-06-29 17:08:00.586525-04:00
# America/New_York


maya의 경우, 파싱한 시간대에서 지정한 timezone의 시간만큼 계산하여 보여주지만 dateutil의 경우 datetime과 같이 시간은 계산하지 않고 timezone만 붙여서 보여주고 있습니다.

결론

문자열에서 datetime 객체로 변환하는 것은 third-party인 maya나 dateutil을 사용하는 것이 좋습니다. 또한 timezone을 변경하거나 각 다르게 사용할 경우, UTC로 변환하여 실행하는 서버 및 로컬의 시간대를 사용하지 않도록 하는 것이 중요합니다.

+ Random Posts