본문 바로가기
데이터 엔지니어링(DE)/파이썬 활용

DataBase / ORM SQLalchemy / session

by kiimy 2021. 7. 24.
728x90
728x90

학습 목표

  • ORM 에 대해서 설명할 수 있어야 합니다.
  • ORM 의 장단점 및 특징들을 이해할 수 있어야 합니다.
  • ORM 을 통해 데이터베이스에 저장할 수 있어야 합니다.
  • sqlalchemy 라이브러리를 사용할 수 있어야 합니다.
  • 파이썬 DBAPI 에 대해서 설명할 수 있어야 합니다.
  • 파이썬으로 데이터베이스와 연결할 수 있어야 합니다.
  • 파이썬에서 데이터베이스와 상호작용 할 수 있어야 합니다.

PEP 249

파이썬에서는 데이터베이스와 연결하는 파이썬 모듈들은 권장되는 가이드라인을 따라 제작되도록 안내합니다.

실제로 문서를 보시면 어떤 함수들, 메소드, 파라미터, 키워드 등을 어떻게 해야 하는지도 나와있습니다.

이러한 명시적인 가이드라인 덕분에 파이썬에서 데이터베이스와 연결하는 작업은

다양한 데이터베이스라도 동일한 API를 이용해 데이터를 다룰 수 있습니다.

https://www.python.org/dev/peps/pep-0249/

SQLite

SQLite 데이터베이스는 기본적으로 파이썬과 함께 설치되는 가벼운 관계형 데이터베이스

 

PostgreSQL 와 같이 데이터베이스 서버를 띄우고 작업하지 않고 SQL 을 사용할 때 기능적 제한으로

복잡하거나 고급 쿼리 등은 실행하기 어렵다.

 

또한 파일형 데이터베이스이며 메모리에 상주할 수 있기 때문에 파일을 삭제하거나 프로세스 종료 등으로

인한 데이터 손실도 주의해야 한다

설치

파이썬 2.5x 이후로 기본적으로 설치되는 모듈인 sqlite3 는 PEP 249 에 적힌 DBAPI 2.0 요구조건에

부합한 SQL 인터페이스를 제공

 

데이터베이스 연결( connect, cursor, execute )

연결하는 방법은 매우 간단합니다. sqlite3 의 connect 메소드를 활용해 데이터베이스 파일의

위치를 알려주기만 하면 됩니다.

  • connect = 데이터베이스와 연결 및 세션을 보관
  • cursor = 데이터베이스와 소통
  • execute = 소통 방식
import sqlite3

conn = sqlite3.connect('test.db') 

--> conn 은 데이터베이스와 연결된 하나의 session을 보관( 다리를 놓아준다 )

# 확장자 .db or .sqlite3

해당 세션을 통해 데이터베이스와 소통하기 위한 cursor

cur = conn.cursor()

--> 커서를 통해서 데이터베이스와 소통

소통 cursor (명령어를 전달?)

가장 기본적인 데이터베이스 소통 방식 cursor의 execute 메소드를 활용

==>이 메소드의 인수로는 SQL 쿼리문을 바로 넘겨줄 수 있다.

* pep249에 의거해서 작성 *

cur.execute("""CREATE TABLE test_table (
                        name VARCHAR(32),
                        age INT);
                """)

# ==> Table 생성

cur.execute("INSERT INTO test_table (name, age) VALUES ('spongebob', 12);")

conn.commit() - 마지막에 해줘야 반영됨 ==> # execute 사용하면 무조건!!

# ==> 해당 칼럼에 값 설정

추가한 데이터 조회( cursor.fetchone, cursor.fetchall )

cur.execute("SELECT * FROM test_table;") ==> X 보이지않는다

return cursor 객체에 대한 정보를 보여줌

so, response = cur.execute("SELECT * FROM test_table;") # 변수 할당

     response.fetchone() , response.fetchall() 사용

or cur.fetchone()으로 바로 들어갈 수도 있음(= response 변수 할당 x 진행시 )

  • - fetchall()의 경우 결과를 모두 리턴 
  • - fetchone()의 경우 하나의 row를 리턴
  • - fetchmany(num rows)의 경우 rows의 숫자 만큼 리턴을 한다.(=n개만큼의 데이터를 가져올 때 사용)

PostgreSQL 연결하기

데이터베이스 서버와 연결하는 예시

먼저 이 예시를 실행하기 위해서는 postgresql 서버와 연결해야 하기 때문에 실행되고 있는 서버가 있어야 합니다.

PostgreSQL 와 같은 경우에는 psycopg2 를 설치해 사용

 

psycopg2 와 psycopg2-binary

The psycopg2-binary package is meant for beginners to start playing with Python and PostgreSQL without the need to meet the build requirements.

If you are the maintainer of a published package depending on psycopg2 you shouldn’t use psycopg2-binary as a module dependency. For production use you are advised to use the source distribution. 라는데 잘모르겠다.

=> psycopg2는 곧 psycopg3로 이전될 것이니, psycopg2-binary를 설치

import psycopg2

conn = psycopg2.connect(
                             host="서버 호스트 주소",
                             database="데이터베이스 이름",
                             user="유저 이름",
                             password="유저 비밀번호")

 

이하 데이터베이스와 상호작용하는 것은 동일

ORM Object-Relational Mapper

- 코딩을 편안하게(= 코드와 데이터베이스를 연결해서 사용할 때 )

SQL - 관계형데이터 베이스

Python - 객체 중심

==> 데이터베이스와 코드의 연결을 간편하게 해주면서 사용자가 코드에 관한 부분만 신경쓸수 있도록 해줌

Object-Relational Mapping

SELECT * FROM users;

이런 단순한 쿼리 보다 더 복잡한 쿼리를 작성할 때 원하는 객체 지향 언어로 작성하게 해주는 것

데이터베이스 구조와 코드상에 구조를 연결

코드로 작성한 부분을 데이터베이스에 실행할 때 ORM데이터베이스가 이해할 수 있도록 번역을 해줍니다.

마찬가지로 데이터베이스에서 돌려받는 결과들을 코드에서 사용할 수 있도록 마찬가지로 번역

 

데이터베이스를 Postgres 가 아닌 MySQL 로 변경한다고 하더라도 파이썬 코드를 수정할 필요없이

ORM 이 데이터베이스 변경을 인지할 수 있도록 알맞은 엔진을 설정해 주기만 하면됨

 

이러한 부분들이 잘 동작하려면 코드와 데이터베이스는 동일한 정보를 가지고 작업해야된다.

 

예를 들어 데이터베이스에는 User 테이블이 존재하해당 테이블에는 id, name 등의 필드들이 존재한다면 코드에서도 동일하게 이 필드들이 있다고 ORM 에게 알려줘야 합니다.

그렇지 않은 경우 ORM 작업이 원활하게 돌아가지 않고 에러가 발생할 수도 있습니다.

 

* 장점

  • 현재 사용하고 있는 언어만 사용해도 됩니다.
  • 데이터베이스 시스템으로부터 분리가 됩니다. 따라서 MySQL, PostgreSQL 등 원하는 시스템을 사용할 수 있습니다.
  • ORM 에 따라 상이하지만 다양한 기능들을 지원합니다.
  • 직접 작성하는 SQL 쿼리문이 없고 더 높은 성능의 SQL 쿼리문들이 작성될 수도 있습니다.

* 단점

  • 데이터베이스와 바로 연결하는 것보다 초기설정이 더 많아지거나 복잡해질 수 있습니다.
  • 내부 작동에 대한 충분한 이해가 없는 경우 문제 해결이 힘들 수 있습니다.
  • 데이터베이스에 직접 쿼리문을 보내는 것이 아니기 때문에 성능 저하가 발생합니다.
  • 데이터베이스 복잡도를 데이터베이스 시스템에서 어플리케이션 코드로 옮기게 됩니다.

단점을 정확히 알고 사용할 필요가 있다.

Object-Relational Impedance Mismatch

수학 원리에 기반한 데이터베이스 시스템과 소프트웨어 엔지니어링 원리에 기반한 객체 지향 언어가

함께 작동할 때 차이가 발생( 차이를 알려면... 찾아봐야한다 )

http://www.agiledata.org/essays/impedanceMismatch.html

 

The Object-Relational Impedance Mismatch

This book, Choose Your WoW! A Disciplined Agile Delivery Handbook for Optimizing Your Way of Working, is an indispensable guide for agile coaches and practitioners to identify what techniques - including practices, strategies, and lifecycles - are effectiv

www.agiledata.org

SQLAlchemy 

ORM 기능을 제공하는 SQLAlchemy 라이브러리

https://docs.sqlalchemy.org/en/13/orm/tutorial.html

 

Object Relational Tutorial — SQLAlchemy 1.3 Documentation

A Query object is created using the query() method on Session. This function takes a variable number of arguments, which can be any combination of classes and class-instrumented descriptors. Below, we indicate a Query which loads User instances. When evalu

docs.sqlalchemy.org

$ pip install sqlalchemy ==> 낮은 버전으로 install됨 / pip install --pre sqlalchemy로 설치

'Core' 

  • Core 가 데이터베이스 시스템과 상호작용하는 부분
  • SQL 추상화 툴킷으로 온갖 기능을 포함
  • 파이썬의 다양한 DBAPI 들과 각각의 행동들을 추상화 할 수 있고 SQL 문법을 파이썬 언어로 표현 가능하게 해줌
  • 추가적으로 DDL 을 사용할 수 있으며 기존에 정의가 되어 있는 내부 스키마를 확인
  • (DDL 은 데이터를 정의할 때 사용되는 언어)
  • 파이썬의 데이터 타입을 데이터베이스 타입으로 변환할 수 있는 타입 시스템이 있다.

DBAPI( 'Python Database API Specification' )

Python에서 데이타베이스를 엑서스하기 위한 한 방법으로 Python DB API

 

데이터베이스 연결 패키지들 간에 공통된 사용 패턴들을 설립

데이터베이스 연결과 관련된 파이썬 패키지들이 공통된 특징들을 보여주는 것이 목적( PEP 249 명시 )

 

어떤 방식으로 작성을 해야 하는지 가이드를 보여주고 이에 따라 데이터베이스 관련된 파이썬 패키지들을

사용할 때 사용자들이 더 손쉽게 모듈들을 이해할 수 있도록

 

예를 들어 connection 객체를 리턴하는 함수는 connect(parameters...) 라고 정의하고 있습니다.

또한 이러한 객체를 리턴받았을 때 어떤 메소드가 있어야 하는지 등도 명시되어 있습니다.

SQLAlchemy 또한 이러한 DBAPI 를 사용하기도 하며 특정 데이터베이스 시스템과 더불어 DBAPI 를 사용

'ORM' SQLAlchemy

ORM 은 Core 를 기반으로 한 기능

==> SQLAlchemy 에서의 기능들을 어플리케이션 코드와 ORM 사용에 더 초점을 맞춰서 변경을 한 것

데이터베이스와의 연결

제일 먼저 신경써야 할 부분은 데이터베이스 시스템과 연결을 하는 것입니다. 이 부분은 SQLAlchemy

(이하 SQLA) 에서 엔진을 통해 연결하도록 되어 있습니다.

1. 엔진

엔진을 통해 연결을 하게 되면 SQLA 의 기능 중 하나인 추상화를 통해서 DBAPI 를 사용할 수 있게 됩니다.

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

정확히 하자면 엔진의 생성이 DBAPI 사용으로 이어지지는 않습니다.

엔진을 생성한 뒤에 연결을 하는 경우에는 Pool 을 통해 DBAPI 를 사용하게 됩니다

from sqlalchemy import create_engine

engine = create_engine("데이터베이스 주소")

=> 데이터베이스 주소는 URI 형식 / DBAPI = sqlite.connect('test.db')

==> connect 함수에 들어가게 되는 값은 애초부터 sqlite 를 가정하기 때문 ( URI가 아님 )

# 메모리 상에서 sqlite 데이터베이스를 사용하고 싶은 경우
engine = create_engine("sqlite:///:memory:")

2. Mapping ==> sqlalchemy - declarative_base 활용

매핑은 데이터베이스의 구조와 코드 상에 구조를 연결하는 것

즉, 데이터베이스에 user 라는 테이블이 존재한다면 코드 상에서도 이 테이블과 정상적으로

     연결이 될 수 있도록 user 테이블을 코드로 구현을 하는 것

 

ORM 에서 매핑을 하기 위해서는 두 가지

  1. 어떤 테이블들을 다루게 될지 ORM 에게 알려주는 것
  2. 데이터베이스의 테이블에 매핑될 테이블 클래스들을 코드로 구현하는 것

==> SQLA 에서는 이렇게 둘로 나뉘어진 작업을 하나로 묶어서 작업할 수 있다.

declarative_base == 테이블 클래스를 구현할 때

from sqlalchemy.orm import declarative_base

Base = declarative_base()

이 클래스를 이용해 상속을 받는 클래스들을 자동으로 인지하고 알아서 매핑하도록 설정이 되어 있습니다.

따라서 이 클래스를 생성한 뒤에 코드로 테이블 클래스를 생성할 때 부모 클래스로 넘겨주게 되면

ORM 에서는 매핑을 알아서 해주고 테이블을 인식하게 됩니다.

테이블 생성

* sql

CREATE TABLE user (
    id INTEGER PRIMARY KEY,
    name STRING,
    age INTEGER
)

* ORM

from sqlalchemy import Column, Integer, String

class User(Base):
    __tablename__ = 'user'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    age = Column(Integer)
    
    # relationship
    addresses = relationship('Address', back_populates='users')
    
# 관계 설정 - ForeignKey

class Address(Base):
    __tablename__ = 'address'

    id = Column(Integer, primary_key=True)
    country = Column(String(24))
    city = Column(String(24))
    street = Column(String(64))
    user_id = Column(Integer, ForeignKey('user.id'))
    
    # relationship
    users = relationship('User', back_populates='addresses')

ForeignKey

안에 들어가 있는 문자열은 데이터베이스 상에서 존재하는 칼럼에 접근하는 방식

user.id 는 데이터베이스에서 조회할 때 사용하게 되는 문자열

 

# relationship

==> ForeignKey 가 데이터베이스 스키마를 반영하도록 도와준다면 relationship 은 코드 상에서

       데이터를 더 편리하게 다룰 수 있는 방법

 

Integer 같은 경우에는 숫자 타입이지만 따로 길이를 정하지는 않습니다.

만약에 숫자의 한계에 대해 명시하고 싶을 때에는 BigIntSmallInt 등을 사용해서 조절해야 합니다.

 

String 과 같은 경우에는 어떤 데이터베이스 시스템을 사용하는지에 따라 다른 조건들이 있습니다.

하지만 가장 안전한 방식은 숫자를 넘겨서 문자열의 한계를 정해주는 것

primary_key=True
레코드가 추가될 때마다 자동으로 숫자가 증가하는 auto increment 기능은 칼럼 타입이 숫자 타입이고
기본키에 포함될 때 자동으로 설정
# 실제로 데이터베이스에 저장하지 않고 조회를 할 경우에는 따로 숫자가 부여되지 않았을 수도 있습니다.
다만 primary_key=True 설정이 되어 있는 등 특정 조건에 따라 알아서 nullable=False 로 설정

nullable
파라미터는 기본적으로 True 로 설정
따라서 따로 설정을 하지 않은 경우에는 실제 데이터베이스에는 NOT NULL
False

* 클래스의 인스턴스를 조회헀을 때 어떻게 보여질지는 __repr__ 함수사용

class User(Base):
    __tablename__ = "user"

    (... 생략 ...)

    def __repr__(self):
        return f"User {self.id}: {self.name}"
        
patrick = User(name='Patrick', age=20)

print(patrick) #=> "User 1: Patrick"

# 설정이 되어 있지 않은 경우
print(patrick) #=> "User(id=None, name="Patrick", age=20)"

3. 스키마 생성

테이블들을 다 설정했다면 이제는 스키마를 생성

메타데이터를 보관하고 있는 Base 를 이용해서 스키마를 간단하게 생성해준다.

 

Base.metadata.create_all(engine)

스키마를 생성하기 위해서 Base 를 사용하게 되고 사전에 정의된 엔진을 활용해서 연결된 데이터베이스에

스키마대로 테이블들을 생성함

4. session 생성

connection은 클라이언트 프로세스와 데이터베이스 인스턴스 간의 물리적 경로를 말합니다.

즉, 클라이언트와 인스턴스 간의 네트워크 커넥션을 말하는 겁니다.

 

session은 인스턴스안에 있는 논리적인 실체로 현재 유저의 로그인 상태를 나타냅니다.

session = connect 와 session 이라는 두 객체가 존재
일단 먼저 엔진이라는 것을 통해서 데이터베이스와 연결을 하게 된다면 connection pool 이라는 것이 생긴다.
== > 연결된 데이터베이스와 소통할 수 있게 해주는 연결선들을 제공
여기에서 connect 와 session 은 이 커넥션 풀에서 연결선 하나를 끌어와 사용

다만,
session = 해당 커넥션 풀을 알아서 관리
              즉, 연결이 끊어지거나 종료되면 알아서 데이터베이스 연결선을 돌려준다
              ==> 알아서 관리해주는 것 덕분에 쓰레드 활용이 안전

connect = connect 는 알아서 연결선을 열고 닫아야한다.
              ==>쓰레드 위험성도 증가하고 이를 관리해주지 않은 경우에는 문제가 발생할 수도 있다,

세션 객체란 ORM 으로 매핑된 객체들을 활용한 지속적 작업을 관리해주는 역할( session object )

==> SQLA 에서는 알아서 관리해주는 세션 객체만 사용하셔도 무방

 

class sqlalchemy.orm.sessionmaker(bind=None,
                       	 	class_=<class 'sqlalchemy.orm.session.Session'>,
                                    autoflush=True, 
                                    autocommit=False, 
                                    expire_on_commit=True, 
                                    info=None, **kw)

 

* add : session에 인스턴스를 배치하는 데 사용합니다. 그리고 다음 flush 시에 INSERT가 발생합니다.


* flush : 트랜잭션을 데이터베이스로 전송합니다. 아직 커밋되지 않은 상태입니다.  

          flush를 통해 트랜잭션 순서를 보장해줘야 합니다. 


* commit : 트랜잭션을 커밋합니다. 내부적으로 항상 flush()를 실행해서 트랜잭션을 flush 합니다.

              sessionmaker에서 설정된 autoflush 여부와 상관없습니다. 

 

트랜잭션(Transaction)은 데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위

                                  또는 한꺼번에 모두 수행되어야 할 일련의 연산들을 의미

 

이전의 커밋(COMMIT)이 일어난 뒤부터 다음의 커밋(COMMIT) 전까지의 작업이 하나의 트랜잭션 이며,

커밋과 롤백(ROLLBACK)은 (비정상적인 종료, system failure)
이러한 트랜잭션 단위로 데이터 베이스에서 발생한 작업을 저장, 삭제하는 일이다.

COMMIT

  • 모든 작업을 정상적으로 처리하겠다고 확정하는 명령어이다.
  • 트랜젝션의 처리 과정을 데이터베이스에 반영하기 위해서, 변경된 내용을 모두 영구 저장한다.
  • COMMIT 수행하면, 하나의 트랜젝션 과정을 종료하게 된다.
  • TRANSACTION(INSERT, UPDATE, DELETE)작업 내용을 실제 DB에 저장한다.
  • 이전 데이터가 완전히 UPDATE된다.
  • 모든 사용자가 변경한 데이터의 결과를 볼 수 있다. 
from sqlalchemy.orm import sessionmaker

# case1
Session = sessionmaker()
Session.configure(bind=engine)

# case2
Session = sessionmaker(bind=engine)

# 세션 객체 생성 sessionmaker 설정 후 
Session 클래스 인스턴스화(==> 안해주면 .add, .delet 속성 없다고 Error나옴)

session = Session()

# case2

엔진을 알아서 찾지않기 때문에  만드는 시점에 엔진을 bind 파라미터를 통해 넘겨줘야한다

 

# case1

초기에 바인딩 하지 않았다면 해당 코드로 진행가능 ( Session.configure(bind=engine) )

세션 객체가 엔진이 바인딩이 되어 있지 않은 경우에는 UnboundExecutionError 라는 SQLA 에러

 

코드로 데이터베이스 관련 작업을 하게 되면 바로 데이터베이스 시스템으로 작업 내용을 보내지 않습니다.

이 작업들은 세션 객체에서 일시적으로 관리를 하게 됩니다.

commit을 해야 session에서는 작업 내용들을 데이터베이스 시스템에 보냅니다.

==> 사용자는 세션 객체를 통해서 데이터베이스 관련 객체들을 관리 =  세션 객체는 엔진으로부터 파생

5. 데이터베이스 작업

Create

데이터를 추가하는 작업입니다. 앞서 생성했던 'user' 테이블을 사용해서 설명을 해보도록 하겠습니다.

먼저 ORM 에서는 객체 지향이기 때문에 클래스, 인스턴스 등 '객체' 를 통해 작업을 하게 됩니다. 따라서 제일 먼저 필요한 것은 'user' 클래스의 인스턴스를 생성하는 것

patrick = User(name="Patrick", age=20)

session = Session(bind=engine)

* data = session.query(Invoice_Item).filter_by(item_id = item_id).all()

= 데이터 조회시(메소드 체이닝)

session에 Query를 하는데 Invoice_Item table을 쿼리를 하고 필터를 하는데 모두 가져와라

data.(점) 을 통해서 접근할 수 있음

ex) data.id , data.name .....

                                                         

session.add(patrick) = 객체를 넘기기만 하면 세션에서는 테이블 클래스의 인스턴스라는 것을 알아서 인지

                               인지함과 동시에 일단 보관

                               세션에서는 현재 말 그대로 patrick 이라는 객체를 추가 받았은 것

session.delete(patrick) = 삭제시 query문 작성(조회)하고 나서 삭제해야 가능

만약에 이대로 종료가 되거나 세션의 연결이 종료가 된다면 SQL 의 ROLLBACK 이 실행

session.commit()

6. Session 종료

세션으로 인한 불필요한 오버헤드도 제거하고 데이터베이스의 연결도 안정적으로 종료한다는 것을

명시할 수 있습니다.

SQLalchemy

연결이 끊어지거나 종료되면 알아서 데이터베이스 연결선을 돌려준다. SQLalchemy 사용하면 알아서 관리해줌

  • 현재 connecting pool 에 연결된 모든 connection 리소스를 풀어줍니다. 또한 이 과정에서 진행중이던 트랜잭션 또한 롤백됩니다. 다시 말해 세션을 종료하게 되면 따로 롤백 함수를 실행하지 않아도 알아서 롤백 해준다는 뜻입니다.
  • 세션에서부터 모든 객체를 제거합니다. 즉, 세션에 로딩되었던 파이썬 객체들이 있었다면 이제는 연결이 제거된 상태로 변경이 됩니다. 또한 세션을 커밋하게 되는 과정에서 데이터베이스와 연결된 객체들은 더 이상 연결이 되어 있지 않게 되고 기능을 잃게 됩니다.
728x90

댓글