파이썬에서 SQL Formatting 하는 법을 자꾸 잊어버립니다 (ORM 최고야)

Contents


  1. Prerequisite
  2. SQL multiline style
  3. SQL paramstyle

Prerequisite

💡 코딩 스타일은 항상 민감한 문제이므로 '', "", ''', """ 에 대해서
이것이 옳다! 라는 소모적인 논쟁은 정중히 사양하겠습니다 🙇‍♂️
(개인적으로는 "He's a man" 같은 문장을 위해, double quote 가 좋다고 생각합니다)

Multiline String in Python


파이썬에서 긴 문자열을 여러줄에 걸쳐 작성하는 방법이 몇 가지 있습니다
얘기를 시작하기 전에, SQL 문자열은 double quote 로 감싸는 방법을 권장합니다

  1. Python 은 '' 와 "" 중 어떤것을 사용해도 관계 없다고 제안하고 있습니다 (일관성만 있다면)
  2. ANSI SQL 에서 ``, "" 는 같은 의미이므로, string literal 표현은 '' 를 권장합니다
  3. WHERE username = 'scott' 과 같이 표현해야 하므로, "" 로 감싸는 것이 더 바람직 합니다
    (escaping 을 해서라도 '' 를 사용하겠다 하신다면 어쩔 수 없지만 킹독성 갓독성이 떨어집니다)

String format


string format 방법은, 기가 막힌 설명이 있기에 제가 다시 정리하지는 않겠습니다.
%(printf style), .format(), f-string 에 대한 내용입니다

SQL multiline style

교통정리를 마쳤으니 Python 에서 SQL statement 을 알아봅니다
SQL statement 를 작성하는 방법은 세 가지 정도가 있습니다
(여기 쓰지 않은 다른 방법도 얼마든지 가능합니다. 문자열만 만들면 되니까요)

with parentheses


소괄호를 사용하여, 여러 줄에 걸쳐 긴 문자열을 완성할 수 있습니다

>>> sql = (
...     "SELECT * "
...     "FROM user "
...     "WHERE username = 'scott'"
... )

>>> print(sql)
SELECT * FROM user WHERE username = 'scott'

아쉬운 점은, SELECT, FROM 끝에 공백을 넣어줘야 한다는 점 입니다
그렇지 않으면 SELECT *FROM userWHERE username = 'scott' 이렇게 됩니다
(ProgrammingError 가 발생합니다)

with line break character


>>> sql = "SELECT * " \
...       "FROM user " \
...       "WHERE username = 'scott'"

>>> print(sql)
SELECT * FROM user WHERE username = 'scott'

소괄호로 묶는 방법과 유사합니다만 나중에 문자열을 더 추가하면서
문자열 맨 마지막의 escaping character 를 잊어버릴 수 있으니 소괄호로 묶는게 더 나을듯 합니다
(저만의 걱정일수 있으니 항상 스타일은 자유롭게... ㅇㅅㅇ)

마찬가지로, 맨 끝에 공백을 넣어줘야 하는 점이 아쉽습니다

with triple (single) quotes


docstring 을 작성할 때 많이 사용되지만, 문자열로 지정할 수 있습니다
triple quotes 스타일에서, 첫 문장을 붙여서 또는 줄 바꿈 하여 시작할지에 대한 고민은
오픈소스의 docstring 작성 방법이 각각 다르니, '', "" 의 문제처럼 일관성만 맞추면 된다 생각합니다

>>> sql = """
...     SELECT *
...     FROM user
...     WHERE username = 'scott'
... """

>>> print(sql)

    SELECT *
    FROM user
    WHERE username = 'scott'

print(sql) 을 하게 되면, 첫 줄은 줄 바꿈(\n), 이후에는 indentation(\t) 이 유지됩니다
위 두 가지 스타일과 다르게, 별도의 공백 처리가 필요하지 않다는 점이 좋아 보입니다

아래의 paramstyle 을 설명하면서는 triple quote 형태를 사용하도록 하겠습니다
저는 보통 이렇게 사용하기 때문이죠 🐶

SQL paramstyle

💡 SQL paramstyle 에 맞춰 parameterize 를 해야 하는 이유는, SQL injection 을 막기 위함 입니다
더불어, paramstyle 이라는 단어는 PEP-0249#paramstyle 부분에서 차용했습니다.

처음부터 완성된 문자열을 만들어서 실행할 수도 있지만
보통은 변수를 받아, 그 값을 SQL 문에 할당하여 실행하곤 합니다

def get_user(user_id: int):
    sql = '''
        SELECT *
        FROM user
        WHERE id = %s
    '''
    cursor.execute(sql, user_id)    
    return cursor.fetchone()

지금은 바로 저 id = %s 를 어떻게 만들 것인가에 대한 내용을 정리하려고 합니다
매 번 작성할 때마다 헷갈리기 때문이죠 😭

PEP-0249 에는 아래와 같이 다양한 방법으로 작성할 수 있다고 설명합니다

paramstyle Meaning
qmark Question mark style, e.g. ...WHERE name=?
numeric Numeric, positional style, e.g. ...WHERE name=:1
named Named style, e.g. ...WHERE name=:name
format ANSI C printf format codes, e.g. ...WHERE name=%s
pyformat Python extended format codes, e.g. ...WHERE name=%(name)s

그리고 Python 에서 사용할 수 있는 각각의 DB API 에 따라
권장하는 방식을 직접 확인할 수 있습니다 (https://wiki.python.org/moin/DbApiFaq)

# MySQL
>>> pymysql.paramstyle
'pyformat'
>>> MySQLdb.paramstyle
'format'
>>> mysql.connector.paramstyle
'pyformat'

# Postgresql
>>> psycopg2.paramstyle
'pyformat'

# MS-SQL
>>> pymssql.paramstyle
'pyformat'

# Oracle
>>> cx_Oracle.paramstyle
'named'

# Sqlite3
>>> sqlite3.paramstyle
'qmark'

# Infomix
>>> IfxPy.paramstyle
'qmark'

다 찾아보느라 힘들었습니다... 대표적인 것만 추려봤어요
는 미친 이런 사이트가 있네요 Python-wiki#DbApiModuleComparison
(대체 뭘 한거지...)

사용하는 DB API 에 따라서, 필요한 paramstyle 을 적용하면 됩니다

대표적으로 많이 사용하는 pymysql 을 이용하여 pyformat
어떻게 올바르게 사용할 수 있는지 알아보겠습니다

insert_stmt = (
  "INSERT INTO employees (emp_no, first_name, last_name, hire_date) "
  "VALUES (%s, %s, %s, %s)"
)
data = (2, 'Jane', 'Doe', datetime.date(2012, 3, 23))
cursor.execute(insert_stmt, data)

select_stmt = "SELECT * FROM employees WHERE emp_no = %(emp_no)s"
cursor.execute(select_stmt, {'emp_no': 2 })

dev.mysql.com 에서 안내하는 Python Developer Guide 예제 입니다

insert_stmt 에서 처럼, format 스타일을 사용할 수도 있고,
select_stmt 에서 처럼, pyformat 스타일을 사용할 수도 있습니다

format 스타일은, tuple(또는 list)로 값을 전달하고
pyforamt 스타일은, dict 형태의 값으로 전달 되어야 합니다

pyformat 에 tuple(또는 list) 스타일로 값을 전달하면
TypeError: format requires a mapping 와 같은 에러가 발생합니다

반대로, format 에 dict 형태로 값을 전달하면
dict 값이 문자열로 치환 되면서 pymysql.err.ProgrammingError 에러가 발생합니다

마치며


다시 한 번, SQLAlchemy, Django ORM 에 고마움을 느낍니다 (?)
각 DB API 별로 지원하는 paramstyle 이 있다는 걸 알아낸 것만으로도 큰 도움이 되었습니다
다음에 기회가 된다면, 여러 줄의 쿼리문을 실행하는 방법이나
한 번에 여럿을 insert 하는 방법도 정리해 보려고 합니다... 언젠가

긴 글 읽어주셔서 감사합니다. 지적은 언제나 환영입니다