Python/sqlalchemy

sqlalchemy에서 joinedload와 Query.join()의 차이점

검정비니 2023. 10. 22. 14:54
728x90
반응형

tl;dr joinedload를 사용하면 JOIN된 전체 attribute들을 select하나, Query.join()은 query의 대상이 되는 table의 항목들만 select의 대상이 된다.

 

이 글은 SQLAlchemy의 공식 문서를 번역한 것입니다.

원문: https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#sqlalchemy.orm.joinedload

 

Joinedload는 Query.join()의 사용과 많은 유사점이 있기 때문에, 언제 어떻게 사용해야 하는지에 대해 혼동을 일으키는 경우가 많습니다. Query.join()은 쿼리 결과를 변경하는 데 사용되는 반면, joinedload()는 쿼리 결과를 변경하지 않고 렌더링된 조인의 효과를 숨기고 관련 개체만 표시되도록 하는 데 많은 노력을 기울인다는 차이점을 이해하는 것이 중요하다.

 

Loader 전략의 철학은 특정 쿼리에 어떤 로딩 스키마 집합이든 적용할 수 있으며, 결과는 변경되지 않고 관련 개체 및 컬렉션을 완전히 로드하는 데 필요한 SQL 문의 수만 변경된다는 것이다. 특정 쿼리는 모든 지연 로드를 사용하여 시작할 수 있다. 컨텍스트에 따라 사용하다 보면 특정 속성이나 컬렉션이 항상 액세스되는 것을 발견할 수 있으며, 이에 대한 로더 전략을 변경하는 것이 더 효율적이라는 것을 알 수 있다. 쿼리에 대한 다른 수정 없이 전략을 변경할 수 있으며, 결과는 동일하게 유지되지만 SQL 문은 더 적게 생성된다. 이론적으로(그리고 실제로도 거의 대부분) 쿼리에 대해 수행할 수 있는 작업은 로더 전략의 변경에 따라 다른 기본 개체 또는 관련 개체 집합을 로드하도록 만드는 것이 아니다.

 

특히 joinedload()가 반환되는 엔터티 행에 영향을 주지 않는 결과를 얻을 수 있는 방법은 쿼리에 추가하는 조인의 익명 별칭을 생성하여 쿼리의 다른 부분에서 참조할 수 없도록 하기 때문이다. 예를 들어, 아래 쿼리는 joinedload()를 사용하여 사용자에서 주소로 좌측 외부 조인을 생성하지만 Address.email_address에 대해 추가된 ORDER BY는 유효하지 않은데, 주소 엔터티가 쿼리에 이름이 지정되지 않았기 때문이다:

jack = (
    session.query(User)
    .options(joinedload(User.addresses))
    .filter(User.name == "jack")
    .order_by(Address.email_address)
    .all()
)


# 위의 SQLAlchemy 코드는 아래의 SQL문으로 변환된다.
SELECT
    addresses_1.id AS addresses_1_id,
    addresses_1.email_address AS addresses_1_email_address,
    addresses_1.user_id AS addresses_1_user_id,
    users.id AS users_id,
    users.name AS users_name,
    users.fullname AS users_fullname,
    users.nickname AS users_nickname
FROM users
LEFT OUTER JOIN addresses AS addresses_1
    ON users.id = addresses_1.user_id
WHERE users.name = ?
ORDER BY addresses.email_address   # <-- this part is wrong !

 

 

위에서는 주소가 FROM 목록에 없으므로 ORDER BY addresses.email_address가 유효하지 않게 된다. 올바른 방법은 사용자 레코드를 로드하고 이메일 주소로 주문하는 올바른 방법은 Query.join()을 사용하는 것이다:

jack = (
    session.query(User)
    .join(User.addresses)
    .filter(User.name == "jack")
    .order_by(Address.email_address)
    .all()
)


# 위의 코드는 아래의 SQL문으로 변형됩니다.
SELECT
    users.id AS users_id,
    users.name AS users_name,
    users.fullname AS users_fullname,
    users.nickname AS users_nickname
FROM users
JOIN addresses ON users.id = addresses.user_id
WHERE users.name = ?
ORDER BY addresses.email_address

 

물론 위의 문은 주소의 열이 결과에 전혀 포함되지 않는다는 점에서 이전 문과 동일하지 않다. 여기에 joinedload()를 다시 추가하면 조인이 두 개가 되는데, 하나는 order문을 위해 사용되는 조인이고 다른 하나는 User.addresses 컬렉션의 내용을 로드하는 데 익명으로 사용된다:

jack = (
    session.query(User)
    .join(User.addresses)
    .options(joinedload(User.addresses))
    .filter(User.name == "jack")
    .order_by(Address.email_address)
    .all()
)


# 위의 코드는 아래의 SQL문으로 변형됩니다.
SELECT
    addresses_1.id AS addresses_1_id,
    addresses_1.email_address AS addresses_1_email_address,
    addresses_1.user_id AS addresses_1_user_id,
    users.id AS users_id, users.name AS users_name,
    users.fullname AS users_fullname,
    users.nickname AS users_nickname
FROM users JOIN addresses
    ON users.id = addresses.user_id
LEFT OUTER JOIN addresses AS addresses_1
    ON users.id = addresses_1.user_id
WHERE users.name = ?
ORDER BY addresses.email_address

 

위에서 볼 수 있듯이 Query.join()의 사용법은 후속 쿼리 조건에 사용할 JOIN 절을 제공하는 것이고, joinedload()의 사용법은 결과의 각 사용자에 대한 User.addresses 컬렉션의 로딩에만 관련되어 있다는 것을 알 수 있다. 이 경우, 두 조인이 중복되어 보일 가능성이 높으며 실제로도 그렇다. 컬렉션 로딩과 정렬에 하나의 조인만 사용하려는 경우, 아래의 "https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html#contains-eager"에서 설명하는 contains_eager() 옵션을 사용합니다. 그러나 joinedload()가 왜 그렇게 하는지 알아보려면 특정 주소에 대해 필터링하는 경우를 생각해 보십시오:

jack = (
    session.query(User)
    .join(User.addresses)
    .options(joinedload(User.addresses))
    .filter(User.name == "jack")
    .filter(Address.email_address == "someaddress@foo.com")
    .all()
)


# 대응되는 SQL문:
SELECT
    addresses_1.id AS addresses_1_id,
    addresses_1.email_address AS addresses_1_email_address,
    addresses_1.user_id AS addresses_1_user_id,
    users.id AS users_id, users.name AS users_name,
    users.fullname AS users_fullname,
    users.nickname AS users_nickname
FROM users JOIN addresses
    ON users.id = addresses.user_id
LEFT OUTER JOIN addresses AS addresses_1
    ON users.id = addresses_1.user_id
WHERE users.name = ? AND addresses.email_address = ?

위에서 두 조인의 역할이 매우 다르다는 것을 알 수 있다. 하나는 정확히 하나의 행, 즉 Address.email_address=='someaddress@foo.com'인 User와 Address의 조인 행과 일치한다. 다른 좌측 외부 조인은 User와 관련된 모든 Address 행을 일치시키며, 반환되는 User 객체에 대해 User.addresses 컬렉션을 채우는 데만 사용된다.

 

joinedload()의 사용법을 다른 로드 스타일로 변경하면 원하는 실제 사용자 행을 검색하는 데 사용되는 SQL과 완전히 독립적으로 컬렉션이 로드되는 방식을 변경할 수 있다. 아래에서는 joinedload()를 subqueryload()로 변경한다:

jack = (
    session.query(User)
    .join(User.addresses)
    .options(subqueryload(User.addresses))
    .filter(User.name == "jack")
    .filter(Address.email_address == "someaddress@foo.com")
    .all()
)


# 대응되는 SQL문
SELECT
    users.id AS users_id,
    users.name AS users_name,
    users.fullname AS users_fullname,
    users.nickname AS users_nickname
FROM users
JOIN addresses ON users.id = addresses.user_id
WHERE
    users.name = ?
    AND addresses.email_address = ?

# ... subqueryload() emits a SELECT in order
# to load all address records ...

조인된 eagered 로딩을 사용하는 경우, 만약 쿼리의 조인에 외부에서 반환되는 행에 영향을 미치는 수정자가 포함된 경우(예: DISTINCT, LIMIT, OFFSET 또는 이와 동등한 사용 시)에는 완성된 sql문이 먼저 하위 쿼리 안에 래핑되고 조인된 열 로딩에 특별히 사용되는 조인이 하위 쿼리에 적용된다. SQLAlchemy의 조인된 eagered 로딩은 쿼리의 형식에 관계없이 컬렉션 및 관련 개체가 로드되는 방식에만 영향을 미치고 쿼리의 최종 결과에는 전혀 영향을 미치지 않도록 하기 위함이다.

 

반응형