앞서 작성한 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')]

숫자세기

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

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에 직접 넣은 지시자는 이 컬럼의 내용이 대상된 컬럼을 따르도록 만든다. 이 점이 관계 데이터베이스의 주요 특징 중 하나인데 풀과 같은 역할을 해, 연결되지 않은 테이블 사이를 잘 붙여준다. 위에서 작성한 ForeignKeyaddresses.user_id 컬럼이 users.id 컬럼을 따르도록 만든다.

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

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

Address.userUser.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 하는 방법과 여러가지 좋은 설명이 위키피디아에 있으니 참고.

간단하게 UserAddress 두 테이블을 완전 조인하는 방법은 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()UserAddress 사이에 있는 하나의 외래키를 기준으로 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

서브쿼리 사용하기

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()를 쓰는 것과 비슷한데 자세한건 SQL 표현 언어 튜토리얼 문서를 참고.

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
# jack

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

for name, in ssession.query(User.name).\
        filter(User.addresses.any()):
    print name
# jack

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

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

has()any()와 동일한 기능을 하는데 대신 다대일 관계에서 사용한다. (~연산자는 NOT이란 뜻이다.)

session.query(Address).\
    filter(~Address.user.has(User.name=='jack')).all()
# []

일반 관계 연산자

관계(relationship)에서 사용할 수 있는 모든 연산자인데 각각 API 문서에서 더 자세한 내용을 볼 수 있다.

__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')

선행 로딩 (Eager Loading)

lazy loading의 반대 개념으로 User.addressesUser 호출할 때 바로 불러오도록 하는 방법이다. eager loading으로 바로 불러오면 쿼리 호출의 수를 줄일 수 있다. SQLAlchemy는 자동화와 사용자정의 기준을 포함해 3가지 타입의 선행 로딩(eager loading)를 제공한다. 3가지 모두 query options로 제어하는데 Query에 불러올 때 Query.options() 메소드를 통해 쓸 수 있다.

서브쿼리 로딩

선행로딩하도록 User.addresses에 표기하는 방법이다. orm.subqueryload()를 이용해서 서브쿼리를 불러올 떄 한번에 연계해 불러오도록 처리한다. 기존의 서브쿼리는 재사용이 가능한 형태지만 이것는 바로 Query를 거쳐 선택되기 때문에 관계된 테이블을 선택하는 것과 상관없이 서브쿼리가 동작한다. 복잡해보이지만 아주 쉽게 쓸 수 있다.

from sqlalchemy.orm import subqueryload
jack = session.query(User).\
                options(subqueryload(User.addresses)).\
                filter_by(name='jack').one()
jack
# <User('jack', 'Jack Bean', 'sadfjklas')>
jack.addresses
# [<Address('jack@gmail.com')>, <Address('jack@yahoo.com')>]

연결된 로딩 (Joined Load)

또 다른 자동 선행로딩 함수로 orm.joinedload()가 있다. join할 때 사용할 수 있는 방법으로 관계된 객체나 컬렉션을 불러올 때 한번에 불러올 수 있다. (LEFT OUTER JOIN이 기본값) 앞서의 addresses를 동일한 방법으로 불러올 수 있다.

from sqlalchemy.orm import joinedload

jack = session.query(User).\
                options(joinedload(User.addresses)).\
                filter_by(name='jack').one()
jack
# <User('jack', 'Jack Bean', 'sadfjklas')>
jack.addresses
# [<Address('jack@gmail.com')>, <Address('jack@yahoo.com')>]

사실 OUTER JOIN 결과라면 두 행이 나타나야 하는데 여전히 User 하나만 얻을 수 있다. 이 이유는 Query는 엔티티를 반환할 때 객체 유일성을 위해 “유일하게 하기(uniquing)” 전략을 취한다.

joinedload()는 오랜동안 써왔지만 subqueryload() 메소드가 더 새로운 형태의 선행로딩 형태다. 둘 다 한 행을 기준으로 관계된 객체를 가져오는 것은 동일하지만 subqueryload()는 적합한 관계 컬렉션을 가져오기에 적합하고 반면 joinedload()가 다대일 관계에 적합하다.

joinedload()join()의 대체재가 아니다.

joinedload()으로 join을 생성하면 익명으로 aliased되어 쿼리 결과에 영향을 미치지 않는다. Query.order_by()Query.filter() 호출로 이런 aliased된 테이블을 참조할 수 없기 때문에 사용자 공간에서는 Query.join()을 사용해야 한다. joinedload()은 단지 관계된 객체 또는 콜랙션의 최적화된 내역을 불러오기 위해 사용하는 용도이기 때문에 추가하거나 제거해도 실제 결과엔 영향을 미치지 않는다. 더 궁금하면 선행 로딩의 도를 참고.

명시적 Join + 선행로딩

세번째 스타일의 선행 로딩은 명시적 Join이 primary 행에 위치했을 때 추가적인 테이블에 관계된 객체나 컬렉션을 불러온다. 이 기능은 orm.contains_eager()를 통해 제공되는데 다대일 객체를 미리 불러와 동일 객체에 필터링 할 경우에 유용하게 사용된다. 아래는 Address행에 연관된 User 객체를 가져오는 코드인데 “jack”이란 이름의 Userorm.contains_eager()를 사용해 user 컬럼을 Address.user 속성으로 선행로딩한다.

from sqlalchemy.orm import contains_eager
jack_addresses = session.query(Address).\
                            join(Address.user).\
                            filter(User.name=='jack').\
                            options(contains_eager(Address.user)).\
                            all()
jack_addresses
# [<Address('jack@gmail.com')>, <Address('jack@yahoo.com')>]
jack_addresses[0].user
# <User('jack', 'Jack Bean', 'sadfjklas')>

기본적으로 어떻게 불러오는지 설정하는 다양한 방법 등 선행 로딩의 추가적인 정보는 관계 불러오기 테크닉 문서를 참고.

삭제하기

jack을 삭제해보자. 삭제하고나면 count는 남은 행이 없다고 표시한다.

session.delete(jack)
session.query(User).filter_by(name='jack').count()
# 0

여기까진 좋다. Address 객체는 어떤지 보자.

session.query(Address).filter(
    Address.email_address.in_(['jack@gmail.com','jack@yahoo.com'])
).count()
# 2

여전히 남아있다. SQL을 확인해보면 해당 Address의 user_id 컬럼은 모두 NULL로 되어 있지만 삭제되진 않았다. SQLAlchemy는 제거를 종속적으로(cascade) 하지 않는데 필요로 한다면 그렇게 할 수 있다.

삭제/삭제-외톨이 종속처리 설정하기

cascade 옵션을 변경하기 위해서는 User.addresses의 관계에서 행동을 변경시켜야 한다. SQLAlchemy는 새 속성을 추가하는 것과 관계를 맵핑하는 것은 언제나 허용되지만 이 경우에는 존재하는 관계를 제거하는게 필요하므로 맵핑을 완전히 새로 시작해야한다. 먼저 Session을 닫는다.

session.close()

그리고 새 declarative_base()를 사용한다.

Base = declarative_base()

다음으로 User 클래스를 선언하고 addresses 관계를 종속처리 설정과 함께 추가한다. (생성자는 대충 두자)

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    password = Column(String)

    addresses = relationship("Address", backref='user', cascade="all, delete, delete-orphan")

    def __repr__(self):
        return "<User('%s','%s','%s'>" % (self.name, self.fullname, self.password)

그리고 Address도 다시 생성한다. 이 경우에는 이미 User에서 관계를 생성했기 때문에 Address.user는 따로 생성할 필요가 없다.

class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    email_address = Column(String, nullable=False)
    user_id = Column(Integer, ForeignKey('users.id'))

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

이제 Jack을 불러오고 삭제하면 Jack의 addresses 컬랙션은 Address에서 삭제된다.

# jack을 primary key로 불러옴
jack = session.query(User).get(5)
# 첫 Address를 삭제 (지연 로딩이 동작한다)
del jack.addresses[1]
# address는 하나만 남는다
session.query(Address).filter(
    Address.email_address.in_(['jack@gmail','jack@yahoo.com'])
).count()
# 1

Jack을 지우면 Jack과 남은 Address도 삭제된다.

session.delete(jack)
session.query(User).filter_by(name='jack').count()
# 0
session.query(Address).filter(
    Address.email_address.in_(['jack@gmail.com','jack@yahoo.com'])
).count()
# 0

종속처리(cascade)에 대해

종속처리에 대한 더 자세한 설정은 Cascades 문서를 참고. 종속처리는 함수적으로 관련된 데이터베이스가 자연스럽게 ON DELETE CASCADE될 수 있도록 통합할 수 있다. Using Passive Deletes 문서 참고

다대다 관계(Many To Many Relationship) 만들기

일종의 보너스 라운드로 다대다 관계를 만드는 방법을 살펴본다. 블로그와 같은걸 만들 때를 예로 들면 BlogPost와 그에 따른 Keyword를 조합해야 하는 경우가 있다.

평범한 다대다 관계를 위해, 맵핑되지 않은 Table 구조를 조합 테이블로 만들 수 있다.

from sqlalchemy import Table, Text
# 조합 테이블
post_keywords = Table('post_keywords', Base.metadata,
    Column('post_id', Integer, ForeignKey('posts.id')),
    Column('keyword_id', Integer, ForeignKey('keywords.id'))
)

위 코드는 맵핑된 클래스를 선언하는 것과는 약간 다르게 Table를 직접 선언했다. Table은 생성자 함수로 각각 개별의 Column 아규먼트를 쉼표(comma)로 구분한다. Column 객체는 클래스의 속성명을 가져오는 것과 달리 이름을 명시적으로 작성해준다.

다음은 BlogPostKeywordrelationship()으로 post_keywords 테이블에 연결해 정의한다.

class BlogPost(Base):
    __tablename__ = 'posts'

    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    headline = Column(String(255), nullable=False)
    body = Column(Text)
    # 다대다 관계 : BlogPost <-> Keyword
    keywords = relationship('Keyword', secondary=post_keywords, backref='posts')

    def __init__(self, headline, body, author):
        self.author = author
        self.headline = headline
        self.body = body

    def __repr__(self):
        return "<BlogPost('%r', '%r', '%r')>" % (self.headline, self.body, self.author)

class Keyword(Base):
    __tablename__ = 'keywords'

    id = Column(Integer, primary_key=True)
    keyword = Column(String(50), nullable=False, unique=True)

    def __init__(self,keyword):
        self.keyword = keyword

위에서 BlogPost.keywords는 다대다 관계다. 다대다 관계를 정의하는 기능은 secondary 키워드로 연관 테이블인 Table객체를 참조한다. 이 테이블은 단순히 양측의 관계를 참고하는 형태며 만약 다른 컬럼이 있다면, 예를 들어 자체 primary key가 있거나 foreign key를 가진다면 연관 객체(association object) 라는 다른 형태의 사용패턴을 사용해야 한다. 연관 객체 문서 참고.

그리고 BlogPost 클래스는 author필드를 가진다. 그래서 다른 양방향 관계를 만들 것인데 단일 사용자가 엄청나게 많은 블로그 포스트를 가질 수 있다는 문제점을 처리해야한다. 다시 말해 User.posts에 접근하면 모든 포스트를 불러올 것이 아니라 일부 필터된 결과만 가져와야 한다. 이런 경우를 위해 relationship()lazy='dynamic'을 지원하는데 속성을 불러오는 전략의 대안 중 하나다. 이것을 relationship()의 역방향으로 사용하려면 backref()를 사용하면 된다.

from sqlalchemy.orm import backref
# User에서의 관계를 "다이나믹" 로딩 처리
BlogPost.author = relationship(User, backref=backref('posts', lazy='dynamic'))

그리고 새 테이블을 생성한다.

Base.meta.create_all(engine)

사용 방법은 크게 다르지 않다.

wendy = session.query(User).\
                filter_by(name='wendy').\
                one()
post = BlogPost("Wendy's Blog Post", "This is a test", wendy)
session.add(post)

지금 키워드는 데이터베이스에 각각 유일하게 저장한다. 아직 뭔가 거창한걸 한건 아니고 그냥 생성할 뿐이다.

post.keywords.append(Keyword('wendy'))
post.keywords.append(Keyword('firstpost')) 

이제 키워드가 ‘firstpost’인 모든 글을 찾아볼 것이다. 여기서 any 연산자로 ‘firstpost’인 글을 찾는다.

session.query(BlogPost).\
        filter(BlogPost.keywords.any(keyword='firstpost')).\
        all()
# [BlogPost("Wendy's Blog Post", 'This is a test', <User('wendy','Wendy Williams', 'foobar')>)]

만약 Wendy의 포스트만 보고 싶다면,

session.query(BlogPost).\
        filter(BlogPost.author=wendy).\
        filter(BlogPost.keywords.any(keyword='firstpost')).\
        all()
# [BlogPost("Wendy's Blog Post", 'This is a test', <User('wendy','Wendy Williams', 'foobar')>)]

또는 Wendy가 소유하고 있는 posts 관계 즉 dyanmic 관계를 이용해 불러오는 방법은 아래와 같다.

wendy.posts.\
    filter(BlogPost.keywords.any(keyword='firstpost')).\
    all()
# [BlogPost("Wendy's Blog Post", 'This is a test', <User('wendy','Wendy Williams', 'foobar')>)]

이후 읽어볼 만한 문서

(주. 아마 아래 문서 중 세션과 관계 문서를 먼저 옮길 것 같습니다.)

오라일리 책을 구입해두고 안보고 있다가 이제야 보니 다른 부분이 너무나도 많아서 문서 보면서 배우기로 급 선회했다. 한글 문서로 먼저 훑어보면 좋을텐데 검색 능력이 부족해서 찾질 못하겠더라. 문서 보면서 대충 날림 번역으로 남겨놨다. 하루면 페이지 다 따라해볼 수 있을 것 같았는데 딴짓하느라 하루에 다 완료를 못해서 파트를 쪼개기로. 주중에 짬짬이 나머지를 보기로 하고 일단 먼저 업로드!

(여기서 뭔가 모자란 부분이나 틀린게 있으면 틀린게 맞으므로 언제든 지적해주시고, 애매한 표현은 원본 문서를 봐주시면 감사하겠습니다. 원본 문서는 SQLAlchemy Tutorial. 한글로 된 sqlalchemy 튜토리얼 있으면 알려주세요!)


SQLAlchemy 객체 관계형 매퍼는 데이터베이스 테이블을 이용해 사용자가 정의한 파이썬 클래스의 메소드와 각각의 행을 나타내는 인스턴스로 표현된다. 객체와 각 연관된 행들의 모든 변경점들이 자동으로 동기되어 인스턴스에 반영되며, 그와 동시에 사용자가 정의한 클래스와 각 클래스 사이에 정의된 관계에 대해 쿼리할 수 있는 (Unit of work이라 하는)시스템을 포함하고 있다.

이 ORM에서 사용하는 SQLAlchemy 표현 언어는 ORM의 구성 방식과도 같다. SQL언어 튜토리얼에서는 직접적인 의견을 배제한 채 데이터베이스들의 초기에 어떻게 구성해 나가야 하는지에 대해 설명하는 반면 ORM은 고수준의, 추상적인 패턴의 사용 방식과 그에 따른 표현 언어를 사용하는 방법을 예로 보여준다.

사용 패턴과 각 표현 언어가 겹쳐지는 동안, 초기와 달리 공통적으로 나타나는 사항에 대해 표면적으로 접근한다. 먼저 사용자가 정의한 도메인 모델서부터 기본적인 저장 모델을 새로 갱신하는 것까지의 모든 과정을 일련의 구조와 데이터로 접근하게 해야한다. 또 다른 접근 방식으로는 문자로 된 스키마와 SQL 표현식이 나타내는 투시도로부터 명쾌하게 구성해, 각 개별적인 데이터베이스를 메시지로 사용할 수 있게 해야 한다.

가장 성공적인 어플리케이션은 각각 독자적인 객체 관계형 매퍼로 구성되야 한다. 특별한 상황에서는, 어플리케이션은 더 특정한 데이터베이스의 상호작용을 필요로 하고 따라서 더 직접적인 표현 언어를 사용할 수 있어야 한다.

(제 실력이 미천해 깔끔하게 번역이 안되네요. 공통된 부분에만 집중하고 각 데이터베이스의 특징을 몰개성화 하며 단순히 저장공간으로 치부하는 다른 ORM과 달리 SQLAlchemy는 각 데이터베이스의 특징도 잘 살려내 만든 ORM이다, 대충 이런 내용입니다. 원문 보세요. ㅠㅠ)


버전 확인하기

import sqlalchemy
print sqlalchemy.__version__

접속하기

이 예시는 메모리서만 사용하는 sqlite 데이터베이스를 사용. create_engine()을 이용해 접속.

from sqlalchemy import create_engine
engine = create_engine('sqlite:///:memory:', echo=True)

echo는 로그를 위한 플래그. 파이썬 표준 logging 모듈 사용. 순수 SQL 코드를 보여준다.

engine은 선언만 해서 바로 연결되는게 아니라 첫 실행이 될 때 연결이 됨.

print engine.execute("select 1").scalar()

ORM을 사용할 때는 위처럼 engine을 직접 이용할 필요는 없다. 맨 처음 연결 할 때 작성하고 ORM 사용하면 됨.

매핑 선언

ORM에서는 처음에 데이터베이스 테이블을 써먹을 수 있게 설정한 다음 직접 정의한 클래스에 맵핑을 해야한다. sqlalchemy에서는 두가지가 동시에 이뤄지는데 Declarative 란걸 이용해 클래스를 생성하고 실제 디비 테이블에 연결을 한다.

from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

이러면 준비 끝. 이렇게 해두면 몇개고 매핑 클래스를 만들 수 있다. 매핑 클래스 내에서 디비의 컬럼을 나타내는 Column 클래스, 각 컬럼의 데이터타입을 나타내는 Integer, String 클래스를 불러와야한다.

from sqlalchemy import Column, Integer, String

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    password = Column(String)

    def __init__(self, name, fullname, password):
        self.name = name
        self.fullname = fullname
        self.password = password

    def __repr__(self):
        return "<User('%s', '%s', '%s')>" % (self.name, self.fullname, self.password)

User 클래스는 __tablename__에서 정의한 테이블에 맵핑되고 primary key인 id와 name, fullname, password 컬럼을 가진다.

메소드는 마음껏 만들어도 상관없다. 파이썬 기본 class와 똑같음. __init____repr__도 만들어도 되고 안만들어도 된다. Base를 상속하지만 이는 단지 최소의 설정만 담당할 뿐이다.

Declarative system으로 만들어진 이 클래스는 table metadata를 가지게 되는데 이게 사용자정의 클래스와 테이블을 연결해주는 구실을 한다. 예전엔 이 metadata를 만들고 클래스에 맵핑해서 썼는데 그 방식을 Classical Mapping이라고 얘기한다. 그 예전 방식에서는 Table이라는 데이터 구조와 Mapper 객체로 클래스와 맵핑한다. (오라일리에서 나온 sqlalchemy 책에선 이 구방식으로 설명한다 ;ㅅ;)

metadata를 보고 싶다면,

User.__table__

mapper 클래스는,

User.__mapper__

Declarative 기반 클래스는 모든 Table 객체들을 MetaData로 정의해두고 .metadata 속성을 통해 접근할 수 있게 도와준다.

아직 위의 예제 클래스는 테이블이 생성이 되지 않은 상태인데 MetaData를 통해 손쉽게 생성할 수 있도록 도와준다. 테이블을 생성할 때 MetaData.create_all() 로 생성할 수 있는데 이 메소드를 호출하면 Engine으로 연결된 데이터베이스에 테이블을 생성해준다.

Base.metadata.create_all(engine)

최소 테이블 묘사 vs. 완전 상세돋는 묘사

sqlite나 postgresql은 테이블을 생성할 때 varchar 컬럼을 길이를 설정하지 않아도 별 문제 없이 데이터타입으로 쓸 수 있지만 그 외 데이터베이스에서는 허용되지 않는다. 그러므로 컬럼 길이가 필요한 데이터베이스의 경우 length가 필요하다.

Column(String(50))

Integer, Numeric 같은 경우에도 위와 동일하게 쓸 수 있다.

덧붙여 Firebird나 오라클에서는 PK를 생성할 때 sequence가 필요한데 Sequence 생성자를 써야 한다.

from sqlalchemy import Sequence
Column(Integer, Sequence('user_id_seq'), primary_key=True)

위에서의 User 클래스를 다시 작성해보면,

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, Sequence('user_id_seq'), primary_key=True)
    name = Column(String(50))
    fullname = Column(String(50))
    password = Column(String(12))

    def __init__(self, name, fullname, password):
        self.name = name
        self.fullname = fullname
        self.password = password

    def __repr__(self):
        return "<User('%s', '%s', '%s')>" % (self.name, self.fullname, self.password)

파이썬 안에서만 쓸꺼라면, 그리고 디비를 밖에서 이미 생성했다면 이전에 작성한 대로만 작성해도 상관 없다.

매핑된 클래스로 인스턴스 만들기

ed_user = User('haruair', 'Edward Kim', '1234')
ed_user.name        # 'haruair'
ed_user.password    # '1234'
str(ed_user.id)     # 'None'

id는 __init__()에서 정의되지 않았지만 맵핑을 해뒀기 때문에 None으로 존재한다. 기본적으로 ORM에서 생성된 클래스 속성들은 테이블에 맵핑된 것으로 표현된다. 이런 클래스 속성들은 descriptors로서 존재하는데 맵핑된 클래스를 위해 instrumentation을 정의해둔다. 이 instrumentaion은 이벤트를 바꾸거나 변경을 추적하거나 자동으로 새로운 데이터를 불러온다거나 할 때 도움을 주는 기능을 한다.

위의 값에서 ‘Edward Kim’을 디비에 넣기 전까진 id는 None이다. 디비에 넣으면 id값은 알아서 들어오게 된다.

세션 만들기

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

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

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

Session = sessionmaker()

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

Session.configure(bind=engine)

위처럼 작성한 Session 클래스는 새 객체를 만들어서 데이터베이스와 연결이 된다. 다른 트랜잭션을 위한 것들은 sessionmaker()에서 호출될 때 정의되야 하는데 자세한건 이후 챕터에서 알려준다고. 이제부터 언제든 데이터베이스와의 대화가 필요할 때 Session을 불러서 쓰면 된다.

session = Session()

Session은 Engine과 연결이 되어 있지만 아직 연결이 열린 상태는 아니다. 앞서와 같이 처음으로 사용될 때 Engine과 연결되고 모든 변경을 커밋하고 세션을 종료할 때까지 열려있게 된다.

세션 생성하는 패턴들

Session은 다양한 기반에 다양한 타입의 어플리케이션, 프래임워크에서 다양한 요구사항에서 짱짱 좋다. 그러니까 Session은 오브젝트와 일반적인 데이터베이스 접속에서 쓰면 된다. 어플리케이션 스레드를 저녁 만찬이라 생각하면, 세션은 손님의 접시이고 객체는 놓여질 음식이라 볼 수 있다. (디비는 주방쯤?) 세션을 어떻게 써야할지 고민한다면 다음 링크 참조.

새 객체 추가하기

ed_user= User('haruair', 'Edward Kim', '1234')
session.add(ed_user)

여기선 실제로 데이터베이스에 추가된게 아니라 pending인 상태다. 아직 데이터베이스에 발행되지는 않은 상태인데 입력이 필요한 순간에는 flush라는 과정을 통해 입력이 된다. 만약 디비에 쿼리를 하면 모든 pending 된 정보는 flush되고 접근 가능한 상태가 된다. (실제로 저장된 상태는 아님. 여전히 pending.)

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

our_user = session.query(User).filter_by(name='haruair').first()
our_user     # <User('haruair', 'Edward Kim', 'secret')>

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

ed_user is our_user     # True

ORM의 컨셉이 identity map이라서 session에서 하는 모든 처리들이 실제 데이터셋과 함께 동작한다. Session에서 PK를 가지면 PK 가진 같은 파이썬 객체를 반환한다. 그러니까 이미 있는 PK를 입력하면 에러가 난다.

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

session.add_all([
    User('wendy', 'Wendy Williams', 'foobar'),
    User('mary', 'Mary Contrary', 'xxg527'),
    User('fred', 'Fred Flinstone', 'blar')])

비밀번호를 함 바꿔보자.

ed_user.password = 'test1234'

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

session.dirty        # IdentitySet([<User('Edward', 'Edward Kim', 'test1234')>])

새로 추가한 애들도 볼 수 있다.

session.new
# IdentitySet([<User('mary', 'Mary Contrary', 'xxg527')>,
#              <User('wendy', 'Wendy Williams', 'foobar')>,
#              <User('fred', 'Fred Flinstone', 'blar')>])

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

session.commit()

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

암튼, 앞서 id가 ‘None’ 이었던 녀석을 다시 보면,

ed_user.id    # 1

Session이 새 행을 데이터베이스에 입력한 이후에 새로 생성된 행들은 식별자들과 데이터베이스에서 기본으로 설정된 값들을 instance에서 사용할 수 있게 된다. 즉시 사용할 수 있거나 첫 액세스에 로딩될 때 모두 사용할 수 있다. 위 경우엔 commit()을 실행한 이후 새 트랜잭션이 실행되어 모든 행이 다시 로드된 상태다.

sqlalchemy에서는 기본적으로 이전 트랜잭션에서 새 트랜잭션으로 처음 실행될 때 모든 데이터를 새로 가져온다. 그래서 가장 최근의 상태를 바로 사용할 수 있다. 다시 불러오는 레벨을 설정하고 싶으면 세션 사용하기 문서를 확인하자.

세션 객체의 상태들

User 객체가 Session 외부에서 PK 없이 Session 안에 들어가고 실제로 데이터베이스에 추가될 때 까지 각 “객체 상태” 를 가지고 있다. transient, pending, persistent 세가지. 이 상태들을 알고 있으면 도움이 되므로 객체 상태에 대한 설명을 잽싸게 읽어보자.

롤백하기

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

ed_user.name = 'edkim'

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

fake_user = User('fakeuser', 'Invalid', '12345')
session.add(fake_user)

Session을 query하면 일단 flush된 현재의 트랜잭션을 확인할 수 있다.

session.query(User).filter(User.name.in_(['edkim', 'fakeuser'])).all()
#[<User('edkim', 'Edward Kim', 'test1234')>, <User('fakeuser', 'Invalid', '12345')>]

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

session.rollback()
ed_user.name            # 'haruair'
fake_user in session    # False

쿼리 보내기

Query 객체는 session에서 query() 메소드로 생성한다. 이 함수는 다양한 수의 아규먼트를 가질 수 있는데 다양한 클래스의 조합과 클래스 descriptor를 사용할 수 있다. 사실 QueryUser 인스턴스를 부를 때 이미 써봤다. iterative context를 evaluated할 때, User 객체 리스트를 반환한다.

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

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

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

label()을 이용하면 컬럼 이름을 다르게 쓸 수 있다. 어떤 클래스 속성이든 매핑해서 쓸 수 있다. ColumnElement-derived object.

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

컬럼은 위 방식으로 하지만 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

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

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

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

for name in session.query(User.name).filter_by(fullname='Edward Kim'):
    print name

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

for name in session.query(User.name).filter(User.fullname=='Edward Kim'):
    print name

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

for name in session.query(User).\
            filter(User.name=='haruair').\
            filter(User.fullname=='Edward Kim'):
    print user

일반 필터(filter) 연산자들

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'))

SQLAlchemy 시작하기 – Part 2에서 계속.

Coursera 강의는 예전에도 몇강의 도전했었는데 환상적인 인터넷 환경에 있다보니 몇번 듣다 결국 포기했었다. (게으른 것도 절반 이상이긴 하지만.) Startup Engineering은 HN에서도 그렇고 추천글을 몇번이고 보다보니 들어봐야겠단 생각에 등록을 했다.

https://class.coursera.org/startup-001

강의 분위기는 좀 지루하게 읽는 편이긴 하지만 내용은 상당히 흥미롭다. 첫 과제는 nodejs로 파일 저장, 소수 100개 찾기였는데 첫강부터 aws, heroku를 세팅하는걸 보면 앞으로의 과정에 기대를 안할 수가 없다. 수업 마지막까지 부지런히 들어야지!

Published on June 24, 2013

When I installed W3 Total Cache, It was always problem with Comprehensive Google Map Plugin. Although W3 Total Cache is awesome plugin, I couldn’t use it for this problem. I spent time today and I found what problem is.

It was problem with JSON parsing. In W3 Total cache, every page minify by replacing space to new line(\n). So we can fix a code like below:

/plugins/comprehensive-google-map-plugin/assets/js/cgmp.framework.js

// CGMPGlobal.errors = parseJson(CGMPGlobal.errors);<br /> CGMPGlobal.errors = parseJson(CGMPGlobal.errors.replace(/\n/gi,""));

And then, wrapping jQuery. It need to be run after jQuery loaded.

jQuery(function(){ /* all code */ });

/plugins/comprehensive-google-map-plugin/assets/js/cgmp.framework.min.js

// find w(i) and then change to the code<br /> w(i.replace(/\n/gi,""))

Also wrapping jQuery in same way.

jQuery(function(){ /* all code */ });

I hope you get answer what you want.

오늘은 하루종일 비가 내렸다. 겨울이 가까워질수록 비가 많이 내리는 멜번의 기후는 일년 사이에 적응하기란 절대 불가능해 보인다. 눈도 없으면서 매서운 찬바람이 가득한 겨울이 돌아오고 있다.

회사에 다닌지 벌써 1년이 되었다. 한국에서 경험해보기 힘든 다양한 환경에서 실로 다양한 웹사이트, 웹어플리케이션 개발에 참여할 수 있었다. 이전에 다니던 회사는 기술력이 기반이 아니었던 곳이라 창발적인 아이디어를 빠르게 구현하는 것에 집중했던 반면 지금의 회사는 안정적이고 신뢰할 수 있는 시스템을 만드는데 촛점을 두고 있어 이전과 비슷한 업무조차도 다른 방향으로 접근해 해결해가는 모습이 큰 공부가 되고 있다.

영어공부는 반성을 좀 해야한다. 출퇴근 하며 아티클 읽는게 전부. 이번 겨울에는 빈둥거리지 말고 작년처럼 집중해서 영어공부를 할 수 있도록 해야겠다.

파이썬은 틈틈히 보고있다. 한동안 영어공부 겸 파이썬을 공부한다는 핑계로 PEPs 번역을 했는데 일이 잠깐 바빠진 틈에 손을 놓고 있다. 이건 분명 게으름인데. Flask를 살펴보다가 요즘은 tornado를 보고있다. 파이썬은 이미 멋진 패키지들이 많아서 참 좋다.

지난 한 달 사이에 VBScript랑 씨름을 했는데 그 덕분에 베이직 문법이랑 친해졌다. 닷넷을 베이직으로 해볼까 하는 생각이 들 정도.

시간이 빠르게 흐른다고 느껴질수록 잘 지내고 있다기보단 내 삶 어딘가에 time-leak이 있는건 아닌가 생각이 든다. 비도 오고 든든하게 저녁 챙겨먹어야지.

얼마 전 생활코딩에서 즉시실행 익명함수라는 표현을 보고 깜짝 놀랐다. 근래에 JavaScript로 몇번 개발을 해봤다면 예제를 보면 이게 무슨 의미인지 바로 이해할 수 있겠지만 "이건 아무래도 과하지 않은가" 라고 생각했다. 이렇게까지 거창한(?) 이름이 붙는 내용이었나 하고.

개발을 배우다보면 첫눈에 이해하기 힘든 한자 조어들이 꽤 있다. 함수, 변수와 같은 용어도 프로그래밍 강의 첫시간에 나와서 쉽게 쓰는거지 그 자체로 상당히 어려운 용어다. 객체 생성자, 논리 연산자 정도는 어떤 내용을 정의한 단어인지 쉽게 이해(내지는 유추) 할 수 있지만 대리자, 비동기와 같은 단어는 쉽게 내용을 생각해내기 어렵다. 용어의 정의는 모두가 동의할 수 있고 그 정의한 용어가 해당 용례에 대한 대표성을 띌 수 있어야 한다. 용어 함의적이라면 문제가 생길 수 밖에 없다.

물론 개발에만 국한된 것이 아니라 대다수의 학문에서 사용되는 용어가 과도하게 한자로 조어되는 경향이 있다. 한자문화권이기에 이해도 되고, 서양-일본-한국의 경로로 학문이 들어오는 경우도 적잖은 영향이 있을듯 하다. 배경은 뒤로 밀어두고서 단순히 직역해 한자로 옮겨 조어를 만드는게 올바른가에 대해 좀 생각해볼 필요가 있다.

영어권 환경에서 일을 하다보니 가장 크게 느끼는, 한국과 다른 점은 용어에 대한 설명이 거의 필요 없을 정도로 쉽게 이해한다는 점이다. 디자이너도, 세일즈도, CS담당자도 모두 개발용어를 거침없이 쉽게 사용한다. 영어권이니까. 그 개발용어 자체로도 별다른 설명없이 알 수 있다는 것이 얼마나 큰 장점인지 모른다. 그런 언어적 이점에 명확한 설명을 선호하는 문화적 배경이 영어권 IT기업이 가지는 장점이라고 본다. (이와 관련해서는 적자면 내용이 더 많을 것 같아 여기까지)

단어만으로 더 쉽게 이해할 수 있는 개발언어 용어를 만들 순 없을까. 모두가 쉽게 이해할 수 있는 용어는 위에서 말한 것처럼 충분히 매력적이다. 한자어로 단어를 만들더라도 단순 사전식 직역 말고 보다 정서에 맞게 옮길 순 없을까. 앞서 언급한 즉시실행 익명함수를 예로 해보면, 함수란 원래 호출하면 바로 실행되는건데 즉시실행이란 표현도 좀 재미있는듯 하다. 익명함수는 어딘가에 선언 없이 바로 사용하는거니까, 임시함수 실행, 호출 정도가 되지 않을까. (하루종일 고민한 단어가 이 수준이라 아쉽긴 하지만;)


추가. 작성하고 나니 이전에 SICP 번역서가 생각났는데, 쉽게 풀어쓴 용어가 기존에 조금이라도 알고 있던 사람들에게는 요즘말로 멘붕을 야기했다는 후기. 차라리 한자로 쓴, 이전의 용어체계가 훨씬 이해하기 쉽다는 얘기였다. 쉽게 이해할 수 있는 용어는 매력이 있지만, 그런 용어의 정의는 역시 말처럼 쉽게 되지 않는다.

오늘 두개의 프로젝트가 종료되었다. 하나는 1월에 마무리했지만 어른들의 사정으로 지연되어 지금까지 온 프로젝트였다. 다른 하나는 4월에 작업이 끝나 예정대로 “go live”로 진행되었다. 완료되지 않고 오랜 기간 끌어온 프로젝트가 정리되니 마음이 홀가분하다. 다른 나머지 프로젝트도 거의 완료단계에 다가왔기 때문에 이번달은 아마 지난 몇 달의 고생과는 달리 쉽게 지나가지 않을까 생각이 든다.

웹 에이전시의 특성상 여러가지 프로젝트가 동시에 진행되는 경우가 많다. 적절하게 업무 순서가 배분되어 진행된다면 다행이지만 프로젝트끼리 진행되는 순서가 비슷하게 될 때 문제가 된다. 아이디어도 잘 안떠오르고 진행도 잘 안되는 교착상태에 빠지게 되는데 이 상태를 어떻게 잘 해결하는가가 가장 큰 과제다. 이러한 문제를 “번 아웃”이라 얘기하며 더 자세한 이야기는 여기에서 볼 수 있다.

내 경우에는 문제에 직면했을 때 쉽게 문제에 휩쓸리는 몹쓸 멘탈(…)을 가지고 있는 터라 잦은 피드백이나 사소한 실수에도 쉽게 문제에 전도되고 만다. 문제를 쉽고 빠르게 해결하기 위해서는 가장 먼저 평정심을 찾는게 중요하다. 문제에 지나치게 고민하고 휩쓸려 내 중심을 찾지 못한다면 객관적으로 상황을 판단하기 어렵고 결과적으로 오랜 지연을 만들기 마련이다.

평정심을 찾는 방법은 어떤게 있을까? 내 스스로도 아직 완벽한 해결책을 찾지 못했는데 좀 어처구니 없긴 하지만 업무에 집중 안하기가 하나의 방법이다. 적당히 업무에 거리감을 두기 위한 딴짓(대표적으로 커뮤니티나 SNS)을 한다면 나와 같은 문제가 있는 경우에 조금은 도움이 된다. 문제는 쉽게 주객전도가 된다는 것이고 역설적으로 집중력 문제를 야기한다. (그래서 집중력이 무지 안좋아지고 있습니다. 좋지 않은 방법이니 좀 더 건설적인 방법을 찾도록 합시다. 네.)

나는 문제는 본인에게 있다는 이야기를 싫어한다. 인기를 끌고 있는 수많은 힐링 서적들을 좋아하지 않는 이유도 사회적인 문제를 개인의 문제로 과하게 몰고가서 싫어하는 편인데, 이 평정심 문제에 한해서는 본인의 문제로 개선해야 하지 않는가 생각이 든다. 이전 회사에 있을 때는 아무래도 팀을 관리하던 입장이어서 그런지 시스템에서 이와 같은 문제를 발생하지 않도록 하는데 노력했다. (물론 뜻대로 잘 되진 않았지만.) 상황, 입장이 바뀌면 생각하는게 달라지는거구나 하고, 요즘 그런 생각을 좀 많이 하게 되었다.

주절주절 적다보니 무슨 이야기를 하고 있는지 잘 모르겠지만, 평정심을 찾는 것이 얼마나 중요한지, 그리고 어떻게 그 평정심을 유지하는지에 대해 고민해봐야겠다. 퇴근해야겠다 🙂

생활코딩의 강의 영상이 1,000건에 다달았다는 소식을 접했다. 밥 로스와 같이 담담하고 차분한 어조로 강의하는 내용들은 제목이 말하듯 누구나 생활에서 코딩을 할 수 있도록 쉽게 풀어 보여주고 있다. 이번에 생활코딩 페이스북 그룹을 통해 공유된, 일종의 회고 영상인 ㅋㅋㅋ전략이 참 인상적이라 포스트 하게 되었다. 해당 영상은 아래의 링크를 통해 볼 수 있다.

http://opentutorials.org/course/488/4108

이고잉님의 그간 수고와 노력, 그리고 통찰을 엿볼 수 있는 내용으로 단순히 이 ㅋㅋㅋ 전략은 생활코딩에만 적용되는 것이 아니라 모든 스타트업, 서비스 프로바이더에게 공통적으로 적용되는 전략이다. 세가지가 모두 중요하나 이 세가지가 한번에 얻어지는 것이 아니란 점도 생각해봐야 한다. 각 영역이 나선형으로 상호작용하며 성장한다는 사실을 눈여겨 볼 필요가 있다.

단 한번의 기획으로 끝까지 가는 것이 아니라 끊임없이 보완하고 그 보완 과정에서 과감하게 컨테이너를 개편하는 등의 결단, 그리고 징글징글하게(?) 꾸준하게 만들어지는 컨텐츠, 다양한 사연을 가진 사람들이 모여 구성된 커뮤니티. 단순하게 세가지로 집약되는 ㅋㅋㅋ전략은 가장 쉽고 가장 어려운 전략임에 틀림 없다.

2년이란 기간, 지금까지 달려온 생활코딩에 박수와 찬사를 보내며 앞으로 성장할 그 모습을 힘차게 응원한다.

미술사에 대한 관심은 어려서부터 많았다. 외삼촌이 미술에 관심이 많았었는데 대학에 들어가며 수많은 미술 서적들을 우리집에 두고 갔다. 올 컬러 인쇄의 호화 양장본이었고 오랜 기간이 지난 책인데도 약간 퀘퀘한 냄새가 날 뿐이지 작가의 그림을 보기에, 그리고 뒤에 붙어있던 작가의 설명을 보기에 전혀 문제가 없었다. 그걸 유치원 때 재미있다고 읽고 보고 했었다. 시간이 지나면서도 가끔씩 들여다보던 책이었다.

본격적으로 미술사에 대해 책을 찾아보고 공부하게 된 것은 재미있게도 고등학교 때 소설 _<냉정과 열정 사이>_를 보고나서 였다. 소설 중 아가타 쥰세이의 직업은 미술작품을 복원하는 복원사였는데 흥미가 생겨서 소설을 읽고 난 후에 다카시나 슈지의 <명화를 보는 눈> 등 여러 미술사 관련 책을 통해 좀더 체계적으로 배우려고 노력했었다.

스탕달 신드롬은 _명화와 같은 뛰어난 예술 작품을 보게 될 때 겪는 현상으로 심장이 두근거리거나, 의식이 혼미해지거나, 심하면 환상을 보는 등의 경험을 하는 것을 뜻_한다. 이전까지는 미술을 책으로만 접했던 나는 르누아르의 작품을 보다가 너무 놀랐는지 미술관 한복판에서 현기증에 주저 앉은 적이 있었다. 그게 진짜 스탕달 신드롬인지 그냥 다리 힘이 갑자기 풀린 것인지는 모르겠지만, 그런 경험 이후에는 좀 더 깊게 미술사를 살펴보게 된 건 어쩌면 당연한 일인지도 모르겠다. 근래에도 National Gallery of Victoria에서 인상파전을 하길래 다녀온 적이 있었는데 요번에 출근하며 가만 기억을 더듬다가 프로그래밍에도 스탕달 신드롬이 있지 않나 생각이 들었다.

이 작품 앞에서 주저 앉았다. 르누아르의 Moulin de la Galette

실무에 있다보면 새로운 것에 대한 자극이 점점 옅어져 권태에 접어들거나 좌절감에 빠져들기도 한단다. (난 경력도 얼마 안되는데도 나에게서 요즘 이런 모습이 보인다…) 그래도 개발일을 하면서 겪었던 경험들에 빗대어 생각하게 되었다. 한참을 고민하다 해결책을 찾았을 때라거나, 생각지도 못한 기발한 방법으로 작성한 코드를 보고 심장이 두근거리다 못해 현기증이 나고 마는 프로그래밍 스탕달 신드롬! 가끔 마주하는 그런 순간들이 프로그래밍의 깊이를 더하고 앞으로 더 나아가게 만드는 힘이 되지 않았던가 하고 말이다.

그래서 한참 생각하다 그 설렘이 가장 컸던 순간이 언제였을까? 되감아 생각해보니 무엇보다 가장 강렬했던 그 순간은 이런 저런 복잡한 일이 아닌 Hello World를 화면에 가장 먼저 띄웠을 때였다.

Hello World를 처음 띄운 순간에 다들 그렇지 않나요? 나만 그랬나…

처음 Hello World에서 보았던 미래를 다시 가슴에 품고, 프로그래밍 스탕달 신드롬을 기대하며 부지런히 해야하지 않을까. 얼렁뚱땅 결론을 내는 기분이 나지만 ^^; 다시 그 스탕달 신드롬을 마주하는 순간을 기대하며 부지런히 나아가야겠다.

제목은 거창하지만 그냥 생각나는대로 적어본 글이다.

지난주 트위터를 달궜던 이슈는 역시 구글리더가 아닌가 싶다. 봄청소라는 명목으로 매년 자사의 서비스를 검토하고 정리하는 구글이 이번엔 구글 리더를 정리한다는 글을 올렸다. 덕분에 대체재로 부상된 newsblur나 feedly 등의 서비스가 트래픽 폭탄을 맞이하는 웃는게 웃는게 아니야 같은 상황도 벌어졌다. Feedly는 이 날을 준비했다며 대거 서비스 이주 프로젝트로 노르망디에 대해 알리기도 했다. (노르망디라니 작명 센스가 대단하다.)

Quora에 구글 리더 PM이 내부적인 상황을 공개했기 때문에 단순히 수익이 나지 않아 닫는다는 얘기보다는 다음 두가지 이야기로 대충 결론이 났다. 첫째는 sns 서비스 개발에 대다수 구글리더 인력이 이동했고 둘째는 구글플러스로의 통합이 이루어지고 있기 때문이다. 내가 내부에 있는 사람이 아니지만 외부에서 보고만 있더라도 회사 전체가 구글플러스에 힘을 더하려는 노력이 많이 보일 정도니 내부에서도 분명 난리일듯 싶다.

이 일로 참 다양한 의견이 많이 나왔다. Rss 자체에 대한 회의론, rss 비지니스 무용론이 대표적. 과연 이 서비스로 장사가 되는가에 대한 진지한 접근이 다시 이야기 되었다. (이 얘기는 메타블로그가 블로고스피어를 양분하던 시절을 생각나게 한다. 그때도 결국 문제는 돈이었으니까) 사실 RSS는 무지무지 오랜 역사를 가지고 있기 때문에(벌써 2013년이다) rss 리더 하나쯤 없어진다고 기술규약이 바뀐다거나 뭔가 심각해질만한 그런 기술도 아니었다. 만약 비지니스 로직이 필요한 상황이라면 차라리 주도적으로 하위 호환성과 함께 다음 세대의 rss를 빠르게 만들어 냈어야 했다. 유료 구독(이거 규약에 들어있다고 어디서 봤었는데…) 같은 접근들 말이다.

누구 말처럼 sns가 포괄적으로 rss의 형태를 가져갈 수 있다는 얘기도 있어서 차대 rss는 사실 sns라는 말도 일부 동의한다. 피드를 구독한다는 rss의 속성이 sns에도 충분하게 녹아져 있고 또 구글도 구글플러스를 염두에 두고 대체할 것이 있다 생각한게 분명하다. 하지만 sns와 rss는 소비 컨텐츠가 아직까진 많이 다르다는 생각이 든다. 정보성 글과 사생활의 글이 정제되지 않은 체로 흐르는 트윗을 보다보면 잦은 화제 변화로 생각이 잘 연결되지 않게 되더라. 근황을 보기엔 정말 좋은 구성이지만 블로그스피어에 비해 가벼운 컨텐츠가 소비되기에만 좋은 환경이란 생각이 든다. 비지니스 쪽으로 생각하면 그런 환경이 훨씬 나은 방향이다. 가십이 많을수록 돈이 될테니까. (지저분해지는 것은 늘 두번째 문제다.)

또한 모두가 구글리더의 전지적 독점 시대 속에서 레드오션으로 생각하고만 있었지 폭탄 선언 이후 그 틈에서 조금씩 점유를 가지고 있던 서비스들로 신속하게 (정말 신속하게) 재편되는 시장을 보니 구글이 잘못된 선택을 했다는 이야기도 정말 많이 보였다. 긍정적으로 보자면 구글리더에 가려져 들어나지 않았던 시장이 다시 살아났다는 얘기도 있다. 구글리더의 api에 의해 굴러가던 수많은 리더들이 “사실 우리 구글리더 없어도 잘되요” 커밍아웃과 함께 엄청난 속도로 시장의 판도를 뒤바꿨다. 뭐 여러모로 대기업이 망해야 수많은 중소기업이 살아난다는 이야기가 허상이 아닌게 들어난게 아닌가 싶다.

또한 장기적으로 봤을 때 구글이 버린 사람들 즉, 서비스 난민은 구글이 제공하는 대다수의 서비스를 믿지 않는, 모종의 적대적 관계가 될 수도 있다. 구글리더의 사용자층이 비록 한정적이라 닫아도 서비스 전반에 영향이 없을 것이란 의견도 있었지만 사용자층이 아닌 수많은 사람들이 컴퓨터를 포맷하기 위해 이 사람들을 만나게 되고 컴퓨터와 관련된 수많은 잡담을 나눌텐데 과연 영향이 없을까 싶다. (개발자라고 노트북 포맷하고 윈도 설치해주는 사람이 아닙니다 여러분!)

주섬주섬 글을 정리 해보자면 내 생각은 이렇다. 서비스 하나 닫는다고 해서 세상이 달라지는 아니라고 생각해 왔다. 대다수 닫는 이유가 늘 그 서비스보다 더 나은게 나오고 있기 때문이니까. 하지만 가장 큰 서비스가 닫는 것은 확실히 다르다. 시장에 대해 진지하게 고민하고 더 큰 서비스가 나올 원동력이 되길 기대한다.

그리고 레드오션이라고 안들어가면 좋은 기회가 왔을 때 구경만 해야 하는 상황이 온다. 그렇다고 레드오션에 무작정 뛰어들란 얘기는 아니지만, 노르망디 프로젝트처럼 준비하고 있는 곳만 시장 재편에 참여할 수 있다. 여튼 여러가지 생각해볼만한 사건인듯 하다.

웹사이트 설정

웹페이지 색상을 선택하세요

Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.