My Profile Photo

DongChanS's blog


수학과 학생의 개발일지


ORM - SQL을 PYTHON으로

ORM과 PYTHON

기존의 sqlite 라이브러리는 sql문법에 의존해서 파이썬코드로 sql을 동작했었다.

그러지 말고 sql문법을 전부 파이썬 객체로 맵핑하여 그냥 파이썬 객체의 인스턴스, 메소드를 통해서 구현할 수 있다면 훨씬 더 체계적이고 가독성이 좋을 것이다.

그러한 객체와 기능의 MAPPING을 바로 ORM이라고 한다.


1. 객체란?

우선 ORM을 보기 전에 파이썬의 객체에 대해서 생각해보자.

CREATE TABLE articles (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    created_at TEXT NOT NULL,
    author TEXT NOT NULL
);

이러한 sql의 table의 데이터를 가져와본다고 생각해보자.


만약 그런 데이터가 리스트로 저장된다면,

articles1 = [
    [1,'제목입니다','내용입니다','신동찬','2019-02-07']
    [2,'제목입니다','내용입니다','신동찬','2019-02-07']
]

이런식으로 되어있을거고

딕셔너리라면,

articles2 = [
    {'id':1,"title":"제목입니다","content":"내용입니다","author":신동찬,"created_at":'2019-02-07'}
    {'id':2,"title":"제목입니다","content":"내용입니다","author":신동찬,"created_at":'2019-02-07'}
]

이런식으로 정의되서 키를 통해서 데이터를 갖고올 수 있을 것이다.

그리고 Object라면?

class Article:
    def __init__(self,id,title,content,author,created_at):
        self.id=id
        self.title=title
        self.content=content
        self.author=author
        self.created_at=created_at
        
a1 = Article(1,'제목입니다','내용입니다','신동찬','2019-02-07')
a2 = Article(2,'제목입니다','내용입니다','신동찬','2019-02-07')

articles3 = [
    a1,a2    
]

이런식으로 저장되서 a1.title 이런식으로 데이터를 갖고올 수 있을 것이다.

그런데 클래스를 이용했을 때의 압도적인 장점이 있는데,

가져온 데이터에 대해서 메소드를 수행할 수 있다는 점이다.

예를 들어서,

class Article:
    def delete(self):
        del(self)

이런 식으로 delete메서드를 만들게 되면, sqlite라이브러리의 connect의 힘을 빌리지 않고도 데이터가 스스로 소멸될 수 있게 되는 것이다. 물론 이것 이외에도 무수한 메서드가 있고, orm의 장점은 엄청 많다.

2. SQLAlchemy

그러면 그런 ORM이 어떻게 적용되는지 예를 한번 들어보자.

class Flight(db.Model):
    __tablename__ = "flights"
    id = db.Column(db.Integer, primary_key=True)
    origin = db.Column(db.String, nullable=False)
    destination = db.Column(db.String, nullable=False)    
    duration = db.Column(db.Integer, nullable=False)    

굳이 설명을 하지 않았는데도 불구하고 무슨 기능을 하는건지 감이 쉽게 올 것이다.

그럼 SQLAlchemy를 설치해보자.

sudo pip3 install sqlalchemy
sudo pip3 install flask-sqlalchemy

이름을 보면 알 수 있듯이 flask와 결합해서 좋은 기능을 발휘할 수 있다.


이것을 파이썬에서 기본적으로 세팅하는 방법은 이렇다.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

app.config["SQLALCHEMY_DATABASE_URL"] = "sqlite:///blog2.db"
# DB를 지정 + 파일명을 지정, 작대기 3개면 상대경로 4개면 절대경로
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False # 꼭 필요하다고 합니다.

db = SQLAlchemy(app) # Flask app과 SQLAlchemy를 합체함.
db.init_app(app)

여기서 ORM의 장점을 볼 수 있다. 우리가 sqlite3을 쓰다가 다른 db(예를 들면 postgre)로 넘어가야할 일이 생기는데, sqlite3문법으로 파이썬코드를 작성하면 다 갈아엎어야하는 문제가 발생한다.

그러나 ORM으로 구성되었다면

app.config[“SQLALCHEMY_DATABASE_URL”] = “sqlite:///blog2.db”

위의 문장을 postgre에 대해서 바꿔놓기만 했을 뿐인데 ORM이 모두 postgre db의 문법으로 맵핑시켜주기 때문에 매우 편리하다.

그리고 이런 ORM은 여러 테이블들을 이용할 때 더 빛을 발한다고 합니다.

3. SQLAlchemy 사용해보기

  • CREATE TABLE

    처음 언급했던 articles 테이블을 생성하는 것은 sqlalchemy에서는 아래와 동일하다.

    class Article(db.Model):
        # db.Model을 상속받으면 db.Column()이라는 메소드를 사용가능함.
        __tablename__ = "articles"
        id = db.Column(db.Integer, primary_key=True)
        title = db.Column(db.String, nullable=False)
        content = db.Column(db.String, nullable=False)
        author = db.Column(db.String, nullable=False)
        created_at = db.Column(db.String, nullable=False)
          
    db.create_all() # 이렇게 만든 테이블을 다 생성해봐라.
    

    Article은 그냥 테이블에 접근하는 class의 이름이고,

    실제로 db에는 articles라는 이름의 테이블이 생성된다. (두개를 다르게 생각하는게 좋음)

  • INSERT INTO

    """ ORM을 이용하여 데이터를 저장한다. """
    title = request.args.get('title')
    content = request.args.get('content')
    a = Article(title=title,content=content) # Article이라는 객체 생성
    db.session.add(a) # 생성한 객체를 디비에 넣어라.
    db.session.commit() # commit
    
  • SELECT

    article = Article.query.all()
    # SELECT * FROM articles와 동일함
    # 이렇게 뽑힌 것들은 객체들의 리스트가 된다.
    # [<article1>, <article2>, ...]
    # 따라서 객체의 원소를 참조하려면 멤버변수를 통해서 들고와야한다!
    

    sqlite 라이브러리를 이용하면 db연결하고, cursor만들고, execute하고 fetch해야하는데 이건 한줄로 다 처리할 수 있다.

  • WHERE

    a = Article.query.filter_by(title="제목인가요?").all()
    

    filter_by라는 메서드를 통해서 제어를 할 수 있다.

    그리고 우리가 많이 쓰는 where문은 바로 id를 통해서 제어하는 방식인데,

    이건 더 간단한 방식으로 구현되어있다.

    a = Article.query.filter_by(id=28).first()
    # SELECT * FROM articles WHERE id=28 LIMIT 1과 동일한 문법
    a = Article.query.get(28) # 이렇게 간단하게도 줄여쓸 수 있다!
    
  • DELETE

    a = Article.query.get(28)
    db.session.delete(a)
    db.session.commit()
    # 삭제할 객체를 찾는다 -> 객체를 제거한다
    

    역시 데이터베이스를 변화시킬때는 반드시 커밋과정을 거쳐야한다.

  • UPDATE

    a.title = "Good Title!"
    # 각 row가 인스턴스이기 때문에 클래스의 멤버변수에 직접 접근해서 바꾸면 됩니다.
    db.session.commit() # 반드시 커밋!
    
  • 기타 함수들

    1. Count

      #SELECT COUNT(*) FROM articles
      Article.query.count()
      
    2. MAX

      a = Article.query.order_by(Article.id.desc()).first()
      

      이상하게 Article.query.max()가 안되는데, 다른 방법이 있다.

      id기준으로 max인 데이터를 출력하고 싶으면 id의 역순으로 정렬한 다음에 첫번째를 뽑으면 된다.

    3. 인스턴스 전체가 아니라 특정 column만 뽑고 싶을 때

      a = Article.query.with_entities(Article.title).all()
      # [(title1,),(title2,),...]
      

      이렇게 뽑으면 인스턴스들의 리스트가 출력되는게 아니라 튜플이 출력된다.


4. 왜 DB에 Commit이 필요한가?

여러 사람이 DB에 동시에 접근하기 때문이다.

은행계좌를 관리하는 은행 DB에 대해서 생각해보자.

은행 DB로 접근하는 방식은 인터넷뱅킹이나, ATM기를 통해서 하는 등 여러가지 방법이 있다.

이러한 여러 접근들이 같은 계좌에 겹치게 되면 문제가 발생하는데,

예를 들어서 신동찬이라는 사람의 계좌에 100원이 있을 때

  • A는 12시 00분 00초에 ATM기를 통해서 신동찬 계좌에 100원이 있음을 확인하고,

    12시 00분 03초에 100원을 인출하였다.

  • B는 12시 00분 02초에 인터넷뱅킹을 통해서 신동찬 계좌에 100원이 여전히 있음을 확인하고,

    12시 00분 05초에 100원 인출명령을 내렸다.

만약 커밋과정이 없다면 돈은 100원밖에 없지만 인출을 둘 다 시행하여 결국 -100원이 되는 것이다.


이러한 상황을 막기 위해서 은행 DBMS에는 transaction이라는 개념이 있다.

(transaction : 여러 가지 sql문장들을 쌓아서 한번에 실제로 반영시키는 하나의 단위)

A와 B는 각자의 transaction을 가지고 있으며,

A가 (Commit을 통해서) 인출명령을 내리면 A의 transaction이 시행되며, 동시에 다른 transaction들의 접근을 막는다 (transaction locking) 그러면 그 시간에는 B의 transaction이 시행될 수 없는 것이다.

따라서, B는 3초 이후에 A의 거래가 끝나고 계좌에 0원이 있다는 것을 확인하게 될 것이다.


이러한 개념을 SQLAlchemy에서는 Session이라는 것을 통해서 관리하는데,

데이터베이스에 접근할 때 마다 사용자 각각마다 Session을 줘서 관리하는 것이다.

그래서 Session을 커밋하면 이제껏 구성한 transaction이 시행되는 개념이라고 생각하면 된다.

comments powered by Disqus