Python/FastAPI

FastAPI와 SQLAlchemy로 ORM 사용하기

검정비니 2023. 10. 9. 19:36
728x90
반응형

ORM이란?

ORM(Object Relational Model)은 사물을 추상화시켜 이해하려는 OOP적 사고방식과 DataModel을 정형화하여 관리하려는 RDB 사이를 연결할 계층의 역할로 제시된 패러다임으로 RDB의 모델을 OOP에 Entity 형태로 투영시키는 방식을 사용한다. 

이는 시스템에 따라, 사용하는 Database 및 DB Connector 에 따라 달라질 수 있는 데이터 매핑 구조를 객체지향형태로 통일시켜, SQL 구조의 Database를 OOP 구조의 형태로 매핑시키려는 패러다임이다. OOP 적 구조와 SQL 구조의 차이는 데이터를 다루는 방법에서 나타난다. OOP 적 구조에서 모든 데이터는 객체이며, 각 객체는 독립된 데이터와 독립된 함수를 지닌다. 반면 SQL 에서 데이터는 테이블단위로 관리되며 객체들을 조회하기 위한 명령어를 사용한다.

ORM 은 각 테이블 또는 구분하고자하는 데이터 단위로 객체를 구현하고, 데이터간의 관계를 형성한다. 가령 개인 정보 저장을 위한 RDB의 테이블 Personal 은 다음과 같은 객체 Personal 로 매핑될 수 있다.

 

FastAPI + SQLAlchemy로 ORM 사용하기

SQLAlchemy는 파이썬 프로그램이 SQL 데이터베이스와 소통할 수 있도록 도와주는 기능을 하는 라이브러리이다. 또한, SQLAlchey는 파이썬으로 ORM을 사용할 수 있도록 하는 기능을 제공한다.

SQLAlchemy 객체 관계형 매퍼는 데이터베이스 테이블을 이용해 사용자가 정의한 파이썬 클래스의 메소드와 각각의 행을 나타내는 인스턴스로 표현된다. 객체와 각 연관된 행들의 모든 변경점들이 자동으로 동기되어 인스턴스에 반영되며, 그와 동시에 사용자가 정의한 클래스와 각 클래스 사이에 정의된 관계에 대해 쿼리할 수 있는 (Unit of work이라 하는)시스템을 포함하고 있다.

이 ORM에서 사용하는 SQLAlchemy 표현 언어는 ORM의 구성 방식과도 같다. SQL언어 튜토리얼에서는 직접적인 의견을 배제한 채 데이터베이스들의 초기에 어떻게 구성해 나가야 하는지에 대해 설명하는 반면 ORM은 고수준의, 추상적인 패턴의 사용 방식과 그에 따른 표현 언어를 사용하는 방법을 예로 보여준다.

사용 패턴과 각 표현 언어가 겹쳐지는 동안, 초기와 달리 공통적으로 나타나는 사항에 대해 표면적으로 접근한다. 먼저 사용자가 정의한 도메인 모델서부터 기본적인 저장 모델을 새로 갱신하는 것까지의 모든 과정을 일련의 구조와 데이터로 접근하게 해야한다. 또 다른 접근 방식으로는 문자로 된 스키마와 SQL 표현식이 나타내는 투시도로부터 명쾌하게 구성해, 각 개별적인 데이터베이스를 메시지로 사용할 수 있게 해야 한다.

가장 성공적인 어플리케이션은 각각 독자적인 객체 관계형 매퍼로 구성되야 한다. 특별한 상황에서는, 어플리케이션은 더 특정한 데이터베이스의 상호작용을 필요로 하고 따라서 더 직접적인 표현 언어를 사용할 수 있어야 한다.

 

SQLAlchemy 설치하기

pip install SQLAlchemy

 

 

SQLAlchemy 버전 확인하기

import sqlalchemy
print(sqlalchemy.__version__)

SQL 접속하기

sqlalchemy를 사용해서 데이터베이스에 접속하기 위해서는 우선 sqlalchemy engine 객체를 생성해야 한다. 접속하려는 데이터베이스와 연결된 엔진 객체를 생성하려면 `create_engine` 함수를 사용하면 된다.

from sqlalchemy import create_engine

sql_db_url = 'URL_TO_YOUR_DB' # i.e. 'sqlite:///:memory:'
engine = create_engine(sql_db_url, echo=True)

echo는 로그를 위한 플래그이며, 파이썬 표준 logging 모듈을 사용해서 구현되었다. echo 옵션을 사용하면 순수 SQL 코드를 보여준다.
engine은 선언만 해서 바로 연결되는게 아니라 첫 실행이 될 때 연결이 된다. 따라서 첫 쿼리 실행 시에는 약간의 딜레이가 있을 수 있다.

 

ORM을 위한 맵핑 선언

ORM에서는 처음에 데이터베이스 테이블을 써먹을 수 있게 설정한 다음 직접 정의한 클래스에 맵핑을 해야한다. sqlalchemy에서는 두가지가 동시에 이뤄지는데 `Declarative` 라는 기능을 이용해 클래스를 생성하고 실제 DB 테이블에 연결을 한다.

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


이렇게 생성한 Declarative 객체인 `Base`를 상속하는 클래스를 선언함으로서 ORM을 위한 맵핑 클래스를 선언할 수 있게 된다.

아래는 이름, 성명, 비밀번호 이렇게 3개의 멤버변수를 가지는 User class를 생성하는 예제이다.

from sqlalchemy import Column, Integer, String

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    password = Column(String)

    def __init__(self, name, fullname, password):
        self.name = name
        self.fullname = fullname
        self.password = password

    def __repr__(self):
        return "<User('%s', '%s', '%s')>" % (self.name, self.fullname, self.password)


ORM 맵핑 클래스에서 `__tablename__` 멤버 변수는 맵핑이 될 SQL 테이블의 이름과 일치해야 한다. 그 외의 `__init__`이나 `__repr__` 등의 메소드들은 반드시 만들어야 하는 것은 아니다. `__tablename__` 이외의 내용은 개발자가 어떻게 설계하고 구현하느냐에 따라 충분히 달라질 수 있다.


Declarative system으로 만들어진 이 클래스는 table metadata를 가지게 되는데 이게 사용자정의 클래스와 테이블을 연결해주는 구실을 한다. 과거에는 이 metadata를 만들고 클래스에 맵핑해서 썼는데 그 방식을 `Classical Mapping`이라고 얘기한다. 그 예전 방식에서는 `Table`이라는 데이터 구조와 `Mapper` 객체를 ORM 맵핑을 위한 타겟 클래스와 mapping한다.


Declarative system 기반 방법에서는 `metadata` 멤버 변수를 사용해서 자동 생성된 테이블 메타데이터에 접근하는 것이 가능하다.

 

DB별 SQL 테이블 설정 방법

`sqlite`나 `postgresql`은 테이블을 생성할 때 `varchar` 컬럼을 길이를 설정하지 않아도 별 문제 없이 데이터타입으로 쓸 수 있지만 그 외 데이터베이스에서는 허용되지 않는다. 그러므로 컬럼 길이가 필요한 데이터베이스의 경우 length가 필요하다.

Column(String(50))

 

Integer, Numeric 같은 경우에도 위와 동일하게 쓸 수 있다.

덧붙여 Firebird나 오라클에서는 PK를 생성할 때 sequence가 필요한데 Sequence 생성자를 써야 한다.

from sqlalchemy import Sequence
Column(Integer, Sequence('user_id_seq'), primary_key=True)

 

위에서 정의하였던 `User` 클래스를 위의 문법들을 적용해서 다시 구현하면 아래와 같은 형태가 된다.

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, Sequence('user_id_seq'), primary_key=True)
    name = Column(String(50))
    fullname = Column(String(50))
    password = Column(String(12))

    def __init__(self, name, fullname, password):
        self.name = name
        self.fullname = fullname
        self.password = password

    def __repr__(self):
        return "<User('%s', '%s', '%s')>" % (self.name, self.fullname, self.password)

세션 만들기

ORM은 데이터베이스를 session을 이용해 다룰 수 있는데 처음 앱을 작성할 때 create_engine()과 같은 레벨에서 Session 클래스를 factory 패턴으로 생성할 수 있다.

from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)

모듈레벨에서 작성하고 있어서 Engine이 아직 존재하지 않는다면, Session을 생성하고 나중에 engine이 생성된 후에 session의 configure를 이용한다.

Session = sessionmaker()

...
engine = create_engine(sql_db_url, echo=True)
Session.configure(bind=engine)

위처럼 작성한 Session 클래스는 새 객체를 만들어서 데이터베이스와 연결이 된다.

SQLAlchemy session에 대한 자세한 내용은 https://docs.sqlalchemy.org/en/20/orm/session.html#session-faq를 참조하기를 바란다.

 

새 객체 추가하기

Table에 새로운 객체 데이터를 추가하기 위해서는 add() 나 add_all() 메소드를 사용해야 한다.

ed_user= User('haruair', 'Edward Kim', '1234')
session.add(ed_user)

# add multiple objects at once
session.add_all([
    User('wendy', 'Wendy Williams', 'foobar'),
    User('mary', 'Mary Contrary', 'xxg527'),
    User('fred', 'Fred Flinstone', 'blar')])

위와 같이 add() 메소드로 저장을 하게 되면 DB에 바로 적용되는 것이 아니라 `Pending` 상태로 데이터베이스로 발행 되기 위한 대기 상태에 들어가게 된다. 이러한 Pending 상태에 있는 데이터를 데이터베이스에 적용하기 위해서는 `flush`라는 과정이 진행되어야 한다.

commit() 함수로 flush하기

Session에 Pending 상태로 남아 있는 객체들을 실행시키기 위해서는 `commit()` 메소드를 실행시키면 된다. commit()은 모든 변경, 추가 이력을 반영한다. 이 트랜잭션이 모두 실행되면 세션은 다시 connection pool을 반환하고 물려있던 모든 객체들을 업데이트 한다.

session.commit()

데이터베이스 롤백하기

데이터베이스에서는 변경 사항을 되돌리는 기능인 롤백(Rollback)이라는 기능이 있다. 롤백은 데이터베이스에서 업데이트에 오류가 발생할 때, 이전 상태로 되돌리는 것을 말한다. 후진 복귀라고도 한다. 데이터베이스는 업데이트 이전 저널 파일을 사용하여 원래의 정상적인 상태로 되돌린다. 이것은 오류 동작 이후에도 깨끗한 사본으로 복원시킬 수 있기 때문에, 무결성을 위해 중요하다. 데이터베이스 서버의 충돌로부터 복원하는데도 중요하다. 충돌이 일어날 때, 특정 트랜잭션을 롤백시킴으로써 데이터베이스는 일관적인 상태로 되돌려진다.

SQLAlchemy에서 롤백을 사용하기 위해서는 session의 rollback() 메서드를 호출하면 된다.

session.rollback()

 

반응형

'Python > FastAPI' 카테고리의 다른 글

FastAPI + SQLAlchemy 로 N+1 쿼리 해결하기  (0) 2023.10.09