건프의 소소한 개발이야기

[MySQL-Flask SqlAlchemy] Python 으로 ORM 완벽사용 (2) 본문

개발 이야기/MySQL(DB) 이야기

[MySQL-Flask SqlAlchemy] Python 으로 ORM 완벽사용 (2)

건강한프로그래머 2016. 12. 1. 19:24

안녕하세요, 건프입니다.


 앞에서 ORM 의 개념과, 클래스 지정 방법에 대해서 알아봤습니다.


 이번에는 ORM 방법으로 Flask-Sqlalchemy 에서는 어떤식으로 DB 에서 Query 하는지를 알아보고, 실전에서 자주 사용하는 여러가지 기법들에 대해서도 간단하게 메모해보려고 합니다.



첫번째로 일반적으로 테이블에서 값을 가져오는 Query 인 Select 하는 방법을 먼저 알아봅니다.


def search_events_by_userid(user_id):
"""
user_id 를 기준으로 event 찾기, all 로 찾음
"""
return TravelEvent.query.filter_by(user_id=user_id).all()

def search_event_by_eventid(event_id):
"""
event_id 를 기준으로 event 를 찾기. 한개만 리턴
"""
return TravelEvent.query.filter_by(event_id=event_id).first()


이렇게 두개의 예제가 간단하게 확인할 수 있는 방법입니다.

search_events_by_userid 로 정의된 함수는 TravelEvent 라는 디비클래스를 이용해서 쿼리를 날립니다.

저기 있는 TravelEvent 라는 클래스는 ORM 완벽사용 (1) 에서 처럼 정의한 db 클래스입니다. 

from datetime import datetime

class TravelEvent(db.Model):
__tablename__ = 'travel_event'
__table_args__ = {'mysql_collate': 'utf8_general_ci'}

event_id = db.Column(db.Integer, primary_key=True, unique=True, autoincrement=True) # primary key for event table
user_id = db.Column(db.String(30)) # 어느 사용자가 이벤트를 오픈했는지
course_id = db.Column(db.Integer) # 어느 코스를 사용하는지, 해당 코스의 detail 정보를 모두 가져올 수 있다
title = db.Column(db.String(30)) # 해당 모집글의 제목정보
description = db.Column(db.String(1000)) # 사용자가 작성할 이벤트의 설명글
max_tourist = db.Column(db.Integer) # 최대 모집자 수
current_tourist = db.Column(db.Integer) # 현재 모집자 수
travel_start_time = db.Column(db.DateTime) # 여행 시작 날짜와 시간
travel_end_time = db.Column(db.DateTime) # 여행 끝 날짜와 시간
event_end_time = db.Column(db.DateTime) # 모집이 끝나는 시간, 모집글이 등록되는 순간부터 모집 시
status = db.Column(db.Integer) # 모집여부, 0: 모집 끝, 1:모집 진행중
hash_tag = db.Column(db.String(200)) # 검색을 위해서 이벤트가 지나가는 경로에 대한 해시태그
created = db.Column(db.DateTime)

def __init__(self, user_id, course_id, title, description, max_tourist, start_time, end_time, event_end_time, hash_tag):
self.user_id = user_id
self.course_id = course_id
self.title = title
self.description = description
self.max_tourist = max_tourist
self.current_tourist = 1 # 처음에는 무조건 본인 한명
self.travel_start_time = start_time
self.travel_end_time = end_time
self.event_end_time = event_end_time
self.status = 1 # 처음에 등록하면 무조건 모집 중 상태
self.hash_tag = hash_tag
self.created = datetime.now() # 만들면 현재 날짜를 등록

def as_dict(self):
return {x.name: getattr(self, x.name) for x in self.__table__.columns}

별로 중요하진 않지만, 모호하게 설명하는 것을 안좋아해서 첨부합니다.


TravelEvent.query.filter_by(user_id=user_id).all()

TravelEvent.query 객체를 부른다음, 

filter_by() 함수에서 user_id 가 

매개변수로 들어온 user_id 에 매칭이 되는 애들을 찾아서, 

all()  : 모두 리턴해달라. 

TravelEvent.query.filter_by(event_id=event_id).first()

first() : 첫번째에 매칭되는 인스턴스만 달라


어떤 식으로 사용되는지 아시겠나요?

filter_by 의 경우 sql 에서 where 문을 생성해주는 것이라고 생각하면 될 것 같습니다.


여러 조건을 한번에 query 하고 싶은 경우에는 어떻게 할까요?

예를들어 where a == 'a' and b == 'b' 요렇게 말이죠.

from sqlalchemy import and_
queries = CourseDetail.query.filter(and_(CourseDetail.content_id == item['content_id'],
CourseDetail.sequence_id == item['sequence_id'])).all()


이렇게 진행하시면 됩니다. filter() 함수 안에서 flask-sqlalchemy 를 설치하면 함께 설치되는 sqlalchemy 안에 있는 and_ 함수를 이용하는 것이죠.

CourseDetail 로 정의되어 있는 Table 에서 content_id 와 sequence_id 가 정확히 일치되는 모든 인스턴스를 찾아서 리턴해주는 역할을 하게 됩니다.


정렬은 어떻게할까요?

일반적으로 정렬은 sql 문에서 order by 로 작업합니다.


new_course_id = CourseMeta.query.order_by(CourseMeta.created.desc()).first().course_id

이렇게 하시면 동작합니다. 

CourseMeta 테이블에서 created 칼럼을 내림차순으로 정렬한 뒤에,

첫번째 인스턴스의

course_id 를 반환하는 예제입니다.


offset 과 limit 도 지정할 수 있을까요?

네 가능합니다.


def search_course_reviews(course_id, off_set=0, limit_num=20):
if limit_num is None:
limit_num = 20
return CourseReview.query.filter_by(course_id=course_id).order_by(CourseReview.created.desc()).offset(off_set).limit(limit_num)


이 함수는 CourseReview 라는 테이블에서 course_id 로 필터하고, created 를 내림차순으로 정렬한뒤, offset 을 줘서 앞에서부터 스킵할 인스턴스 단위를 전달합니다. limit 는 한번에 가져올 인스턴스의 최대량을 의미하게 되죠.


이러한 방법은 sql 에서도 limit, offset 기능을 사용하는 것으로 예상하고, MongoDB 에서는 skip () 이라는 함수로 제공되는 것 같더군요?

클라이언트에서 버튼을 누르면 페이지나 내용물을 추가로 로딩하는 기능을 구현할때 유용합니다.



그렇다면 어떻게 인스턴스를 추가(Insert) 할까요?

from app import db
from app.models.event import TravelEvent
def add_new_event(new_event_object, course_id):
new_event = TravelEvent(new_event_object['user_id'],
course_id,
new_event_object['title'],
new_event_object['description'],
new_event_object['max_tourist'],
new_event_object['start_time'],
new_event_object['end_time'],
new_event_object['event_end_time'],
new_event_object['hash_tag'])
db.session.add(new_event)
db.session.commit()

이 예제는 TravelEvent 테이블에 새로운 인스턴스를 추가하는 예제입니다.

TravelEvent 테이블을 클래스로 매핑했고(ORM) 클래스의 인스턴스를 만들때 생성자에 넣어야 할 것들을 정의했습니다.

그렇게 new_event 를 정의했고,

db.session.add() 함수에 해당 인스턴스를 넘긴뒤에

db.session.commit() 명령을 수행하면 됩니다.


db 객체는 ORM 완벽사용(1) 에서 봤듯이 app/__init__.py 안에 있던, 그 DB 객체를 가져온 것입니다.


없애는 것은 (delete) insert 와 워낙 닮았기 때문에 따로 언급하지 않겠습니다.


이미 넣었던 인스턴스의 정보를 어떻게 업데이트(Update) 하나요?

def user_join_event(user_id, event_id):
"""
유저가 이벤트에 참여하는 행동을 합니다
"""
new_user_bag = UserBag(user_id, 1, event_id)
db.session.add(new_user_bag)
db.session.commit()
print(user_id, " 의 UserBag 에 ", event_id, " 를 추가했습니다")
event = search_event_by_eventid(event_id)
current_tourist = event.current_tourist
if current_tourist + 1 <= event.max_tourist:
event.current_tourist=(event.current_tourist+1)
db.session.commit()
return True
else:
# 이미 모집인원이 꽉차버림..
return False

다른 코드는 보지말고 우리가 원하는 update 하는 코드만 살펴보겠습니다.

event = search_event_by_eventid(event_id)

이렇게 해서 event_id 로 쿼리한 결과 인스턴스를 받아냅니다.

current_tourist = event.current_tourist

이렇게 하면, event 객체 안에 있는 현재 모집된  여행자수의 값을 받을 수 있습니다.

if current_tourist + 1 <= event.max_tourist:

따라서 이렇게 if 분기 연산을 진행할 수 있습니다.

event.current_tourist=(event.current_tourist+1)
db.session.commit()

event.current_tourist 맴버변수의 값을 새로운 값으로 업데이트 해주고

db.session.commit() 을 날려주면 업데이트가 완료됩니다!


사용하면 사용할 수록 ORM 의 뼈대가 되는 db.Model 의 역할이 너무나도 대단하는 생각이 듭니다. ㅎㅎ




이렇게 Flask-sqlalchemy 를 이용해 ORM 방법으로 디비를 손쉽게 관리하는 방법에 대해서 메모해보았습니다.

예전에 PHP 로 디비작업을 할때는 어떻게든 sql query 문을 객체지향적으로 짜보려고, 쿼리문을 만드는 make 함수를 두고, 테이블 이름과 칼럼명과 값을 넘겨가면서 객체지향적으로 짜보려는 노력을 했었었는데


Python 에서는 위와 같이 이미 정형화된 패턴이 있기 때문에 정말 편한것 같습니다! 갓파이썬


실전적으로 도움이 되기 위해서 제가 진행했었던 프로젝트를 예제로 따와서 전체적인 맥락은 볼 수 없어 불편하시겠지만

ORM 을 사용하는 방법을 익히는데는 큰 무리가 없을 것이라 생각합니다. 

질문은 언제나 환영합니다.


도움이 되었길 바랍니다.


고맙습니다 :)

Comments