ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SQLAlchemy 시작하기 - Part 1
    Python & Flask & Django 2017. 1. 31. 22:16


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


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


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


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



    버전 확인하기


    import sqlalchemy

    print sqlalchemy.__version__



    접속하기


    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 "blabla"

        

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


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


    Declarative system으로 만들어진 이 클래스는 table metadata를 가지게 되는데 이게 사용자 정의 클래스와 테이블을 연결해주는 구실을 한다. 예전엔 이 metadata를 만들고 클래스에서 맵핑해서 썼는데 그 방식을 Classical Mapping이라고 한다. 그 예전 방식에서는 Table이라는 데이터 구조와 Mapper 객체로 클래스와 맵핑한다.


    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)


    class User(Base):

        __tablename__ = 'users'
        id = Column(Integer, Sequence('user_id_seq'), 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 "blabla"



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



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


    ed_user = User('haruair', 'JohnJeong', '1234')

    ed_user.name        # 'haruair'

    ed_user.passwrod  # '1234'

    str(ed_user.id)        # 'None'


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


    위의 값에서 'JohnJeong'을 디비에 넣기 전까지 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', 'JohnJeong', '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', 'JohnJoeng', '1234')>


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


    ed_user is our_user     # True

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


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


    session.add_all([

        User('test', 'JohnJeong1', '1234')

        User('test1', 'JohnJeong2', '1234')

        User('test2', 'JohnJeong3', '1234')

    ])


    비밀번호를 함 바꿔보자.

    ed_user.password = 'test1234'


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


    session.dirty     # IdentitySet([<User['test', 'JohnJeong1', 'test1234')>])


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


    session.new

    # IdentitySet([    User('test', 'JohnJeong1', '1234')

    #    User('test1', 'JohnJeong2', '1234')

     #   User('test2', 'JohnJeong3', '1234')


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


    session.commit()


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


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


    ed_user.id     # 1


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


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




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


    ed_user.name = 'edkim'


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


    fake_user = User('fakeuser', 'Invlid', '1234')

    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, '1234')>]


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

    session.rollback()

    ed_user.name                # 'haruair'   

    fake_user in session     # 'false'



    쿼리 보내기


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


    for instance in session.query(User).order_by(User.id):    

        print instance.name, instance.fullname



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


    for row in session.query(User, user.name).al():

        print row.User, row.name


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


    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


    LIMITOFFSET을 포함한 기본적인 Quqery 동작은 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).fileter_by(fullname='JohnJeong'):

        print name


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


    for name in session.query(User.naem).filter(User.fullname=="JohnJeong"):

        print name


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


    for name in session.query(User).\

            filter(User.name=='haruair').\

            filter(User.fullname=="JohnJeong"):

        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','jack']))


    NOT IN

    query.filter(~User.name.in_(['ed','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.fullname == 'JohnJeong'))


    또는 위에서 본 체이닝 메소드로


    filter.(User.name == 'ed).filter(User.fullname == 'JohnJeong')


    OR

    from sqlalchemy import or_

    filter(or_(User.name == 'ed', User.name == 'JohnJeong'))


    match 

    query.filter(User.name.match('johnjeong'))




    끄읕. 


    'Python & Flask & Django' 카테고리의 다른 글

    SQLAlchemy Tutorial  (0) 2017.02.02
    SQLAlchemy 시작하기 - Part 2  (0) 2017.02.01
    Python public, private, protected  (0) 2017.01.25
    WSGI (Web Server Gateway Interface)  (0) 2017.01.19
    if __name__ = &quot;&quot;__main__: 의미!  (0) 2017.01.19
Designed by Tistory.