[SQLAlchemy] Python ORM

원래 학부때 DB와 담쌓고 살았었는데, 살다보니 그게 안되더라
C, C++, Java, Python을 넘나들며 DB에 연결하기 다반사다.
하지만 가장 큰 문제는 매번 DB 클래스와 쿼리를 직접 만들어줘야했던것
무척 귀찮고 힘든 작업이다.

그런와중 현업에 있는 친구들이 ORM을 알려줬다.
ORM은 Object-Relation Mapping의 줄임말로, 말 그대로 객체와 테이블을 매핑해준다는 것이다.
심지어 매핑되기만 하면 클래스 메서드로 DML과 DDL을 사용할 수 있다.
뭐.. 듣기만 해도 최고다.
쓰는 법도, 왜 편리한지도 묻지도 따지지도 않고 바로 해봤다.

얘기를 들어보니 Python에서는 SQLAlchemy가 널리 쓰이는 것 같다.
자바쪽에서는 hibernate나 mybatis가 많이 쓰인다고 얘기는 들어봤지만
실제로 ORM을 직접 돌려보는건 이번이 처음이다.

순서는 이렇다

  1. SQLAlchemy 설치
  2. 드라이버(Engine) 및 세션(Session) 로드
  3. 테이블 클래스 정의
  4. 쿼리 (Execute) 및 커밋 (Commit)

SQLAlchemy가 pymysql이나 libmysql 등의 컨벤셔널 라이브러리와 다른 점은 클래스 정의이다.
기존에는 ORM이 없기 때문에 DB 테이블과 매핑을 위해 클래스를 직접 구현하고 클래스의 DDL/DML도 직접 구현해야 했다.
그러나 ORM이 있는 지금은 클래스 정의만으로 테이블과 자동 매핑된다.
다시 말해, 클래스만 정의하면 그에 상응하는 DDL이나 DML을 사용할 수 있는 것이다.

1. SQLAlchemy 설치

우선 SQLAlchemy를 설치한다.

pip3 install sqlalchemy

나는 mariadb를 사용하기 때문에 mysql 모듈도 설치해줘야한다.
(Python에서의 mysql connector)

pip3 install mysqlclient

이제 사용하기면 된다.

2. 드라이버(Engine) 및 세션(Session) 로드

SQLAlchemy를 사용하기 위해서는 먼저 드라이버를 로드해줘야 한다.

from sqlalchemy import create_engine
engine = create_engine('mysql+mysqldb://<username>:<password>@<host>:<port>/<dbname>')

로드가 잘됐다면 아래처럼 아무거나 실행해보자
실행이 안된다면 당연히 로드가 안된것이다.
참고로 execute query의 결과는 fetchone나 fetchall로 가져올 수 있다.
차이는 이름 그대로이다.

print(engine.execute("desc test").fetchone()) # test 테이블의 schema를 출력
>>> ('a', 'int(11)', 'YES', '', None, '')

끝나면 이제 세션을 연결할 차례다.
세션이란 말 그대로 DB와의 소통을 관리하는 영역(zone)의 개념이다.
그렇기에 DB와의 소통을 따로 관리할 생각이 있는 게 아니라면 세션을 만들지 않아도 무방하다. [docs][SOF]
하지만 관리/운영 상의 면에서 세션의 존재는 필수불가결하므로 왠만하면 항상 사용해주도록 하자.

우선 다음과 같이 sessionmaker를 통해 세션을 만들어보자.
참고로 sessionmaker는 session을 만들어주는 factory class이다. [docs]

from sqlalchemy.orm import sessionmaker
Session = sessionmaker(engine) # Engine에 종속적인 Session을 정의하고
session = Session() # Session 객체를 만든다

3. 테이블 클래스 정의

ORM을 위한 클래스를 만들어보자
참고로 목표로 하는 DB 스키마는 다음과 같다.
굳이 이 스키마를 직접 만든 후 매핑할 생각은 없고
테이블 클래스의 DDL을 통해 테이블을 생성해볼 것이다.

$ mysql -u <username> -p -e "use <dbname>; desc <tablename>"
+-------+-----------+------+-----+-------------------+-------+
| Field | Type      | Null | Key | Default           | Extra |
+-------+-----------+------+-----+-------------------+-------+
| a     | int(11)   | YES  |     | NULL              |       |
| b     | int(11)   | YES  |     | NULL              |       |
| time  | timestamp | NO   | PRI | CURRENT_TIMESTAMP |       |
+-------+-----------+------+-----+-------------------+-------+

테이블 생성을 위해서는 테이블 클래스와 DB를 매개해주는 base 클래스를 사용한다. [docs]
유저가 만드는 커스텀 테이블 클래스는 이 base 클래스를 상속하여 DB에 연결된다.
(상속을 통해 DDL과 DML을 사용할 수 있게된다)
다만, base 클래스를 만들 땐 아래처럼 declarative_base라는 팩토리 클래스를 사용해 만들어야 한다.

from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

이제 base 클래스를 상속받는 매핑 클래스를 만들어주면 DB와 연동된다.

import datetime
from sqlalchemy import Column, Integer, String, DateTime
class User(Base):
    __tablename__ = '<tablename>'
    a = Column(Integer)
    b = Column(Integer)
    time = Column(DateTime, default=datetime.datetime.utcnow, primary_key=True)
    def __init__(self, a, b):
        self.a = a
        self.b = b

이번엔 DDL을 써볼 차례이다.
DDL은 base 클래스에서 제공하는 기본 기능이다.
base에 연결되어있는 테이블의 일부 혹은 전부에 대해 생성(create) 또는 삭제(drop)가 가능하다.

다음은 위에서 만든 User 클래스를 이용한 DDL 예제이다.

Base.metadata.create_all(engine) # Base에 연결된 모든 테이블을 DB에 생성한다.
Base.metadata.tables['TEST_TB'].create(bind = db._engine) # Base에 연결된 TEST_TB 테이블을 DB에 생성한다.
Base.metadata.drop_all(engine) # Base에 연결된 모든 테이블을 DB에서 제거한다.
Base.metadata.tables['TEST_TB'].drop(bind = db._engine) # Base에 연결된 TEST_TB 테이블을 DB에서 제거한다.

4. 쿼리 (Execute) 및 커밋 (Commit)

이제 레코드를 만들고 세션에 추가한 후 커밋해보자.

user = User(1,2)
session.add(user)
session.commit()

아주 좋다.
이제 귀찮게 insert into 뭐시기를 입력하지 않아도 된다.

그 외에도 이런 저런 기능들이 많지만 일단 여기까지만 정리한다.

얘기가 많았지만 결국 사용한 컴포넌트는 드라이버(engine)와 세션(session), 베이스(declarative)의 세 가지이다.
드라이버가 있어야 DB와 연결이 되고, 세션이 있어야 Pool 관리가 되고, 베이스가 있어야 테이블을 매핑할 수 있다는 것만 이해하면 된다.

참 편하다.
다만 언어별로 공통된 ORM이 없다는 점, 그리고 ORM 자체가 무거울수밖에 없다는 점 등이 단점이긴 하다.
그래도 쿼리 직접 안쓰는것만으로도 프로그래머를 고통의 굴레에서 벗어나게 한다.

+ 테이블 관리

테이블을 여러개 미리 만들어두고 사용하고싶을 때가 있다.
이 경우 각 테이블 파일별로 Base를 어떻게 관리해야할 지 난감할 수 있다.
이 때는 모든 테이블이 동일한 Base를 사용하게 하면 된다.
이를 위해 아래와 같이 Base를 위한 별도의 파일을 만들어보자.

# SQLAlchemyCommonBase.py
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

이후, 각 테이블 파일에서는 Base를 import하여 사용하면 된다.

# CustomTable.py
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime
from SQLAlchemyCommonBase import Base

class CustomTable(Base):
    __tablename__ = 'CUSTOM_TB'
    a = Column(Integer, primary_key=True, autoincrement=True)
    b = Column(Integer)
    time = Column(DateTime, default=datetime.utcnow, primary_key=True)
    def __init__(self, b):
        self.b = b

위의 CustomTable은 Base에 연동되어있기 때문에
다른 파일에서도 위의 CustomTable을 import하여 바로 사용할 수 있다.

댓글 남기기