Flask ORM Query에 대해 알아보자

빈코더·2021년 3월 17일
1
post-thumbnail

Flask ORM Query에 대해서 알아보자.

Flask를 모델링을 진행하는데 SQLAlchemy를 사용하여 진행을 한다.

그런데 Django랑 많이 달라서 정리를 한번 하고 진행하려고 한다.

1. SELECT statements

SELECT문은 객체select()를 반환하는 함수에 의해 생성된다.

from sqlalchemy import select
select = select(User).where(User.name == 'bino')

select에 저장된 값은 Session.execute()로 호출이 가능하다.

>>> result = session.execute(stmt)
>>> for user_obj in result.scalars():
...     print(f"{user_obj.name} {user_obj.fullname}")
bino song bino

2. Session만들기

Session 만들기

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

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

모듈 레벨에서 작성하고 있어서 Engine이 아직 존재하지 않는다면

Session = sessionmaker()

이후에 engine을 생성하고 session의 configure를 이용한다.

Session.configure(bind=engine)

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

다른 트랜잭션을 위한 것들을 sessionmaker()에서 호출될 때 정의되야한다.

이제부터 언제든 데이터베이스와의 대화가 필요할때 session을 불러서 쓰면 된다.

session = Session()

위 Session은 Engine과 연결이 되어 있지만 아직 연결이 열린 상태는 아니다.

앞서와 같이 처음으로 사용될 때 Engine과 연결되고 모든 변경을 커밋하고 세션을 종료할 때까지 열려있게 된다.

Session 생성하는 패턴들

Session은 다양한 기반에 다양한 타입의 어플리케이션, 프레임워크에서 다양한 요구사항에 좋다.

그렇기 때문에 Session은 오브젝트와 일반적인 데이터베이스 접속에서 쓰면된다.

새 객체 추가하기

예제부터 확인해보도록 하자.

add_user = User('Good', 'Good bino','1234')
session.add(ed_user)

위 코드를 보면 실제로 데이터베이스에 추가된게 아니라 pending인 상태이다.

아직 데이터베이스에 발행되지 않은 상태인데 입력이 필요한 순간에는 flush라는 과정을 통해 입력이 된다.

만약 디비에 쿼리를 하면 모든 pending된 정보는 flush되고 접근 가능한 상태가 된다.(아직 pending상태)

예를 들면 아래 코드는 User 인스턴스를 로드해 새 Query 객체를 생성한다.

our_user = session.query(User).filter_by(name='bino').first()

사실 Session은 내부적으로 맵구조의 객체라 반환하는 값이 우리가 기존에 집어넣은 인스턴스랑 동일하다.

ORM의 컨셉이 identity map이라서 session에서 하는 모든 처리들이 실제 데이터셋과 함께 동작한다.

Session에서 PK를 가지면 같은 PK를 가진 객체를 반환한다.

그러니까 이미 있는 PK를 입력하면 에러가 난다.

add_all()로 한방에 추가할 수도 있다.

session.add_all([
    User('Song', 'Song Minho', 'asdf123'),
    User('Back', 'Back Good', 'asdf456'),
    User('Pack', 'Pack Good', 'asdf789')])

변경도 가능하다.

add_user.password = 'test1234'

Session은 계속 연결되어 있는 객체를 계속 주시하고 있다. 위처럼 수정하면 session은 이미 알고 있다.

session.dirty        # IdentitySet([<User('Good', 'Good bino', 'test1234')>])

새로 추가한 유저도 확인 가능하다.

session.new
IdentitySet([<User('Song', 'Song Minho', 'asdf123')>,
    		 <User('Back', 'Back Good', 'asdf456')>,
    		 <User('Pack', 'Pack Good', 'asdf789')>])

Session에 pending된 애들을 실행시키려면

session.commit()

commit()은 모든 변경,추가 이력을 반영한다. 이 트랜잭션이 모두 실행되면 세션은 다시 connection pool을 반환하고 물려있던 모든 객체들을 업데이트한다.

Session이 새 행을 데이터베이스에 입력한 이후에 새로 생성된 행들은 식별자들과 데이터베이스에서 기본으로 설정된 값들을 instance에서 사용할 수 있게 된다.

즉시 사용할 수 있거나 첫 액세스에 로딩될 때 모두 사용할 수 있다. 위 경우엔 commit()을 실행한 이후 새 트랜잭션이 실행되어 모든 행이 다시 로드된 상태다.

sqlalchemy에서는 기본적으로 이전 트랜잭션에서 새 트랜잭션으로 처음 실행될 때 모든 데이터를 새로 가져온다.

그래서 가장 최근의 상태를 바로 사용할 수 있다. 다시 불러오는 레벨을 설정하고 싶으면 세션 사용하기 문서를 확인하자.

세션 객체의 상태들

User객체가 Session 외부에서 PK없이 Session 안에 들어가고 실제로 데이터베이스에 추가될때 까지 각 "객체 상태"를 가지고 있다. transient, pending, presistent, deleted 4가지. 이 상태들을 알고 있으면 도움이 많이 된다.

  • Transient : 세션에 없고 데이터베이스에 저장되지 않는 인스턴스, 즉 데이터베이스 ID가 없다. 이러한객체가 ORM에 대해 유일한 관계는 해당 클래스에 연관된 mapper()가 있다는 것이다.
  • Pending : 임시 인스턴스를 Session.add()하면 보류상태가 된다. 아직 실제로 데이터베이스로 Flush되지 않았지만 다음 Flush가 될때 발생한다.
  • Presistent : 세션에 존재하고 데이터베이스에 레코드가 있는 인스턴스이다. 보류중인 인스턴스가 영구적이되도록 Flush하거나 기존 인스턴스에 대한 데이터베이스 쿼리(또는 다른 세션의 영구 인스턴스로 로컬 세션으로 이동)하여 영구 인스턴스를 얻는다.
  • Deleted : 플러시 내에서 삭제되었지만 트랜잭션이 아직 완료지 않은 인스턴스이다. 이상태의 개체는 기본적으로 "보류"상태와 반대이다. 세션의 트랙잭션이 커밋되면 개체가 분리 된 상태로 이동한다. 또는 세션의 트랜잭션이 롤백되면 삭제된 개체가 영구 상태로 돌아간다.

일단은 Document에 있는 내용을 번역기 돌린건데.. 아직은 무슨말인지는 잘 모르겠다.

롤백하기

Session이 트랜잭션으로 동작하고 나서 우린 롤백 하는 것도 가능하다. 롤백하기 위해 값을 변경하자.

add_user.name = 'odin'

그리고 가짜 유저를 하나 생성

fake_user = User('fakeuser', 'goood','123456789')
session.add(fake_user)

session을 query하면 일단 flush된 현재 트랙잭션을 확인이 가능하다.

session.query(User).filter(User.name.in_(['odin', 'fakeuser'])).all()
#[<User('odin', 'Good bino', 'test1234')>, <User('fakeuser', 'goood', '123456789')>]

롤백을 실행하면 변경하기 전 상태로 돌아간다.

session.rollback()
edd_user.name        #Good
fake_user in session #False

Query 보내기

Query객체는 session에서 query()메소드로 생성한다. 이 함수는 다양한 수의 argument를 가질 수 있는데 다양한 클래스의 조합과 클래스 descriptor를 사용할 수 있다.

사실 Query는 User인스턴스를 부를때 이미 써봤다. iterative context를 evaluated할 때, 객체 리스트를 반환한다.

for insntace in session.query(User).orber_by(User.id):
	print(instance.name, instance.fullname)

query()

Query는 keyedTuple 클래스를 통해 튜플로 반환하는데 일반적인 파이썬 객체처럼 활용할 수 있다.
각 저장된 값들을 클래스 이름이나 속성 이름과 동일하다.

for row in session.query(User. User.name).all()
	print(row.User, row.name)

label()

label()을 이용하면 컬럼 이름을 다르게 쓸 수 있다.

for row in session.query(User.name.label('name_label')).all():
	print(row.name_label)

aliased()

컬럼은 위 방식으로 하지만 User 같은 클래스 엔티티는 aliased를 이용해 제어할 수 있다.

from sqlalchemy.orm import aliased
user_alias = aliased(User, name='user_alias')
for row in session.query(user_alias, user_alias.name).all()
	print(row.user_alias)

slice

LIMIT이나 OFFSET을 포함한 기본적인 Query 동작은 order by와 함께 파이썬 배열에서 쪼개는(slice) 방식처럼 쓰면 된다.

for user in session.query(User).order_by(User.id)[1:3]:
    print user

filter_by()

결과물을 filter 할 때에는 filter_by()를 쓰면 된다.

for name in session.query(User.name).filter_by(fullname='Good bino'):
    print name

filter()

filter()를 쓰면 좀더 유연한 SQL표현을 쓸 수 있다. 매핑클래스에서 사용한 클래스 단위의 속성과 파이썬 표준 연산자를 쓸 수 있다.

for name in session.query(User.name).filter(User.fullname=='Good bino'):
    print name
    
#equals
query.filter(User.name == 'ed')

#not equals
query.filter(User.name != 'ed')

#LIKE
query.filter(User.name.like('%ed%'))

#IN
query.filter(User.name.in_(['ed', 'wendy', 'jack']))
서브쿼리식으로도 사용 가능
query.filter(User.name.in_(session.query(User.name).filter(User.name.like('%ed%'))))

#NOT IN
query.filter(~User.name.in_(['ed', 'wendy', 'jack']))

#IS NULL
filter(User.name == None)

#IS NOT NULL
filter(User.name != None)

#AND
from sqlalchemy import and_
filter(and_(User.name == 'ed', User.fillname == 'Edward Kim'))
또는 위에서 본 체이닝 메소드로
filter(User.name == 'ed').filter(User.fullname == 'Edward Kim')

#OR
from sqlalchemy import or_
filter(or_(User.name == 'ed', User.name == 'wendy'))

#match
query.filter(User.name.match('wendy'))

체인방식

Query 객체는 완전 생산적이라 대부분의 메소드 호출은 새 Query 객체를 반환한다. 따라서 아래와 같이 꼬리를 무는 체이닝 방식으로 사용이 가능하다. (Where … And … 식으로 된다.)

for name in session.query(User).\
            filter(User.name=='Good').\
            filter(User.fullname=='Good bino'):
    print user

리스트와 Scalars 반환하기

Query 객체의 all(), one(), first() 메소드는 즉시 SQL을 호출하고 non-iterator 값을 반환한다. all()은 리스트를 반환한다.

query = session.query(User).filter(User.name.like('good%')).order_by(User.id)
query.all()

first()

first()는 첫째를 리밋으로 설정해 scalar로 가져온다.

query.first()

one()

one()은 모든 행을 참조해 식별자를 값으로 가지고 있지 않거나 여러 행이 동일한 값을 가지고 있는 경우 에러를 만든다.

from sqlalchemy.orm.exc import MultipleResultsFound
try:
	user = query.one()
except MultipleResultsFound, e:
	print(e)
    
from sqlalchemy.orm.exc import NoResultFound
try:
	user = query.filter(User.id == 99).one()
except NoResultFound, e:
	print(e)

문자로 된 SQL 사용하기

문자열을 Query와 함께 유연하게 쓸 수 있다. 대부분 메소드는 문자열을 수용한다.

예를 들면 filter()와 order_by()에서 쓸 수 있다.

for user in session.query(User).filter("id<224").order_by("id").all():
	print user.name

params()

열결된 파라미터에서 콜론을 이용한, 더 세세한 문자열 기반의 SQL을 사용할 수 있다.

값을 사용할 때 param() 메소드를 사용한다.

session.query(User).filter("id<:value and name=:name").\
	params(value=1234, name='Good').order_by(User.id).one()

from_statement()

문자열 기반의 일반적인 쿼리를 사용하고 싶다면 from_statement()를 쓴다. 대신 컬럼들은 매퍼에서 선언된 것과 동일하게 써야한다.

session.query(User).from_statement("SELECT * FROM users WHERE name=:name").\
	params(name='Good').all()

또한 from_statement() 아래와 같은 문자열 SQL 방식으로도 쓸 수 있다.

session.query("id", "name", "thenumber12").\
        from_statement("SELECT id, name, 12 as "
                "thenumber12 FROM users WHERE name=:name").\
        params(name='Good').all()

문자열 SQL의 장단점

Query로 생성해서 쓰는건 sqlalchemy의 이점인데 그렇게 쓰지 않으면 당연히 안좋아지는 부분이다.
직접 쓰면 특정하게 자기가 필요한 결과물을 쉽게 만들어낼 수 있지만 Query는 더이상 SQL구조에서 아무 의미 없어지고
새로운 문맥으로 접근할 수 있도록 변환하는 능력이 상실된다.

예를 들면 User 객체를 선택하고 name 컬럼으로 정렬하는데 name이란 문자열을 쓸 수 있다.

q = session.query(User.id, User.name)
q.order_by("name").all()

지금은 문제가 없다. Query를 쓰기 전에 뭔가 멋진 방식을 사용해야 할 때가 있다.
예를 들면 아래처럼 from_self() 같은 고급 매소드를 사용해, 사용자 이름의 길이가 다른 경우를 비교할 수 있다.

from_self()

from sqlalchemy import func
ua = aliased(User)
q = q.from_self(User.id, User.name, ua.name).filter(User.name < ua.name).\
	filter(func.length(ua.name) ! = func.length(User.name))

Query는 서브쿼리에서 불러온 것처럼 나타내는데 User는 내부와 외부 양쪽에서 불러오게 된다.
이제 Query에게 name으로 정렬하라고 명령하면 어느 name을 기준으로 정렬할지 코드로는 예측할 수 없게 된다. 이경우네는 바깥과 상관없이 aliased된 User를 기준이로 정렬된다.

q.order_by("name").all()
[(3, u'fred', u'haruair'), (4, u'haruair', u'mary'), (2, u'mary', u'wendy'), (3, u'fred', u'wendy'), (4, u'haruair', u'wendy')]

User.name 또는 ua.name같이 SQL요소를 직접 쓰면 Query가 알 수 있을 만큼 충분한 정보를 제공하기 때문에 어떤 name을 기준으로 정렬해야할지 명확하게 판단하게 된다. 그래서 아래 두가지 와 같은 차이를 볼 수 있다.

q.order_by(ua.name).all()
# [(3, u'fred', u'haruair'), (4, u'haruair', u'mary'), (2, u'mary', u'wendy'), (3, u'fred', u'wendy'), (4, u'haruair', u'wendy')]

q.order_by(User.name).all()
# [(3, u'fred', u'wendy'), (3, u'fred', u'haruair'), (4, u'haruair', u'wendy'), (4, u'haruair', u'mary'), (2, u'mary', u'wendy')]

숫자세기

Query는 count()라는 숫자를 세는 편리한 메소드를 포함한다.

count()

session.query(User).filter(User.name.like('haru%')).count()

count()는 몇개의 행이 반환될지 알려준다. 위 코드로 생성되는 SQL을 살펴보면, SQLAlchemy는 항상 어떤 쿼리가 오더라도 거기서 행의 수를 센다. ELECT count(*) FROM table 하면 단순해지지만 최근 버전의 SQLAlchemy는 정확한 SQL로 명시적으로 판단할 수 있는 경우를 추측해서 처리하지 않는다.

func.count()

숫자를 세야 할 필요가 있는 경우에는 func.count()로 명시적으로 작성하며 된다.

from sqlalchemy import func
session.query(func.count(User.name), User.name).group_by(User.name).all()
# [(1, u'fred'), (1, u'haruair'), (1, u'mary'), (1, u'wendy')]

SELECT count(*) FROM table만 하고 싶으면

session.query(func.count('*')).select_from(User).scalar()

User의 primary key를 사용하면 select_from 없이 사용할 수 있다.

session.query(func.count(User.id)).scalar()

관계(relationship) 만들기

1 to 1, 1 to many, many to many를 만들어보자.
일단 기본적인 Foreignkey를 어떻게 만드는지 확인해보자.

from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship, backref

class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    email_address = Column(String, nullable=False)
	user_id = Column(Integer, ForeignKey('users.id'))
    
    user = relationship("User", backref=backref('addresses', order_by=id))
    
    def __init__(self, email_address):
    	self.email_address = email_address

    def __repr__(self):
        return "<Address('%s')>" % self.email_address

위에서 작성한 ForeignKey는 addresses.user_id 컬럼이 users.id 컬럼을 따르도록 만든다.

두번째 지시자인 relationship은 ORM에게 address 클래스 자체가 User 클래그에 연결되어 있다는 사실은 Address.user 속성을 이용해 알 수 있게 해준다. relationship()은 외래키 연결에서 두 테이블 사이에 Address.user로 다대일 관계임을 결정한다.

덧붙여 relationship()내에서 호출하는 backref()는 역으로 클래스를 이용할 수 있도록 즉 Address 객체에서 User를 참조할 수 있ㄷ로고 User.address를 구현한다. 다대일 관계의 반대측은 항상 일대다의 관계이기 때문이다.

Address.user와 User.address의 관계는 양방향 관계(bidirectional relationship)로 SQLAlchemy ORM의 주요 특성이다.

relationship()을 원격 클래스를 객체가 아닌 문자열로 연결하는 것에 대해 Declarative시스템에서 사용하는 것으로 문제가 될 수 있지 않나 생각해볼 수 있다.전부 맵핑이 완료된 경우, 이런 문자열은 파이썬 표현처럼 다뤄지며 실제 아큐먼트를 처리하기 위해 사용된다. 위의 경우에선 User 클래스가 그렇다. 이런 이름들은 이것이 만들어지는 동안에만 허용되고 모든 클래스 이름은 기본적으로 선언될 때 사용이 가능해진다.(주 클래스의 선언이 순차적으로 진행되기 때문에 클래스 선언 이전엔 에러가 나므로 이런 방식을 사용하는 것으로 보인다.)

class User(Base):
    # ...
    addresses = relationship("Address", order_by="Address.id", backref="user")
  • 대부분의 관계형 데이터베이스에선 외래키 제약이 primany key컬럼이나 Unique컬럼에만 가능하다.
  • 다중 컬럼primany key에서 외래키 제약은 스스로 다중 컬럼을 가지는데 이를 합성외래키(composite foreign key)라고 한다. 이 또한 이 컬럼의 서브셋을 레퍼런스로 가질 수 있다.
  • 외래키 컬럼은 열결된 컬럼이나 행의 변화에 자동으로 그들 스스로 업데이트 한다. 이걸 CASCADE referential action이라고 하는데 관계형 데이터베이스에 내장된 함수다.
  • 외래키는 스스로 테이블을 참고할 수 있다. 이걸 자기 참조(self-referential)외래키라고 한다.

addresses 테이블을 데이터베이스에 생성해야 하므로 metadata로부터 새로운 CREATE를 발행한다. 이미 생성된 테이블은 생략하고 생성한다.

Base.metadata.create_all(engine)

관계된 객체 활용

위에서 코드만 간단하게 해서 user와 address를 만들어보았다

이제 이것을 활용을 해보자.

jack = User('jack', 'Jack Bean', 'sadfjklas')
jack.addresses # [] 빈 리스트를 반환

자유롭게 Address 객체를 User 객체에 넣을 수 있다. 그냥 리스틑 사용법이랑 비슷하다.

jack.addresses = [
                Address(email_address='jack@gmail.com'),
                Address(email_address='jack@yahoo.com')]

양방향 관계인 경우 자동으로 양쪽에서 접근할 수 있게 된다. 별도의 SQL 없이 양쪽에 on-change events로 동작한다.

jack.addresses[1]       # <Address(email_address='jack@yahoo.com')>
jack.addresses[1].user  # <User('jack', 'Jack Bean', 'sadfjklas')>

데이터베이스에 저장해보자. User인 Jack Bean을 저장하면 두 Address도 알아서 cascading으로 저장된다.

session.add(jack)
session.commit()

Jack을 쿼리해서 다시 불러보자. 이렇게 Query하면 아직 주소들은 SQL을 호출하지 않은 상태다.

jack = session.query(User).filter_by(name='jack').one()
Jack        # <User('jack', 'Jack Bean', 'sadfjklas')>

하지만 addresses 컬랙션을 호출하는 순간 SQL이 만들어진다.

jack.addresses
# [<Address(email_address='jack@gmail.com)>,<Address(email_address='jack@yahoo.com')>]

이렇게 뒤늦게 SQL로 불러오는걸 게으른 불러오기 관계(lazy loading relationship)라고 한다. 이 addresses는 이제 불러와 평범한 리스트처럼 동작한다.

join과 함께 쿼리하기

두 테이블이 있는데 Query의 기능으로 양 테이블을 한방에 가져오는 방법을 살펴볼 것이다. SQL JOIN에 대해 join 하는 방법과 여러가지 좋은 설명이 위키피디아에 있으니 참고.

간단하게 User와 Address 두 테이블을 완전 조인하는 방법은 Query.filter()로 관계있는 두 컬럼이 동일한 경우를 찾으면 된다.

for u, a in session.query(User, Address).filter(User.id==Address.user_id).\
                    filter(Address.email_address=='jack@gmail.com').all():
    print (u, a)
# <User('jack', 'Jack Bean', 'sadfjklas')> <Address('jack@gmail.com')>

반면 진짜 SQL JOIN 문법을 쓰려면 Query.join()을 쓴다.

session.query(User).join(Address).\
        filter(Address.email_address=='jack@gmail.com').all()
# [<User('jack', 'Jack Bean', 'sadfjklas')>]

Query.join()은 User와 Address 사이에 있는 하나의 외래키를 기준으로 join한다. 만약 외래키가 없거나 여러개라면 Query.join() 아래같은 방식을 써야한다.

query.join(Address, User.id==Address.user_id)   # 정확한 상태를 적어줌
query.join(User.addresses)                      # 명확한 관계 표기 (좌에서 우로)
query.join(Address, User.addresses)             # 동일, 명확하게 목표를 정해줌
query.join('addresses')                         # 동일, 문자열 이용

외부 join은 outerjoin()을 쓴다.

query.outerjoin(User.addresses)     # left outer join

join()이 궁금하면 문서를 참고하자. 어떤 SQL에서든 중요한 기능이다.

별칭(aliases) 사용하기

여러 테이블을 쿼리하면 같은 테이블을 여러개 불러와야 할 때가 있는데 그럴 때 동일 테이블 명칭에 별칭(alias)를 지정해 다른 테이블과 문제를 이르키지 않도록 해야한다. Query는 별칭으로 된 녀석들도 잘 처리를 해준다. 아래 코드는 Address엔티티를 두번 조인해서 한행에 두 이메일 주소를 가져오도록 하는 예시이다.

from sqlalchemy.orm import aliased
adalias1 = aliased(Address)
adalias2 = aliased(Address)
for username, email1, email2 in \
    session.query(User.name, adalias1.email_address, adalias2.email_address).\
    join(adalias1, User.addresses).\
    join(adalias2, User.addresses).\
    filter(adalias1.email_address=='jack@gmail.com').\
    filter(adalias2.email_address=='jack@yahoo.com'):
    print username, email1, email2
# jack jack@gmail.com jack@yahoo.com

서브쿼리 사용하기(subquery())

Query는 서브쿼리 만들 때에도 유용하다. User 객체가 몇개의 Address를 가지고 있는지 알고 싶을 때 서브쿼리는 유용하다. SQL을 만드는 방식으로 생각하면 주소 목록의 수를 사용자 id를 기준으로 묶은 후(grouped by), User와 join하면 된다. 이 상황에선 LEFT OUTER JOIN이 사용자의 모든 주소를 가져오므로 적합하다. SQL의 예를 보자.

SELECT users.*, adr_count.address_count
FROM users
LEFT OUTER JOIN (
        SELECT user_id, count(*) AS address_count
        FROM addresses GROUP BY user_id
    ) AS adr_count
    ON users.id = adr_count.user_id

Query를 사용하면 명령문을 안에서 밖으로 빼내듯 쓸 수 있다. 명령문 접근자는 일반적인 Query를 통해 SQL 표현을 나타내는 명령문을 생성해 반환한다. 이건 select()를 쓰는 것과 비슷하다

from sqlalchemy.sql import func
stmt = session.query(Address.user_id, func.count('*').label('address_count')).\
        group_by(Address.user_id).subquery()

func 키워드는 SQL 함수를 만들고 subquery() 메소드는 별칭을 이용해 다른 query에 포함할 수 있는 SELECT 명령문의 형태로 반환해준다. (query.statement.alias()를 줄인 것)

이렇게 만든 서브쿼리는 Table처럼 동작한다. 아래 코드를 잘 모르겠으면 튜토리얼 앞부분에서 Table을 어떻게 다뤘는지 살펴보면 도움이 된다. 여기서는 컬럼에 접근할 때 table.c.컬럼명으로 접근했던, 그 방법처럼 사용한다.

for u, count in session.query(User, stmt.c.address_count).\
    outerjoin(stmt, User.id==stmt.c.user_id).order_by(User.id):
    print(u, count)
# <User('wendy', 'Wendy Williams', 'foobar')> None
# <User('mary', 'Mary Contrary', 'xxg527')> None
# <User('fred', 'Fred Flinstone', 'blar')> None
# <User('haruair', 'Edward Kim', '1234')> None
# <User('jack', 'Jack Bean', 'sadfjklas')> 2

서브쿼리서 엔티티 선택하기

위에서는 서브쿼리서 컬럼을 가져와서 결과를 만들었다. 만약 서브쿼리가 엔티티를 선택하기 위한 맵이라면 aliased()로 매핑된 클래스를 서브쿼리로 활용할 수 있다.

stmt = session.query(Address).filter(Address.email_address !=\
		'jack@yahoo.com').subquery()
adalias = aliased(Address, stmt)
for user, address in session.query(User, adalias).\
        join(adalias, User.addresses):
    print(user, address)
# <User('jack', 'Jack Bean', 'sadfjklas')> <Address('jack@gmail.com')>

exists사용하기

SQL에서 EXISTS 키워드는 불린 연산자로 조건에 맞는 행이 있으면 True를 반환한다. 이건 많은 시나리오에서 join을 위해 쓰는데, join에서 관계 테이블서 적합한 값이 없는 행을 처리하는데에도 유용하다.

외부 EXISTS는 이런 방식으로 할 수 있다.

from sqlalchemy.sql import exists
stmt = exists().where(Address.user_id==User.id)
for name, in session.query(User.name).filter(stmt):
    print(name)

Query의 기능 중 몇가지 연산자에서는 EXISTS를 자동으로 사용한다. 위 같은 경우는 User.addresses 관계에 any()를 사용하면 가능하다.

for name, in ssession.query(User.name).\
        filter(Us(r.ad)dresses.any()):
    print(name)
# jack

any()는 특정 기준이 있어 제한적으로 매치해준다.

for name, in session.query(User.name).\
    filter(User.addresses.any(Address.email_address.like('%gmail%'))):
    print(name)
# jack

3. Method

여러가지 Method에 대해서 알아보자.

__version__

버전 확인 method

import sqlalchemy
print sqlalchemy.__version__
1.3.23

__tablename__

정의한 클래스의 테이블 명을 __tablename__에 선언한 테이블 명으로 변경한다.

Django의 class Meta db_talbe과 비슷한 역할

class User(Base):
    __tablename__ = 'users'

__eq__() 다대일에서의 equals 비교

query.filter(Address.user == someuser)

__ne__() 다대일에서의 not equals 비교

query.filter(Address.user != someuser)

IS NULL 다대일 비교 (eq())

query.filter(Address.user == None)

contains() 일대다 컬렉션에서 사용

query.filter(User.addresses.contains(someaddress))

any() 컬렉션에서 사용

query.filter(User.addresses.any(Address.email_address == 'bar'))
#키워드 아규먼트도 받음
query.filter(User.addresses.any(email_address='bar'))

has() scalar 레퍼런스서 사용

query.filter(Address.user.has(name='ed'))

Query.with_parent() 어떤 관계서든 사용

session.query(Address).with_parent(someuser, 'addresses')
profile
미래의 리눅스 토발즈

1개의 댓글

comment-user-thumbnail
2021년 4월 27일

최선을 다하는 모습이 멋지십니다 미래의 리눅스 토발지 빈코더님^^

답글 달기