Language/Python

[SQLAlchemy] Imperative Mapping, exclude properties

j4ko 2024. 3. 7. 16:16
728x90
반응형

목차

     

    개요 

    imperative mapping을 사용하다 SELECT 쿼리로부터 특정 Column들을 제외하고 싶은 상황들이 있다. 이때 주요한 점은 SELECT로 얻은 Class의 attribute를 제외하는 방법이 아닌 SELECT 단계부터 특정 열을 제외하는 방식을 적용해 보고자 함이다.

     

    즉,  instance의 namespace 자체에서 임의로 정의한  Column들이 제외되어있어야 함을 의미한다.

     

    1.  Idea 

    "특정 Column을 제외시키고 싶다"라는 생각에서 출발했지만 SQLAlchemy가 그러한 기능을 제공하지 않는다면 코드를 따로 작성해야 했다. 다행히 https://docs.sqlalchemy.org/en/14/orm/mapping_api.html에서 exclude_propeties에 대한 힌트를 얻을 수 있었다.

     

    구글 검색을 통해서 발견할 수 있었던 예제들은 declarative_base Model에 exclude_properties를 적용하는 예제들이 많았기에 imperative_mapping 방식에 이를 적용하는 예제를 찾는 건 쉽지 않은 일이었다. 몇 시간의 검색을 거쳐서 github  sqlalchemy repository의 issue에서 exclude_properties를 어떻게 사용할 수 있을는지에 대한 용법을 참고할 수 있었다.

     

    https://github.com/sqlalchemy/sqlalchemy/discussions/9869

     

    Name conflict and use of `exclude_properties` in `Composite Columns` · sqlalchemy sqlalchemy · Discussion #9869

    Hello everybody! I have an issue with Composite Column when I want to associate a single database column with my own single dataclass. This issue arises only when there is a name conflict between m...

    github.com

     

    2.  Setup

    "exclude_properties" 기능의 동작을 테스트하기 위해 다음과 같은 설정을 잡았다.

    2.1 SQL

    # member.sql
    CREATE TABLE `member` (
      `pk` int(1) unsigned NOT NULL AUTO_INCREMENT,
      `nanoid` char(24) NOT NULL,
      `name` varchar(32) DEFAULT NULL,
      `age` int(1) unsigned DEFAULT NULL,
      `address1` VARCHAR(1024) DEFAULT NULL,
      `address2` VARCHAR(1024) DEFAULT NULL,
    
      PRIMARY KEY (`pk`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    2.2 Python Object

    # MemberEntity
    class MemberEntity:
        nanoid: str
        name: str
        age: int
        
    # sqlalchemy mapping model
    t_member = Table(
        'member', metadata,
        Column('pk', INTEGER(1), primary_key=True),
        Column('nanoid', CHAR(24), nullable=False),
        Column('name', String(32)),
        Column('age', INTEGER(1)),
        Column('address1', String(1024)),
        Column('address2', String(1024))
    )

    MemberEntity에는 addres1과 address2를 가리키는 속성이 없는 상황이다. 그러나 table과 class를 mapping 해두었기에 select 쿼리가 발생하면 MemberEntity Class의 namespace에는 address1과 address2가 잡히게 된다.

     

    3.  Imperative Mapping,  exclude_properties

    3.1 Setup

    Imperative Mapping에서 exclude_properties를 설정 방법은 다음과 같다.

    from sqlalchemy.orm import registry
    
    orm_mapper = registry(orm.metadata)
    orm_mapper.map_imperatively(
            MemberEntity, orm.t_member,
            exclude_properties={
                orm.t_member.c.address1,
                orm.t_member.c.address2,
            },
    )

     

    3.2 select query 

    이제 간단히 다음과 같이 간단한 orm query를 날려보자.

    query = self.session.query(MemberEntity) \
        .filter(MemberEntity.nanoid == member_id)
    query = query.one_or_none()

    이 쿼리의 로그는 다음과 같이 발생한다.

    2024-03-07 15:41:35,253 INFO sqlalchemy.engine.Engine SHOW VARIABLES LIKE 'sql_mode'
    2024-03-07 15:41:35,254 INFO sqlalchemy.engine.Engine [raw sql] ()
    2024-03-07 15:41:35,275 INFO sqlalchemy.engine.Engine SHOW VARIABLES LIKE 'lower_case_table_names'
    2024-03-07 15:41:35,275 INFO sqlalchemy.engine.Engine [generated in 0.00053s] ()
    2024-03-07 15:41:35,300 INFO sqlalchemy.engine.Engine SELECT DATABASE()
    2024-03-07 15:41:35,300 INFO sqlalchemy.engine.Engine [raw sql] ()
    2024-03-07 15:41:35,303 INFO sqlalchemy.engine.Engine BEGIN (implicit)
    2024-03-07 15:41:35,316 INFO sqlalchemy.engine.Engine SELECT `member`.pk AS member_pk, `member`.nanoid AS member_nanoid, `member`.name AS member_name, `member`.age AS member_age
    FROM `member`
    WHERE `member`.nanoid = %s
    2024-03-07 15:41:35,316 INFO sqlalchemy.engine.Engine [generated in 0.00085s] ('WAS6V0DUH5YiI4XBn-huzygC',)
    2024-03-07 15:41:35,321 INFO sqlalchemy.engine.Engine COMMIT

    SQL 로그에서 확인할 수 있다시피 address1과 address2를 제외된 채로 SELECT를 하게 된다.

     

    3.3 insert query 

    INSERT를 하게 된다면 "address1", "address2"는 제외된 채로 삽입된다. orm query를 다음과 같이 날려보자.

    member_entity = MemberEntity.new(
        name="test",
        age=123,
    )
    
    session.add(member_entity) # insert query

    이에 대한 SQL 로그는  다음과 같이 발생한다.

    2024-03-07 16:12:42,418 INFO sqlalchemy.engine.Engine BEGIN (implicit)
    2024-03-07 16:12:42,421 INFO sqlalchemy.engine.Engine INSERT INTO `member` (nanoid, name, age) VALUES (%s, %s, %s)
    2024-03-07 16:12:42,421 INFO sqlalchemy.engine.Engine [generated in 0.00042s] ('SjcWRG2qoIel7zj_siBwEUBU', 'test', 123)
    2024-03-07 16:12:42,426 INFO sqlalchemy.engine.Engine COMMIT

     

    마치며

    필요에 의해 조사한 기능이긴 하지만 아직 적확한 예제가 어떤 게 있을지 생각해봐야 할 듯싶다. TABLE 상에서 로우 레벨부터 못 읽어 들이게 제한될만한 일이 있을 때 사용하면 좋을 수 있겠단 생각만 막연히 든다. 일단은 imperative mapping을 통해 사용할 수 있는 기능 하나를 더 알게 되었다는 것에 의미를 두자.

    728x90
    반응형