본문 바로가기

Tech/SQLAlchemy

[SQLAlchemy] imperative mapping

반응형

SQLAlchemy ORM 기능을 사용하기 위해서는 DB 의 테이블과 애플리케이션의 클래스를 매핑해야 한다. SQLAlchemy 에서는 테이블과 클래스를 매핑하는 방법으로 두가지 방법을 제공해준다. 하나는 Declarative Mapping, 다른 하나는 Imperative Mapping 이다.

 

Declarative Mapping 은 클래스를 구현할 때 SQLAlchemy DeclarativeBase 를 상속받음으로 명시적으로 ORM 에 의해 매핑될 클래스로 선언하는 방법이다. 반면에 Imperative Mapping 은 일반 클래스를 SQLAlchemy Table 객체를 registry 에 매핑하여 등록함으로써 해당 클래스와 테이블을 매핑시켜주는 방식이다.

 

프로그램을 구현하면서 파이썬 클래스를 SQLAlchemy 에 종속적인 클래스가 아니라 순수 파이썬 클래스로 구현해보고 싶어서 Imeperative Mapping 으로 구현하는 방법을 익혀보려 한다.

1. Imperative Mapping

imperative mapping 은 classifical mapping 이라고도 불리는 SQLAlchemy 의 ORM 매핑 방식이다. 이 방식은 registry.map_imperatively() 함수를 통해서 클래스와 테이블을 매핑하는 방식으로, 클래스는 declarative class 속성들을 가지지 않는다.

 

classifical mapping 이라고 불리는 이유는 이 방식이 전통적인 ORM 방식이기 때문이다. 클래스 자체가 매핑 클래스로 생성되어 테이블과 매핑되는 declarative class 속성들을 가지도록 하는 Declarative Mapping 방식과는 달리, Table 클래스로 생성된 테이블 객체와 매핑할 클래스가 따로 정의되고 registry.map_imperatively() 함수를 통해서 매핑된다.

 

from sqlalchemy import Table, Column, Integer, String, ForeignKey
from sqlalchemy.orm import registry

mapper_registry = registry()

user = Table(
    "user",
    mapper_registry.metadata,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("fullname", String(50)),
    Column("nickname", String(12)),
)


class User:
    pass


mapper_registry.map_imperatively(User, user_table)

 

위의 예시는 SQLAlchemy ORM 문서에 있는 예시로 imperative mapping 방식으로 user 테이블과 User 클래스가 매핑된다.

- properties

register.map_imperatively() 함수를 호출할 때, 매핑할 클래스와 테이블 객체외에 properties 라는 인자를 넘길 수 있다. 이 인자는 클래스들간의 relationship 과 같이 매핑된 클래스에 대한 정보를 가지고 있는 dict 형식의 값이다. 아래의 예제에서는 Address 클래스가 properties 의 relationship() 을 통해 User 클래스에 연결되고있다.

 

address = Table(
    "address",
    mapper_registry.metadata,
    Column("id", Integer, primary_key=True),
    Column("user_id", Integer, ForeignKey("user.id")),
    Column("email_address", String(50)),
)

mapper_registry.map_imperatively(
    User,
    user,
    properties={
        "addresses": relationship(Address, backref="user", order_by=address.c.id)
    },
)

mapper_registry.map_imperatively(Address, address)

 

- default constructor

SQLAlchemy ORM Mapping 에서 registry 가 기본 생성자를 매핑 클래스에 자동으로 적용해준다. 그렇기 때문에 매핑된 클래스들은 따로 __init__ 함수를 구현할 필요가 없다. registry 에서 자동으로 키워드 생성자를 추가해주기 때문에 클래스를 생성할 때 각 속성들에 대해 키워드 arguments 로 생성해주기만 하면 된다.

 

위의 예제들에서도 User 클래스에 아무런 구현이 없지만 아래와 같이 User 객체를 생성할 수 있다.

 

u1 = User(name="some name", fullname="some fullname")

2. 예제

from sqlalchemy import Table, Column, Integer, String, ForeignKey, Engine, create_engine
from sqlalchemy.orm import registry, relationship, sessionmaker, Session

# create registry instance
mapper_registry = registry()

# user table
user = Table(
    "user",
    mapper_registry.metadata,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("fullname", String(50)),
    Column("nickname", String(12)),
)

# User mapped class
class User:
    pass

# address table
address = Table(
    "address",
    mapper_registry.metadata,
    Column("id", Integer, primary_key=True),
    Column("user_id", Integer, ForeignKey("user.id")),
    Column("email_address", String(50)),
)

# Address mapped class
class Address:
    pass

# imperative mapping
mapper_registry.map_imperatively(
    User,
    user,
    properties={
        "addresses": relationship(Address, backref="user", order_by=address.c.id)
    },
)
mapper_registry.map_imperatively(Address, address

# connect to sqlite im-memory database
engine: Engine = create_engine("sqlite://", echo=False)
mapper_registry.metadata.create_all(engine)
session_factory = sessionmaker(bind=engine)

# create user instance with addresses
u1: User = User(name="u1", fullname="user1")u1.addresses = [Address(email_address="u1@gmail.com"), Address(email_address="u2@naver.com")]

session: Session = session_factory()
session.add(u1)
session.commit()


# check user and addresses
print(f"user: id={u1.id} name={u1.name} fullname={u1.fullname} addresses={[address.id for address in u1.addresses]}")

addresses: list[Address] = session.query(Address).all()
for address in addresses:
    assert address.user is not None
    assert address.user_id == u1.id
    print(f"address: id={address.id} email_address={address.email_address} user={address.user_id}")

 

위의 예제는 imperative mapping 으로 매핑된 클래스 User 와 Address 에 대해서 객체를 생성하고 저장한 뒤 정상적으로 저장되었는지를 확인하는 예제이다.

 

User 객체를 생성하고 addresses 에 Address 리스트를 할당한 뒤에 session 에 저장하고 커밋한다. 이후 User 객체의 정보와 session 에서 조회한 Address 객체들의 정보를 비교하고 출력한다.

 

아래의 코드 실행결과를 통해서 imperative mapping 방식으로 매핑된 User 클래스와 Address 클래스의 객체들이 정상적으로 생성 및 저장되고 둘간의 relationship 도 잘 설정되었음을 확인할 수 있다.

 

예제코드 실행결과

 

 

[Reference]

- https://docs.sqlalchemy.org/en/20/orm/mapping_styles.html#imperative-mapping

 

ORM Mapped Class Overview — SQLAlchemy 2.0 Documentation

ORM Mapped Class Overview Overview of ORM class mapping configuration. For readers new to the SQLAlchemy ORM and/or new to Python in general, it’s recommended to browse through the ORM Quick Start and preferably to work through the SQLAlchemy Unified Tut

docs.sqlalchemy.org

 

반응형

'Tech > SQLAlchemy' 카테고리의 다른 글

[SQLAlchemy] Relationship - CASCADE  (0) 2023.11.06
[SQLAlchemy] 연관관계 설정  (0) 2022.07.23
[SQLAlchemy] SQLAlchemy 기본 설명  (0) 2022.07.17