이전 챕터까지 CORE 관점에서 쿼리를 활용하는 방식에 초점을 맞췄습니다. 이번 챕터에서는 ORM 방식에서 쓰이는 Session
의 구성 요소와 수명 주기, 상호 작용하는 방법을 설명합니다.
Session
객체는 ORM을 사용할 때 Insert
객체들을 만들고 트랜잭션에서 이 객체들을 내보내는 역할을 합니다.
Session
은 이러한 과정들을 수행하기 위해 객체 항목을 추가합니다.
그 후 flush라는 프로세스를 통해 새로운 항목들을 데이터베이스에 기록합니다.
이전 과정에서 우리는 Python Dict
를 사용하여 INSERT
를 실행하였습니다.
ORM에서는 테이블 메타데이터 정의에서 정의한 사용자 정의 Python 객체를 직접 사용합니다.
>>> squidward = User(name="squidward", fullname="Squidward Tentacles")
>>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")
INSERT
될 잠재적인 데이터베이스 행을 나타내는 두 개의 User
객체를 만듭니다.
ORM 매핑에 의해 자동으로 생성된 __init__()
생성자 덕에 생성자의 열 이름을 키로 사용하여 각 객체를 생성할 수 있습니다.
>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')
Core의 Insert
와 유사하게, 기본 키를 포함하지 않아도 ORM이 이를 통합시켜 줍니다.
id
의 None
값은 속성에 아직 값이 없음을 나타내기 위해 SQLAlchemy에서 제공합니다.
현재 위의 두 객체(squiward
와 krabs
)는 transient
상태라고 불리게 됩니다.
transient
상태란, 어떤 데이터베이스와 연결되지 않고, INSERT
문을 생성할 수 있는 Session
객체와도 아직 연결되지 않은 상태를 의미합니다.
>>> session = Session(engine) # 반드시 사용 후 close 해야 합니다.
>>> session.add(squidward) # Session.add() 매소드를 통해서 객체를 Session에 추가해줍니다.
>>> session.add(krabs)
객체가 Session.add()
를 통해서 Session
에 추가하게 되면, pending
상태가 되었다고 부릅니다.
pending
상태는 아직 데이터베이스에 추가되지 않은 상태입니다.
>>> session.new # session.new를 통해서 pending 상태에 있는 객체들을 확인할 수 있습니다.
IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')])
IdentitySet
은 모든 경우에 객체 ID를 hash하는 Pythonset
입니다.- 즉, Python 내장 함수 중
hash()
가 아닌,id()
메소드를 사용하고 있습니다.
Session
객체는 unit of work
패턴을 사용합니다. 이는 변경 사항을 누적하지만, 필요할 때까지는 실제로 데이터베이스와 통신을 하지 않음을 의미합니다.
이런 동작 방식을 통해서 위에서 언급한 pending
상태의 객체들이 더 효율적인 SQL DML로 사용됩니다.
현재의 변경된 사항들을 실제로 Database에 SQL을 통해 내보내는 작업을 flush 이라고 합니다.
>>> session.flush()
"""
INSERT INTO user_account (name, fullname) VALUES (?, ?)
[...] ('squidward', 'Squidward Tentacles')
INSERT INTO user_account (name, fullname) VALUES (?, ?)
[...] ('ehkrabs', 'Eugene H. Krabs')
"""
이제 트랜잭션은 Session.commit()
, Session.rollback()
, Session.close()
중 하나가 호출될 때 까지 열린 상태로 유지됩니다.
Session.flush()
를 직접 사용하여, 현재 pending
상태에 있는 내용을 직접 밀어넣을 수 있지만, Session은 autoflush라는 동작을 특징으로 하므로 일반적으로는 필요하지 않습니다. Session.commit()
이 호출 될 때 마다 변경 사항을 flush 합니다.
행이 삽입되게 되면, 우리가 생성한 Python 객체는 persistent
라는 상태가 됩니다.
persistent
상태는 로드된 Session
객체와 연결됩니다.
INSERT
실행 시, ORM이 각각의 새 객체에 대한 기본 키 식별자를 검색하는 효과를 가져옵니다.
이전에 소개한것과 동일한 CursorResult.inserted_primary_key
접근자를 사용합니다.
>>> squidward.id
4
>>> krabs.id
5
ORM이 flush 될 때,
executemany
대신, 두 개의 다른 INSERT 문을 사용하는 이유가 바로 이CursorResult.inserted_primary_key
때문입니다.
- SQLite의 경우 한 번에 한 열을
INSERT
해야 자동 증가 기능을 사용할 수 있습니다.(PostgreSQL의 IDENTITY나 SERIAL 기능등 다른 다양한 데이터베이스들의 경우들도 이처럼 동작합니다.)psycopg2
와 같이 한번에 많은 데이터에 대한 기본 키 정보를 제공 받을 수 있는 데이터베이스가 연결되어 있다면, ORM은 이를 최적화하여 많은 열을 한번에INSERT
하도록 합니다.
Identity Map
(ID Map
)은 현재 메모리에 로드된 모든 객체를 기본 키 ID에 연결하는 메모리 내 저장소입니다.
Session.get()
을 통해서 객체 중 하나를 검색할 수 있습니다.
이 메소드는 객체가 메모리에 있으면, ID Map
에서, 그렇지 않으면 SELECT
문을 통해서 객체를 검색합니다.
>>> some_squidward = session.get(User, 4)
>>> some_squidward
User(id=4, name='squidward', fullname='Squidward Tentacles')
중요한 점은, ID Map
은 Python 객체 중에서도 고유한 객체를 유지하고 있다는 점입니다.
>>> some_squidward is squidward
True
ID map
은 동기화되지 않은 상태에서, 트랜잭션 내에서 복잡한 개체 집합을 조작할 수 있도록 하는 중요한 기능입니다.
현재까지의 변경사항을 트랜잭션에 commit
합니다.
>>> session.commit()
COMMIT
ORM을 통해 UPDATE
하는 방법에는 2가지 방법이 있습니다.
Session
에서 사용하는unit of work
패턴 방식이 있습니다. 변경사항이 있는 기본 키 별로UPDATE
작업이 순서대로 내보내지게 됩니다.- "ORM 사용 업데이트"라고 하며 명시적으로 Session과 함께
Update
구성을 사용할 수도 있습니다.
>>> sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one()
"""
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('sandy',)
"""
이 'Sandy' 유저 객체는 데이터베이스에서 행, 더 구체적으로는 트랙잭션 측면에서 기본 키가 2인 행에 대한 proxy
역할을 합니다.
>>> sandy
User(id=2, name='sandy', fullname='Sandy Cheeks')
>>> sandy.fullname = "Sandy Squirrel" # 객체의 속성을 변화시키면, Session은 이 변화를 기록합니다.
>>> sandy in session.dirty # 이렇게 변한 객체는 dirty 라고 불리우며 session.dirty에서 확인 할 수 있습니다.
True
Session이 flush
를 실행하게 되면, 데이터베이스에서 UPDATE
가 실행되어 데이터베이스에 실제로 값을 갱신합니다. SELECT
문을 추가로 실행하게 되면, 자동으로 flush
가 실행되어 sandy의 바뀐 이름 값을 SELECT
를 통해서 바로 얻을 수 있습니다.
>>> sandy_fullname = session.execute(
... select(User.fullname).where(User.id == 2)
... ).scalar_one()
"""
UPDATE user_account SET fullname=? WHERE user_account.id = ?
[...] ('Sandy Squirrel', 2)
SELECT user_account.fullname
FROM user_account
WHERE user_account.id = ?
[...] (2,)
"""
>>> print(sandy_fullname)
Sandy Squirrel
# flush를 통해 sandy의 변화가 실제로 데이터베이스에 반영되어, dirty 속성을 잃게 됩니다.
>>> sandy in session.dirty
False
ORM을 통해 UPDATE
하는 마지막 방법으로 ORM 사용 업데이트
를 명시적으로 사용하는 방법이 있습니다. 이를 사용하면 한 번에 많은 행에 영향을 줄 수 있는 일반 SQL UPDATE
문을 사용할 수 있습니다.
>>> session.execute(
... update(User).
... where(User.name == "sandy").
... values(fullname="Sandy Squirrel Extraordinaire")
... )
"""
UPDATE user_account SET fullname=? WHERE user_account.name = ?
[...] ('Sandy Squirrel Extraordinaire', 'sandy')
"""
<sqlalchemy.engine.cursor.CursorResult object ...>
현재 Session
에서 주어진 조건과 일치하는 객체가 있다면, 이 객체에도 해당하는 update
가 반영되게 됩니다.
>>> sandy.fullname
'Sandy Squirrel Extraordinaire'
Session.delete()
메서드를 사용하여 개별 ORM 객체를 삭제 대상으로 표시할 수 있습니다. delete
가 수행되면, 해당 Session
에 존재하는 객체들은 expired
상태가 되게 됩니다.
>>> patrick = session.get(User, 3)
"""
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name,
user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (3,)
"""
>>> session.delete(patrick) # patrik을 삭제 할 것이라고 명시
>>> session.execute(select(User).where(User.name == "patrick")).first() # 이 시점에서 flush 실행
"""
SELECT address.id AS address_id, address.email_address AS address_email_address,
address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[...] (3,)
DELETE FROM user_account WHERE user_account.id = ?
[...] (3,)
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('patrick',)
"""
>>> squidward in session # Session에서 만료되면, 해당 객체는 session에서 삭제됩니다.
False
위의 UPDATE
에서 사용된 'Sandy'와 마찬가지로, 해당 작업들은 진행중인 트랜잭션에서만 이루어진 일이며 commit
하지 않는 이상, 언제든 취소할 수 있습니다.
UPDATE
와 마찬가지로 ORM 사용 삭제하기
도 있습니다.
# 예시를 위한 작업일 뿐, 실제로 delete에서 필요한 작업은 아닙니다.
>>> squidward = session.get(User, 4)
"""
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name,
user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (4,)
"""
>>> session.execute(delete(User).where(User.name == "squidward"))
"""
DELETE FROM user_account WHERE user_account.name = ?
[...] ('squidward',)
<sqlalchemy.engine.cursor.CursorResult object at 0x...>
"""
Session
에는 현재의 작업들을 롤백하는 Session.rollback()
메소드가 존재합니다. 이 메소드는 위에서 사용된 sandy
와 같은 Python 객체에도 영향을 미칩니다.
Session.rollback()
을 호출하면 트랜잭션을 롤백할 뿐만 아니라 현재 이 Session
과 연결된 모든 객체를 expired
상태로 바꿉니다. 이러한 상태 변경은 다음에 객체에 접근 할 때 스스로 새로 고침을 하는 효과가 있고 이러한 프로세스를 지연 로딩
이라고 합니다.
>>> session.rollback()
ROLLBACK
expired
상태의 객체인 sandy
를 자세히 보면, 특별한 SQLAlchemy 관련 상태 객체를 제외하고 다른 정보가 남아 있지 않음을 볼 수 있습니다.
>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}
>>> sandy.fullname # session이 만료되었으므로, 해당 객체 속성에 접근 시, 트랜잭션이 새로 일어납니다.
"""
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name,
user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (2,)
"""
'Sandy Cheeks'
>>> sandy.__dict__ #이제 데이터베이스 행이 sandy 객체에도 채워진 것을 볼 수 있습니다.
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>,
'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}
삭제된 객체에 대해서도, Session
에 다시 복원되었으며 데이터베이스에도 다시 나타나는 걸 볼 수 있습니다.
>>> patrick in session
True
>>> session.execute(select(User).where(User.name == 'patrick')).scalar_one() is patrick
"""
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('patrick',)
"""
True
우리는 컨텍스트 구문 외부에서 Session
을 다뤘는데, 이런 경우 다음처럼 명시적으로 Session
을 닫아주는 것이 좋습니다.
>>> session.close()
ROLLBACK
마찬가지로 컨텍스트 구문을 통해 생성한 Session
을 컨텍스트 구문 내에서 닫으면 다음 작업들이 수행됩니다.
- 진행 중인 모든 트랜잭션을 취소(예: 롤백)하여 연결 풀에 대한 모든 연결 리소스를 해제합니다.
- 즉,
Session
을 사용하여 일부 읽기 전용 작업을 수행한 다음, 닫을 때 트랜잭션이 롤백되었는지 확인하기 위해Session.rollback()
을 명시적으로 호출할 필요가 없습니다. 연결 풀이 이를 처리합니다.
- 즉,
Session
에서 모든 개체를 삭제합니다.- 이것은 sandy, patrick 및 squidward와 같이 이
Session
에 대해 로드한 모든 Python 개체가 이제detached
상태에 있음을 의미합니다. 예를 들어expired
상태에 있던 객체는Session.commit()
호출로 인해 현재 행의 상태를 포함하지 않고 새로 고칠 데이터베이스 트랜잭션과 더 이상 연관되지 않습니다. -
>>> squidward.name Traceback (most recent call last): ... sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x...> is not bound to a Session; attribute refresh operation cannot proceed
detached
된 객체는Session.add()
메서드를 사용하여 동일한 객체 또는 새Session
과 다시 연결될 수 있습니다. 그러면 특정 데이터베이스 행과의 관계가 다시 설정됩니다.-
>>> session.add(squidward) # session에 다시 연결 >>> squidward.name # 트랜잭션을 통해 정보를 다시 불러옵니다. """ SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname FROM user_account WHERE user_account.id = ? [...] (4,) """ 'squidward'
- 이것은 sandy, patrick 및 squidward와 같이 이
detached
상태의 개체는 되도록이면 사용을 지양해야 합니다.Session
이 닫히면 이전에 연결된 모든 개체에 대한 참조도 정리합니다. 일반적으로detached
된 객체가 필요한 경우는 웹 어플리케이션에서 방금 커밋된 개체를 뷰에서 렌더링되기 전에Session
이 닫힌 경우가 있습니다. 이 경우Session.expire_on_commit
플래그를 False로 설정합니다.