SQL and Data Modeling

Contents

  • Relational Concepts

    • Why do it in the database
    • Uniqueness and Keys
    • Primary Keys & Foreign Keys
    • Execution Plan
    • Performance
  • Data and Tables

    • Related Tables
    • Uniqueness and Keys
    • Types
  • Review TCP/IP
  • Transactions
  • Interacting with Databases

    • DBMS
    • DBAPIs
    • SQLAlchemy Basic
    • Flask with SQLAlchemy
  • SQL Queries

    • Aggregation
  • SQLAlchemy ORM

    • SQLAlchemy Object Lifecycle
  • Building a CRUD App

    • Model View Controller (MVC)
  • Migration

    • What is Migration?
    • Why use migration?

Relational Concepts

프로그래밍을 하다 보면 데이터를 저장할 공간이 필요하죠. 이를 위해 number나 string, list, dictionary 등이 필요해요. 이러한 것들을 메모리 상에 저장할 수 있지만, 프로그램이 종료되면 데이터도 사라집니다. text, xml, json과 같은 파일에 저장 할 수도 있지만, 우리는 데이터베이스를 이용할 수도 있습니다.

데이터베이스는 파일과 같이 데이터를 잃지 않고 저장할 수 있습니다. 또한, 일반 파일보다 데이터 검색이 빠릅니다. 이에 더불어 데이터베이스를 이용하면 다수의 프로그램 또는 유저가 접속하고 데이터를 수정하는 것이 가능합니다. 이 외에 데이터베이스에서는 데이터를 querying, summarizing 할 수 있는 여러 편리할 툴을 사용할 수 있습니다.

Why do it in the database

데이터베이스의 order by, limit, offset 등과 같은 조건을 접했을 때 웹 애플리케이션 단에서도 관련된 작업을 진행할 수 있지 않나 하는 생각이 들 수 있어요. 자바스크립트, 파이썬과 같은 언어로 같은 작업을 진행할 수 있습니다. 하지만, speed와 space의 관점에서 큰 차이가 존재합니다. 특히, 행이 많고 여러 테이블을 조인하는 작업 등을 진행할 때 데이터베이스를 이용하면 빠르게 처리할 수 있지만, 같은 작업을 파이썬으로 진행하면 초단위로 걸릴 수 있습니다.

Uniqueness and Keys

데이터베이스를 다루며 유니크함에 대한 언급이 자주 나옵니다. 개인이나 특정 지역 등을 예로 들 수 있습니다. 하지만, 개인의 이름은 같은 이름을 가진 사람이 있을 수 있기 때문에 유니크하지 않습니다. 특정 지역명 또한 유니크한 값이 될 수 없습니다. 예로 Springfield란 지역명은 미국의 여러 곳에서 사용되고 있고 국가 이름으로도 존재해 헷갈릴 수 있습니다. 우리는 프로그램상 유니크한 개인에게 별도의 티켓이나 성적을 설정하려면, 개인을 나타내는 유니크한 값이 필요합니다. 그래서 대부분의 데이터베이스는 이러한 작업을 해주는 기능이 있습니다.

Primary Keys & Foreign Keys

관계형 데이터베이스 테이블의 각 행은 다른 행과의 구분을 위해 유니크한 값을 지닌 열이 있어야 해요. 이 column을 Primary Key라고 합니다. Foreign Key는 다른 테이블에서 사용하는 Primary Key를 말합니다. 테이블 간 관계를 형성할 수 있도록 하는데 사용됩니다.

Primary Key는 보통 컬럼 이름으로 "id"를 사용하며, 값은 integer나 uuid 등으로 대게 사용합니다. Primary Key로 integer를 사용하는 경우 postgreSQL에서는 SERIAL 데이터 타입을 사용할 수도 있습니다.

Execution Plan

SELECT * FROM vehicles WHERE driver_id = 1

우리가 위와 같은 쿼리를 작성하면 Database Management System은 vehicles 테이블의 행 중 article_id가 1인 행을 복사한다는 execution plan을 생성합니다. 그리고 데이터베이스 엔진에 의해 실행이 됩니다. 위 Query에 대한 execution plan을 살펴보면 아래와 같습니다.

QUERY PLAN
Seq Scan on drivers (cost=0.00..18.50 rows=850 width=68)

Execution Plan은 쿼리 성능에 대한 기준을 제시해 줍니다. 다양한 쿼리에 따라 trade-off가 존재하고 이에 따라 성능도 각각 다르기 때문에 원하는 결과를 얻기 위해 적합한 execution plan을 선택하는 것이 중요합니다.

아래와 같은 쿼리를 요청 했을 때도 좀 더 자세히 살펴볼게요.

SELECT make, model from vehicles
JOIN drivers on vehicles.driver_id = drivers.id;

이에 해당하는 execution plan을 한 개씩 살펴볼게요.

  1. cost에서 확인할 수 있듯이 Hash Join이 가장 비싼 작업으로, 모든 행에 join 작업을 진행합니다. 이 작업이 꼭 필요한지, execution plan을 나누며 원하는 결과를 얻을 수는 없는지 고민해 볼 수 있을거 같아요. Hash join은 메모리 상 hash를 생성합니다.
  2. Seq Scan은 vehicles 테이블 전체에 걸쳐 실행됩니다. make, model column을 가져오기 때문에 해당 작업은 필요하다 생각됩니다.
  3. Hash with Seq Scan on drivers: as the sequential scan continues, the join key is checked in the Hash returned from Step 1, where if it does NOT exist, given that this is an Inner Join, we ignore that row, and if it does exist (a record was found that does intersect between the vehicles and drivers tables), then we fetch the row from the hash to generate the outputted, joined row.

Performance

효율적인 쿼리 작성은 그 자체만으로 하나의 분야로 다룰 만큼 학습할 것이 많습니다. 하지만, SQL을 학습하며 성능을 항상 염두에 두는 것은 중요하다고 생각해요. 웹 사이트 방문 시 또는 검색 시 수 초간 기다리는 것을 좋아하는 사람은 없으니까요.

Data and Tables

Types

Data 타입 종류를 다 열거하기에는 종유가 매우 많고 관련 링크를 참고할 수 있습니다. 기본적인 타입 몇 개를 살펴보면 아래와 같습니다.

  • Text, String types: text, char(n), varchar(n)
  • Numeric types: integer, real, double precision, decimal
  • Date, Time types: date, time, timestamp

Review TCP/IP

데이터베이스가 API 서버와 통신할 때 클라이언트 - 서버 모델로 통신을 합니다. 네트워크 상에서 컴퓨터 간 통신을 위해서 정해진 약속을 따라야 하고 이를 프로토콜이라 합니다. 그리고, 대표적인 프로토콜이 TCP/IP 입니다. TCP/IP는 IP 주소를 이용해 네트워크 상 컴퓨터의 위치를 파악하고, 특정 연결을 위해 해당 컴퓨터의 Port를 이용합니다. 대표적인 Port 넘버로 80과 5432가 있습니다. 80은 HTTP 요청시, 5432는 데이터베이스 시스템에서 사용됩니다.

TCP/IP는 connection-based로 클라이언트-서버 간 통신을 위해 connection이 성립 되어야 합니다. connection이 성립되면 세션이 시작되고, connection 종료 시 세션도 종료됩니다. 그리고 각 세션에 다수의 transaction이 발생할 수 있습니다.

Transactions

데이터에 대한 변화는 transaction이라 불리는 작업에 의해 이루어 집니다. 데이터베이스에 Update, Insert, Delete와 같이 여러 작업을 진행하면 한 덩이의 번들로 작업이 이루어 집니다. 정해진 순서에 의해 작업이 순차적으로 이루어 지고, 이러한 작업이 성공적으로 완료될 수도 있고 또는 실패할 수도 있습니다.

그리고 위 작업이 마무리 되면 수정 사항을 적용 시키기 위해 데이터베이스에 전달합니다. 이와 같은 번들링 작업으로 인한 이점은 도중에 에러가 발생하는 경우 해당 transaction 자체를 롤백할 수 있다는 것이에요.

데이터베이스 자체에서도 에러가 발생할 수 있기 때문에, 이러한 번들링 작업 단위로 데이터베이스에 수정을 진행합니다. 혹여나 도중에 에러가 발생하더라도 정상적이었던 상태로 돌려 애플리케이션 작동에 차질이 없도록 하기 위함이에요. 좀 더 깊이 알아 보고 싶으면 아래 ACID properties 링크에 추가 정보가 있습니다.

Interacting with Databases

DBAPI

커맨드라인 툴 또는 GUI를 통해 데이터베이스 서버에 접속해 명령어를 입력할 수 있습니다. 하지만, 우리는 실제 애플리케이션을 만들며 프로그래밍적으로 데이터베이스와 상호작용 해야하고, 이를 위해 DBAPI가 필요합니다. DBAPI는 Nodejs, Python과 같은 언어가 데이터베이스와 소통할 수 있도록 인터페이스를 제공하며, 로우레벨에서 데이터베이스와 연결할 수 있도록 SQL Statement를 작성해 줍니다. Database Adapter라고도 부릅니다. 노드에서는 node-postgres, 파이썬에서는 psycopg2, 루비에서는 pg를 사용할 수 있습니다. 예로, psycopg2를 이용해 데이터베이스에 새로운 테이블과 행을 아래와 같이 추가할 수 있어요.

import psycopg2

connection = psycopg2.connect('dbname=example')

# Open a cursor to perform database operations
cursor = connection.cursor()

cursor.execute("""
  CREATE TABLE todos (
    id serial PRIMARY KEY,
    completed BOOLEAN NOT NULL DEFAULT False
  );
""")

# commit, so it does the executions on the db and persists in the db
connection.commit()

cursor.close()

connection.close()

만약 Table을 생성하고, 행을 추가하고자 한다면 테이블 생성 후 아래 쿼리를 추가핼 줄 수 있습니다.

SQL = 'INSERT INTO table2 (id, completed) VALUES (%(id)s, %(completed)s);'

data = {
  'id': 1,
  'completed': True
}
cursor.execute(SQL, data)

# commit, so it does...
...

테이블 생성 및 추가한 행을 확인해 보고 싶으면, 아래 코드로 확인해 볼 수 있습니다.

cursor.execute(
  'INSERT INTO table2 (id, completed) VALUES (%s, %s);', (3, True)
)

cursor.execute('SELECT * FROM table2;')

result = cursor.fetchall()

print(result)
# => print [(1, True), (2, False), (3, True)]

위와 같이 코드를 통해 데이터베이스와 연동하는 작업을 해 보았습니다. 하지만, 실제 웹 애플리케이션에서 SQL을 직접 쓰는 방법은 조금 번거로워, 이를 대신해 줄 라이브러리를 뒤쪽에서 살펴보도록 할게요.

SQLAlchemy Basic

SQLAlchemy는 python 라이브러리로 쿼리를 직접 작성하지 않고도 데이터베이스 작업을 할 수 있도록 해 줍니다. 또한 ORM(Object Relational Mapping)을 제공하기 때문에 데이터베이스의 테이블, 컬럼을 클래스, 객체로 맵핑해서 사용할 수 있다는 짐이에요.

앞서 Table 생성을 위해 아래와 같이 코드에서 쿼리를 직접 작성 했었습니다.

cursor.execute("""
  CREATE TABLE todos (
    id serial PRIMARY KEY,
    completed BOOLEAN NOT NULL DEFAULT False
  );
""")

SQLAlchemy를 사용하면 아래와 같이 클래스로 변경해 테이블을 생성할 수 있어요.

class TODO(db.Model):
  id = db.Column(db.Integer, primary_key=True)
  description = db.Column(db.String(), nullable=False)
  completed = db.Column(db.Boolean, nullable=False, default=False)

클래스를 생성함으로써 데이터베이스 작업을 이 클래스를 통해서 할 수 있습니다. 예로, 테이블의 모든 컬럼을 확인하는 작업을 진행할 때 아래와 같이 클래스 프로퍼티를 사용하는 방식으로 진행할 수 있어요.

SELECT * From todos
Todo.query.all()

이 같은 방식으로 작업했을 때 여러 이점이 있습니다.

  • 데이터베이스 시스템마다 SQL 작성 법이 조금씩 다를 수 있습니다. SQLAlchemy ORM으로 어떠한 데이터베이스 시스템에 연결했는지 신경쓰지 않아도 됩니다.
  • 쿼리문 대신 파이썬 함수를 이용할 수 있어요.
  • 쿼리를 직접 작성하지 않기 때문에, 이 부분에서 발생할 수 있는 에러를 줄일 수 있어요.
  • 개발 모드에서 SQL lite와 같은 가벼운 데이터베이스로 작성하고, 후에 Postgres와 같은 DB로 변경할 수 있습니다.
일반적으로 ORM을 주로 사용하지만, 데이터베이스와 상호작용 하는 방법에 대한 여러 논의도 있습니다. psycopge2와 같이 데이터베이스에 직접 명령하는 방법도 있고, ORM을 사용하는 방법도 있습니다. 이 둘의 중간 단계인 Ouery Builder도 있으니 참고하세요. 이에 대한 관련 글의 링크입니다.

(https://blog.logrocket.com/why-you-should-avoid-orms-with-examples-in-node-js-e0baab73fa5)

이와 같이 SQLAlchemy는 DBAPI 위에 추상회된 레이어를 제공하기 때문에 좀 더 편리하게 데이터베이스 연동을 할 수 있어요.

SQLAlchemy를 사용하면 어떠한 데이터베이스 시스템을 사용하는지에 대해 고민할 필요가 없어진다고 했는데, 이에 대한 작업을 Dialect 층에서 처리해 줍니다.

Connection Pool에서는 DBAPI를 직접 사용할 때 다루어야 했던 connection을 처리해 줍니다. Connection을 재사용하기 때문에 데이터베이스에 수정을 할 때마다 새로이 connection을 생성하는 것을 방지해 줍니다. 또한, 성능에 영향을 줄 수 있는 작은 요청을 계속 보내는 작업을 방지해 주기도 합니다.

Engine

Engine은 Dialect, Connection Pool과 더불어 주요 레이어 중 하나이며, 가장 낮은 단계의 레이어입니다. 데이터베이스 연동 작업을 하며 psycopg2와 유사한 방식으로 작동합니다.

SQL Expression

엔진과 같이 직접 쿼리문 작성을 원치 않다면, SQL Expression을 이용해 Python Object로 대신할 수 있습니다.

이렇게 SQLAlchemy를 이용해 여러 방법으로 데이터베이스와 연동할 수 있습니다. 쿼리문을 Engine에 직접 전달할 수도 있고, 쿼리문 작업 없이 Python 객체를 이용할 수도 있어요.

ORM

좀 더 고차원적인 또다른 방법으로 ORM을 이용할 수 있습니다. ORM을 이용하면 데이터베이스의 테이블과 파이썬 객체를 맵핑해 사용할 수 있습니다.

Flask with SQLAlchemy

SQLAlchemy와 Flask를 연결해 Hello World 앱을 만들어 보아요. 이를 위해 Flask와 Flask SQLAlchemy를 설치해야해요.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# (1)
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://eric@localhost:5432/example"
db = SQLAlchemy(app) # (2)

@app.route("/")
def index():
  return "Hello World!"

Flask-SQLAlchemy를 이용해 Flask 앱과 SQLAlchemy를 연결하는 작업이 필요해요. (1) 부분에서 데이터베이스와 연결을 위한 설정을 추가하고, (2)부분에서 연동하도록 합니다. 데이터베이스 URL의 각 부분에 적합한 정보를 추가해 주어야 하는데, 아래 사진을 참고해 주세요.

Flask 앱 내에서 Database 수정을 위해 SQLAlchemy 라이브러리가 반환한 db 사용할 수 있어요. 여기서 db는 데이터베이스의 인스턴스 입니다. db는 데이터베이스와 상호 작용을 위한 인터페이스를 제공합니다. db.Model을 이용해 데이터 모델을 생성하거나 수정할 수 있고, db.session으로는 transaction을 관리할 수 있어요.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://merryukulele@localhost:5432/example"
db = SQLAlchemy(app)

class Person(db.Model):
  __tablename__ = 'persons'
  id = db.Column(db.Integer, primary_key = True)
  name = db.Column(db.String(), nullable = False)

  def __repr__(self):
    return f'<Person ID: {self.id}, name: {self.name}>'

db.create_all()

@app.route("/")
def index():
  person = Person.query.first()
  return "Hello " + person.name

테이블 생성을 위한 Person 클래스를 생성하고 db.Model을 상속합니다. 이를 통해 클래스를 데이터베이스 테이블과 연동할 수 있어요. 그리고, create_all() 메소드를 호출하는데 기존의 테이블이 존재하지 않으면, 새로운 테이블을 생성합니다.

기본적으로 테이블 명은 lowercase 버전의 클래스명을 사용하지만, 필요한 경우 명시적으로 지정해 줄 수도 있습니다.

SQL Queries

아래의 테이블은 왼쪽부터 동물 캐릭터의 이름과 사진에 대한 테이블, 캐릭터 사진에 대한 인기 투표 테이블이 있습니다. 두 번째 테이블의 첫 row의 3은 winner 컬럼에도 속해있습니다. 첫번째 테이블의 3과 두번째 테이블 첫 째 row의 3은 같지만 각 테이블에서 의미하는 바는 다릅니다. 이렇게 두 테이블 간 관계를 Join해서 생성한 테이블이 세 번째 테이블입니다. 데이터베이스의 join statement를 통해 한 테이블의 row와 다른 테이블의 row를 어떻게 연관지을지 나타낼 수 있습니다.

Aggregation

Aggregation을 통해 테이블의 여러 row를 하나의 row로 나타낼 수 있습니다. 아래의 예시는 animal 테이블의 count aggregation에 대한 결과입니다. species 컬럼의 값을 그룹핑 한 결과를 출력하고 있습니다. 바꿔말하면, 컬럼 내 같은 값을 가진 row의 count 값을 나타냅니다.

관계형 데이터베이스에서 query에 대한 다수의 데이터가 있을때, 일반적으로 여러 row에 걸쳐서 존재합니다. column을 더 생성하지 않는 이유는 테이블을 생성할 때 얼마나 많은 column을 만들어 놔야 할 지 미리 알기 어렵기 때문이에. 또한, 여러 row에 걸쳐 있으면 count aggregation과 같은 툴을 이용할 수 있습니다.

// Elements of SQL 11, 12, 13, 14, 15

SQLAlchemy ORM

SQLAlchemy Object Lifecycle

데이터베이스에 새로운 행을 추가하려 한다면 아래와 같은 명령어를 입력해야해요.

person = Person(name = 'Amy')
db.session.add(person)
db.session.commit()

데이터베이스에 수정할 작업을 하고 commit을 호출해야 수정한 작업이 데이터베이스에 반영되고 존속돼요.

수정을 한다고 즉각 반영이 되지 않고 몇 가지 단계를 거치게 되는데, 이는 commit 전 실수나 에러가 발생할 경우 취소할 수 있도록 하기 위해서에요. 그럼 여기서 적용되는 Object Life Cycle에 대해 확인해 보아요. Life Cycle에는 Transient, Pending, Flushed, Commited 단계가 있습니다.

우선 객체를 정의 하면 아직 session과 연동된 상태가 아닌 trasient stage 단계에 머물게 됩니다. 이후 session.add, session.add_all과 같은 작업을 하면 pending 상태가 되어요. 여기서는 아직 flush 상태 전이기 때문에 필요에 따라 rollback을 할 수 있습니다.

Flush 상태는 데이터베이스에 commit할 준비가 된 단계를 말해요. 데이터베이스에 가할 수정 작업을 엔진이 이해하도록 SQL 명령어로 변환하는 작업이 이 단계에서 이루어집니다. 이렇게 번들된 SQL 명령어는 connection pool에서 특정 connection에 전달되고, Dialect에서 특정 데이터베이스에 맞도록 처리 후 DBAPI에서 작업이 이루어 집니다.

디비 작업을 하며 현재 어떠한 단계의 라이프 사이클에 있는지 인지하고 있어야 해요. transient stageㅇ고 session에 추가되길 기다리고 있는 것인지, 현재 pending 상태이고 rollback 또는 flush가 되길 원하는지 인지함으로써 현재의 데이터베이스 상태에 대해 알 수 있습니다..

Building a CRUD App

SQLAlchemy, psycopg2를 이용해 데이터베이스 연동 작업에 대해 알아보았어요. 이를 바탕으로 간단한 Todo App을 만들어 보려 해요.

CRUD App이란, 유저 관점에서 어떠한 작업을 할 때 데이터베이스에서 특정 데이터를 꺼내 읽을 수 있고 수정, 추가 삭제를 할 수 있도록 하는 애플리케이션이에요.

우선 앞서 작성했던 로직에서 Todo 테이블을 생성할거에요. 그리고, 기본 라우트를 통해 todo 리스트를 유저가 볼 수 있도록 클라이언트에 html response를 보내는 코드를 작성해 보아요.

from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "postgres://merryukulele:@localhost:5432/todoapp"
db = SQLAlchemy(app)

class Todo(db.Model):
  __tablename__ = 'todos'
  id = db.Column(db.Integer, primary_key=True)
  description = db.Column(db.String(), nullable=False)

  def __repr__(self):
    return f'<Todo {self.id} {self.description}>'

db.create_all()

@app.route("/")
def index():
  return render_template("index.html", data=Todo.query.all())

Model View Controller (MVC)

데이터베이스에 있는 Todo 테이블의 데이터를 읽어서 유저에게 보여주니 CRUD 중 "R"에 해당하는 부분이네요. 데이터베이스가 업데이트 되면 유저가 보는 화면도 그에 따라 업데이트가 될 거에요.

간단하고 기본적인 앱이지만 여기에서도 MVC 패턴을 확인해 볼 수 있어요. MVC 패턴은 애플리케이션을 Model, View, Controller 세 개의 레이어로 나누어 생각하는 프로그래밍 패턴이에요.

Model과 View는 직접 소통 하지 않고 Controller가 이를 관장합니다. Controller에서 View, Model이 어떠한 로직을 진행하고 어떻게 연계 작업을 할 지 처리를 합니다. 유저가 웹사이트를 방문 및 클릭을 해 서버에 어떠한 요청을 하면, 해당 request는 서버의 특정 route로 흘러가고 controller가 request에 대한 처리를 합니다.

위 코드 기준으로 MVC 패턴을 살펴보면, 렌더된 index.html이 view에 해당하고, index 함수가 controller가 될 수 있어요. Model에게 필요한 작업을 요청하고 view를 통해 응답을 보내고 있습니다. Controller에서 Model Update를 하게 되면, 유저가 보는 View 또한 업데이트 하는 경우가 많습니다. View가 Model에 Binding된 화면을 보여주고 있고, 업데이트 된 최신 정보를 보여주기 위해서죠.

이 패턴으로 프로그래밍 시 장점은 관심을 분리할 수 있고, 버그가 발생 했을 때 어느 레이어에서 발생한 것일지 스스로 유추해 볼 수 있을거에요.

그럼 이 패턴에 따라 모델을 먼저 형성하고, 이후 View가 어떻게 보여져야 할지 확인해 볼게요. 그리고, Controller가 앱 각 부분에 대한 연동 작업을 진행해 보겠습니다.

handling user input

데이터베이스의 정보를 Creat, Update, Delete 하기 위해서는, 그에 대한 유저가 입력한 정보가 있어야 해요. Controller에서 유저 입력값을 확인 후 Model을 수정합니다. Model에서 특정 작업이 완료 후, 업데이트된 정보를 유저에게 어떻게 보여줄지 Controller에 정합니다.

getting user data using Flask

일반적으로 뷰에서 유저가 어떠한 방식으로 정보를 입력했는지에 따라, 서버에 전달하는 방법은 세 가지가 있어요.

  • URL query parameters
  • Forms
  • JSON

URL query parameter는 URL 뒤에 ?마크와 함께 key-value pair로 데이터를 전달하는 방식이에요. 간단히 정보를 전달할 수 있지만, 정보가 좀 더 많은 객체를 전달하고자 할 때는 JSON을 이용하는 것이 더 좋습니다. Flask 앱에세 request.args를 통해 파라미터에 접근할 수 있습니다.

value = request.args.get("field1")

Form의 기본적인 방식으로 뷰에서 전달하면 form 객체를 통해 데이터에 접근할 수 있어요. 이 방식은 다소 전통적인 방식이며, 실제 제품에서는 JSON을 더 많이 이용합니다.

username = request.form.get("username")

뷰에서 전달하는 데이터의 datatype이 application/json인 경우 Flask 앱에서는 아래와 같이 데이터를 확인할 수 있습니다.

data_string = request.data
data_dictionary = json.loads(data_string)

유저가 Form을 이용해 뷰에서 Todo 정보를 입력하면, controller에서 이를 처리하는 로직을 아래와 같이 작성할 수 있어요. 이렇게 우리는 MVC 패턴 흐름을 만들어 보았습니다.

@app.route("/todos/create", methods=["POST"])
def create_todo():
  description = request.form.get("description", "")
  todo = Todo(description=description)
  db.session.add(todo)
  db.session.commit()
  return redirect(url_for("index"))

using sessions in controller

앱에서 데이터베이스에 정보 저장을 위해 commit을 하면, commit이 실패하는 케이스가 발생할 수 있어요. 일반적으로 실패하는 경우를 대비해 session을 롤백하고 close하는 작업 또한 추가해 줍니다.

@app.route("/todos/create", methods=["POST"])
def create_todo():
  error = False
  body = {}
  try:
    description = request.form.get("description", "")
    todo = Todo(description=description)
    db.session.add(todo)
    db.session.commit()
    body["description"] = todo.description
  except:
    error = True
    db.session.rollback()
    print(sys.exc_info())
  finally:
    db.session.close()
  if error:
    abort(400)
  else:
    return jsonify(body)

Handling changes to our data schema over time

Modeling relationships between objects in our web application

Implementing Search

Migration

What is migration?

애플리케이션 개발을 하다보면 도중에 데이터 스키마가 변경될 수 있습니다. 이미 데이터가 존재하는 상태에서 새로운 컬럼을 추가하거나, 테이블을 생성하는 등의 작업이 필요할 수 있습니다. 개발 도중 이러한 작업을 진행하기 위해서 데이터 마이그레이션에 대해 알아야 해요. 코드를 작성하거나 게임을 하는 중간에 저장을 하듯이, 데이터베이스 관련 작업을 일정 부분 진행했고 현재 시점을 기준으로 저장하고 싶다면 마이그레이션을 진행해야 합니다.

데이터베이스의 작은 실수가 앱 전체 동작에 영향을 줄 수 있기 때문에, 이에 대한 수정 사항을 관리하는 것은 매우 중요합니다. 스키마 관련 실수가 생겼다면, 빠르게 온전했던 이전의 상태로 롤백할 수 있어야 합니다. Schema migration은 데이터베이스 스키마에 대한 수정 사항을 추적 관리하는 파일로, git commmit과 같이 생각해 볼 수 있어요.

마이그레이션은 데이터베이스 스키마에 대한 변화를 캡슐화 해서, 일반적으로 애플리케이션의 저장소 내 로커 파일로 저장합니다. 보통 migrations 폴더 내에 데이터베이스에 수행한 작업 단위 별로 유니크한 이름을 가진 파일을 생성해서 관리합니다.

Why use migrations?

마이그레이션이 없다고 가정한다면, 테이블 생성 후 수정 작업을 한 후 이를 적용시키기 위해 기존의 테이블을 drop하고 새로이 생성을 해야할 거에요. 이러한 경우 기존에 저장된 데이터도 잃을 뿐만 아니라 작은 수정 사항에도 테이블 자체를 새로 생성해야만 적용을 할 수 있습니다. 마아그레이션 라이브러리를 사용하면 변화된 부분만 감지해 기존의 데이터 스키마와 데이터는 보존하면 필요한 부분만 수정을 할 수 있습니다.

Flask migration

앞서 작성한 Flask 앱에서 migration을 추가해 보려 합니다. Flask-Migrate 설치가 필요한데,

pip3 install Flask-Migrate

명령어로 설치를 할 수 있습니다. Flask-Migrate은 Flask앱과 Alembic이라는 데이터베이스 마이그레이션 툴을 연결해 주는 역활을 하고, 실제 마이그레이션 관련 작업은 Alembic을 통해 진행됩니다. 설치를 했으면, 앞서 작성한 Todo 앱에 Migration 클래스를 import 후 Flask앱과 SQLAlchemy 데이터베이스를 연결해 주는 작업을 진행합니다.

from flask_migrate import Migrate

migrate = Migrate(app, db)

그래고 아래 flask db init 명령어를 입력하면 프로젝트 폴더에 migrations 폴더가 생성됩니다.

migrations 폴더 내 versions 폴더에는 migration script이 저장이 되고 alembic.ini 파일에서는 마이그레이션 관련 설정을 할 수 있습니다. Database를 새로 생성한 초기 상태에서 flask db migrate 명령어를 입력하면 migrate이 진행되고 아래와 같은 로그를 출력합니다.

출력 내용을 살펴보면, SQLAlchemy 모델을 검색 후 새로운 Todo 테이블을 발견하고 versions 폴더에 새로운 migration 파일을 생성합니다.

메모

sql 명령은 몇 개의 구로 구성된다.

WHERE 구와 SELECT 구의 내부처리 순서 표준 SQL에는 내부처리 순서가 따로 정해져 있지 않습니다. 하지만, WHERE구 => SELECT구의 순서로 내부 처리를 하는 데이터베이스가 많습니다. 하지만, WHERE구 -> SELECT 구 순서로 내부 처리하는 데이터베이스가 많습니다. alias를 사용하면 SELECT 구에서 지정되기 때문에 WHERE 구에서는 아직 지정되지 않은 상태라 에러가 발생합니다. 반면 ORDER BY는 SELECT 구 이후에 처리되기 때문에 여기서는 alias를 사용할 수 있습니다.

문자세트, 인코드 방식에 따라 저장공간 달라짐 OCTET_LENGTH

169p 데이터베이스 제품에 따라 처리 방식이 다름.

UPDATE table1 SET no = no + 1, a = no;
UPDATE table1 SET a = no, no = no + 1;

Resources

SQLAlchemy

Flask

Migration