앞서 작성한 SQLAlchemy 시작하기 – Part 1에서 이어지는 번역이다.
(여기서 뭔가 모자란 부분이나 틀린게 있으면 틀린게 맞으므로 언제든 지적해주시고, 애매한 표현은 원본 문서를 봐주시면 감사하겠습니다. 원본 문서는 SQLAlchemy Tutorial. 한글로 된 sqlalchemy 튜토리얼 있으면 알려주세요!)
리스트와 Scalars 반환하기
Query
객체의 all()
, one()
, first()
메소드는 즉시 SQL을 호출하고 non-iterator 값을 반환한다. all()
은 리스트를 반환한다.
query = session.query(User).filter(User.name.like('%air')). order_by(User.id)
query.all()
# [<User('haruair', 'Edward Kim', '1234')>, <User('wendy','Wendy Williams', 'foobar')>]
first()
는 첫째를 리밋으로 설정해 scalar로 가져온다.
query.first()
# <User('haruair', 'Edward Kim', '1234')>
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
연결된 파라미터에서는 콜론을 이용한, 더 세세한 문자열 기반의 SQL를 사용할 수 있다. 값을 사용할 때 param()
메소드를 이용한다.
session.query(User).filter("id<:value and name=:name").\
params(value=1234, name='fred').order_by(User.id).one()
문자열 기반의 일반적인 쿼리를 사용하고 싶다면 from_statement()
를 쓴다. 대신 컬럼들은 매퍼에서 선언된 것과 동일하게 써야한다.
session.query(User).from_statement(
"SELECT * FROM users WHERE name=:name").\
params(name='haruair').all()
또한 from_statement()
아래와 같은 문자열 SQL 방식으로도 쓸 수 있다.
session.query("id", "name", "thenumber12").\
from_statement("SELECT id, name, 12 as "
"thenumber12 FROM users WHERE name=:name").\
params(name='haruair').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 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()
라는 숫자를 세는 편리한 메소드를 포함한다.
session.query(User).filter(User.name.like('haru%')).count()
count()
는 몇개의 행이 반환될지 알려준다. 위 코드로 생성되는 SQL을 살펴보면, SQLAlchemy는 항상 어떤 쿼리가 오더라도 거기서 행의 수를 센다. SELECT count(*) FROM table
하면 단순해지지만 최근 버전의 SQLAlchemy는 정확한 SQL로 명시적으로 판단할 수 있는 경우 추측해서 처리하지 않는다.
숫자를 세야 할 필요가 있는 경우에는 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) 만들기
이제 User
와 관계된, 두번째 테이블을 만들 것이다. 계정당 여러개 이메일 주소를 저장할 수 있게 만들 것이다. users 테이블과 연결되는, 일대다 테이블이므로 테이블 이름을 addresses라고 정하고 전에 작성했던 것처럼 Declarative로 address
클래스를 작성한다.
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
를 어떻게 만드는지 보여준다. Column
에 직접 넣은 지시자는 이 컬럼의 내용이 대상된 컬럼을 따르도록 만든다. 이 점이 관계 데이터베이스의 주요 특징 중 하나인데 풀과 같은 역할을 해, 연결되지 않은 테이블 사이를 잘 붙여준다. 위에서 작성한 ForeignKey
는 addresses.user_id
컬럼이 users.id
컬럼을 따르도록 만든다.
두번째 지시자인 relationship()
은 ORM에게 Address
클래스 자체가 User
클래스에 연결되어 있다는 사실을 Address.user
속성을 이용해 알 수 있게 해준다. relationship()
은 외래키 연결에서 두 테이블 사이에 Address.user
로 다대일 관계임을 결정한다.
덧붙여 relationship()
내에서 호출하는 backref()
는 역으로 클래스를 이용할 수 있도록, 즉 Address
객체에서 User
를 참조할 수 있도록 User.addresses
를 구현한다. 다대일 관계의 반대측은 항상 일대다의 관계이기 때문이다. 자세한건 기본 관계 패턴 문서를 참고.
Address.user
와 User.addresses
의 관계는 **양방향 관계(bidirectional relationship)**로 SQLAlchemy ORM의 주요 특징이다. Backref로 관계 연결하기 에서 backref
에 대한 자세한 정보를 확인할 수 있다.
relationship()
을 원격 클래스를 객체가 아닌 문자열로 연결하는 것에 대해 Declarative 시스템에서 사용하는 것으로 문제가 될 수 있지 않나 생각해볼 수 있다. 전부 맵핑이 완료된 경우, 이런 문자열은 파이썬 표현처럼 다뤄지며 실제 아규먼트를 처리하기 위해 사용된다. 위의 경우에선 User
클래스가 그렇다. 이런 이름들은 이것이 만들어지는 동안에만 허용되고 모든 클래스 이름은 기본적으로 선언될 때 사용이 가능해진다. (주. 클래스의 선언이 순차적으로 진행되기 때문에 클래스 선언 이전엔 에러가 나므로 이런 방식을 사용하는 것으로 보인다.)
아래는 동일하게 “addresses/user” 양방향 관계를 User 대신 Address로 선언한 모습이다.
class User(Base):
# ...
addresses = relationship("Address", order_by="Address.id", backref="user")
상세한 내용은 relationship()를 참고.
이건 알고 계시나요?
- 대부분의 관계형 데이터베이스에선 외래키 제약이 primary key 컬럼이나 Unique 컬럼에만 가능하다.
- 다중 컬럼 pirmary key에서의 외래키 제약은 스스로 다중 컬럼을 가지는데 이를
합성외래키(composite foreign key)
라고 한다. 이 또한 이 컬럼의 서브셋을 레퍼런스로 가질 수 있다. - 외래키 컬럼은 연결된 컬럼이나 행의 변화에 자동으로 그들 스스로를 업데이트 한다. 이걸 CASCADE referential action이라고 하는데 관계형 데이터베이스에 내장된 함수다.
- 외래키는 스스로의 테이블을 참고할 수 있다. 이걸 자기참조(self-referential) 외래키라고 한다.
- 외래키에 대해 더 알고 싶다면 위키피디아 외래키 항목을 참고.
addresses 테이블을 데이터베이스에 생성해야 하므로 metadata로부터 새로운 CREATE를 발행한다. 이미 생성된 테이블 은 생략하고 생성한다.
Base.metadata.create_all(engine)
관계된 객체 써먹기
이제 User
를 만들면 빈 addresses
콜렉션이 나타난다. 딕셔너리나 set같은 다양한 컬랙션이 있는데 기본으로 컬랙션은 파이썬의 리스트다. (컬렉션 접근을 커스터마이징 하려면 이 문서 참고)
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')>
하지만 addressses
컬랙션을 호출하는 순간 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에서든 짱짱 중요한 기능이다.