[python] MySQLdb를 사용하여 커서를 닫는 경우

WSGI 웹 앱을 만들고 있는데 MySQL 데이터베이스가 있습니다. 문을 실행하고 결과를 얻기위한 커서를 제공하는 MySQLdb를 사용하고 있습니다. 커서를 가져오고 닫는 표준 방법은 무엇입니까? 특히 커서는 얼마나 오래 지속되어야합니까? 각 트랜잭션에 대해 새 커서를 가져와야합니까?

커밋하기 전에 커서를 닫아야한다고 생각합니다. 중간 커밋이 필요하지 않은 트랜잭션 집합을 찾아서 각 트랜잭션에 대해 새 커서를 가져올 필요가없는 중요한 이점이 있습니까? 새 커서를 가져 오는 데 많은 오버 헤드가 있습니까, 아니면 큰 문제가 아니십니까?



답변

종종 불분명하고 주관적이기 때문에 표준 관행이 무엇인지 묻는 대신 모듈 자체에서 지침을 찾아 볼 수 있습니다. 일반적으로with 으로 다른 사용자가 제안한 키워드를 것은 좋은 생각이지만 이러한 특정 상황에서는 기대하는 기능을 충분히 제공하지 못할 수 있습니다.

모듈 버전 1.2.5 부터 다음 코드 ( github )로 컨텍스트 관리자 프로토콜MySQLdb.Connection구현합니다 .

def __enter__(self):
    if self.get_autocommit():
        self.query("BEGIN")
    return self.cursor()

def __exit__(self, exc, value, tb):
    if exc:
        self.rollback()
    else:
        self.commit()

with이미 몇 가지 기존 Q & A가 있거나 Python의 “with”문 이해 를 읽을 수 있지만 기본적으로 발생하는 일은 블록 __enter__의 시작 부분에서 실행되고 with블록 __exit__을 떠날 때 실행됩니다 with. 나중에 해당 객체를 참조하려는 경우 선택적 구문 with EXPR as VAR을 사용하여에서 반환 된 객체 __enter__를 이름 에 바인딩 할 수 있습니다 . 따라서 위의 구현에서 데이터베이스를 쿼리하는 간단한 방법은 다음과 같습니다.

connection = MySQLdb.connect(...)
with connection as cursor:            # connection.__enter__ executes at this line
    cursor.execute('select 1;')
    result = cursor.fetchall()        # connection.__exit__ executes after this line
print result                          # prints "((1L,),)"

이제 질문은 with블록 을 종료 한 후 연결 및 커서의 상태는 무엇 입니까? __exit__통화 만 위에 표시 방법 self.rollback()이나 self.commit(), 어느 쪽도 아니 그 방법은 전화로 이동 close()방법. 커서 자체에는 __exit__정의 된 메서드 가 없습니다 with. 연결 만 관리 하기 때문에 그렇게하더라도 상관 없습니다 . 따라서 연결과 커서는 모두 with블록 을 종료 한 후에도 열린 상태로 유지 됩니다. 위의 예에 다음 코드를 추가하면 쉽게 확인할 수 있습니다.

try:
    cursor.execute('select 1;')
    print 'cursor is open;',
except MySQLdb.ProgrammingError:
    print 'cursor is closed;',
if connection.open:
    print 'connection is open'
else:
    print 'connection is closed'

stdout에 “커서가 열려 있고 연결이 열려 있습니다.”라는 출력이 표시되어야합니다.

커밋하기 전에 커서를 닫아야한다고 생각합니다.

왜? MySQL의 C API 의 기초가, MySQLdb어떤 커서 객체를 구현하지 않습니다, 모듈 문서에 묵시적으로 : “MySQL은 커서를 지원하지 않습니다하지만, 커서 쉽게 에뮬레이트합니다.” 실제로 MySQLdb.cursors.BaseCursor클래스는 object커밋 / 롤백과 관련하여 커서 에서 직접 상속 되며 커서에 이러한 제한을 적용하지 않습니다. 오라클 개발자 는 다음과 같이 말했습니다 .

cur.close () 전에 cnx.commit ()은 나에게 가장 논리적으로 들립니다. “더 이상 필요하지 않으면 커서를 닫으십시오.”라는 규칙을 따를 수 있습니다. 따라서 커서를 닫기 전에 commit (). 결국 Connector / Python의 경우 큰 차이는 없지만 다른 데이터베이스는 그럴 수 있습니다.

이 주제에 대한 “표준 연습”에 도달하는 것만큼이나 비슷할 것으로 예상합니다.

중간 커밋이 필요하지 않은 트랜잭션 집합을 찾아서 각 트랜잭션에 대해 새 커서를 가져올 필요가없는 중요한 이점이 있습니까?

나는 그것을 매우 의심하며 그렇게하려고 노력할 때 추가적인 인적 오류가 발생할 수 있습니다. 컨벤션을 결정하고 그것에 충실하는 것이 좋습니다.

새 커서를 가져 오는 데 많은 오버 헤드가 있습니까? 아니면 큰 문제가 아니십니까?

오버 헤드는 무시할 수 있으며 데이터베이스 서버에 전혀 영향을주지 않습니다. 전적으로 MySQLdb 구현 내에 있습니다. github 에서 볼 수 있습니다.BaseCursor.__init__새 커서를 만들 때 무슨 일이 일어나는지 정말 궁금하다면 .

논의 할 때 이전으로 돌아 가면 with아마도 이제 MySQLdb.Connection클래스 __enter____exit__메서드가 매번 새로운 커서 객체를 제공 하는 이유를 이해할 수있을 것입니다.with 블록 하고이를 추적하거나 블록 끝에서 닫는 것을 귀찮게하지 않는 입니다. 상당히 가볍고 순전히 편의를 위해 존재합니다.

커서 객체를 미세 관리하는 것이 정말 중요하다면 contextlib.closing 을 사용 하여 커서 객체에 정의 된 __exit__메서드 가 없다는 사실을 보완 할 수 있습니다 . 이를 위해 with블록 을 종료 할 때 연결 개체를 강제로 닫는 데 사용할 수도 있습니다 . 그러면 “my_curs is closed; my_conn is closed”가 출력됩니다.

from contextlib import closing
import MySQLdb

with closing(MySQLdb.connect(...)) as my_conn:
    with closing(my_conn.cursor()) as my_curs:
        my_curs.execute('select 1;')
        result = my_curs.fetchall()
try:
    my_curs.execute('select 1;')
    print 'my_curs is open;',
except MySQLdb.ProgrammingError:
    print 'my_curs is closed;',
if my_conn.open:
    print 'my_conn is open'
else:
    print 'my_conn is closed'

참고 with closing(arg_obj)인수 객체의 호출하지 않습니다 __enter____exit__방법; 그것은 것입니다 인수 객체의 호출 close의 끝에 방법을 with차단합니다. (단순히 클래스 정의, 행동이를 보려면 Foo__enter__, __exit__close방법은 간단한 포함 print문을, 그리고 당신이 할 때 발생하는 비교 with Foo(): pass당신이 할 때 발생에with closing(Foo()): pass .)이 두 가지 중요한 의미가 있습니다 :

첫째, 자동 커밋 모드가 활성화되면 MySQLdb는 블록 끝에서 트랜잭션 BEGIN을 사용 with connection하고 커밋하거나 롤백 할 때 서버에서 명시 적 트랜잭션을 수행합니다 . 이는 모든 DML 문을 즉시 커밋하는 MySQL의 기본 동작으로부터 사용자를 보호하기위한 MySQLdb의 기본 동작입니다. MySQLdb는 컨텍스트 관리자를 사용할 때 트랜잭션을 원하고 명시 적 코드를 코드에 사용하고 트랜잭션 무결성을 잃는다 고 가정합니다. 변경 사항을 롤백 할 수없고 동시성 버그가 나타나기 시작할 수 있으며 그 이유가 즉시 명확하지 않을 수 있습니다.BEGIN 하여 서버의 자동 커밋 설정을 우회합니다. 을 사용하는 데 익숙하다면 with connection실제로 우회되었을 때 자동 커밋이 비활성화되었다고 생각할 수 있습니다. 추가하면 불쾌한 놀라움을 얻을 수 있습니다.closing

둘째, with closing(MySQLdb.connect(user, pass)) as VAR바인드 접속 대상물VAR대조적으로, with MySQLdb.connect(user, pass) as VAR결합, 새로운 커서 개체 로이 VAR. 후자의 경우 연결 개체에 직접 액세스 할 수 없습니다! 대신 connection원래 연결에 대한 프록시 액세스를 제공 하는 커서의 속성 을 사용해야합니다 . 커서가 닫히면 해당 connection속성이로 설정됩니다 None. 이로 인해 다음 중 하나가 발생할 때까지 계속 유지되는 연결이 끊어집니다.

  • 커서에 대한 모든 참조가 제거됩니다.
  • 커서가 범위를 벗어납니다.
  • 연결 시간이 초과되었습니다.
  • 연결은 서버 관리 도구를 통해 수동으로 닫힙니다.

다음 행을 하나씩 실행하는 동안 열린 연결을 모니터링하여 (워크 벤치에서 또는를 사용하여SHOW PROCESSLIST ) 이를 테스트 할 수 있습니다 .

with MySQLdb.connect(...) as my_curs:
    pass
my_curs.close()
my_curs.connection          # None
my_curs.connection.close()  # throws AttributeError, but connection still open
del my_curs                 # connection will close here


답변

‘with’키워드를 사용하여 다시 작성하는 것이 좋습니다. ‘With’는 커서 닫기를 자동으로 처리합니다 (관리되지 않는 리소스이므로 중요 함). 이점은 예외의 경우 커서를 닫을 것입니다.

from contextlib import closing
import MySQLdb

''' At the beginning you open a DB connection. Particular moment when
  you open connection depends from your approach:
  - it can be inside the same function where you work with cursors
  - in the class constructor
  - etc
'''
db = MySQLdb.connect("host", "user", "pass", "database")
with closing(db.cursor()) as cur:
    cur.execute("somestuff")
    results = cur.fetchall()
    # do stuff with results

    cur.execute("insert operation")
    # call commit if you do INSERT, UPDATE or DELETE operations
    db.commit()

    cur.execute("someotherstuff")
    results2 = cur.fetchone()
    # do stuff with results2

# at some point when you decided that you do not need
# the open connection anymore you close it
db.close()


답변

참고 :이 답변은 PyMySQL 에 대한 것입니다 . 이는 MySQLdb의 드롭 인 대체물이며 MySQLdb가 유지 관리를 중단 한 이후 사실상 MySQLdb의 최신 버전입니다. 여기에있는 모든 것이 레거시 MySQLdb 에도 해당 한다고 생각 하지만 확인하지는 않았습니다.

우선, 몇 가지 사실 :

  • Python의 with구문 __enter__with블록 본문을 실행하기 전에 컨텍스트 관리자의 메서드를 호출 하고 그 __exit__후에 는 메서드를 호출합니다 .
  • 연결에는 __enter__커서를 만들고 반환하는 것 외에 아무것도 수행하지 않는 __exit__메서드 와 커밋하거나 롤백 하는 메서드 (예외 발생 여부에 따라 다름)가 있습니다. 그것은 하지 않는 연결을 닫습니다.
  • PyMySQL의 커서는 순전히 Python으로 구현 된 추상화입니다. MySQL 자체에는 동등한 개념이 없습니다. 1
  • 커서에는 __enter__아무 작업도하지 않는 __exit__메서드 와 커서를 “닫는”메서드가 있습니다 (단지 부모 연결에 대한 커서의 참조를 무효화하고 커서에 저장된 모든 데이터를 버리는 것을 의미 함).
  • 커서는 자신을 생성 한 연결에 대한 참조를 보유하지만 연결은 자신이 만든 커서에 대한 참조를 보유하지 않습니다.
  • 연결에는 연결 __del__을 닫는 방법이 있습니다.
  • https://docs.python.org/3/reference/datamodel.html , CPython의 (기본 파이썬 구현) 참조 카운트를 사용하여 자동으로 참조 수가 0 안타 일단 객체를 삭제합니다.

이러한 것들을 종합하면 다음과 같은 순진한 코드가 이론적으로 문제가 있음을 알 수 있습니다.

# Problematic code, at least in theory!
import pymysql
with pymysql.connect() as cursor:
    cursor.execute('SELECT 1')

# ... happily carry on and do something unrelated

문제는 아무것도 연결을 끊지 않았다는 것입니다. 실제로 위 코드를 Python 셸에 붙여 넣은 다음 SHOW FULL PROCESSLISTMySQL 셸에서 실행 하면 생성 한 유휴 연결을 볼 수 있습니다. 연결의 MySQL의 기본 번호이기 때문에 151 하지 않은, 거대한 , 당신은 이론적으로 열어 이러한 연결을 유지하는 많은 프로세스가 있다면 문제로 실행을 시작할 수 있습니다.

그러나, CPython과에, 보장하지만 위 내 예를 들어 다음과 같은 코드가 있다고하는 구원의 은혜가 아마 당신이 열려있는 연결의 부하를 주변에두고 발생하지 않습니다는. 그 절약의 은혜는 cursor범위를 벗어나는 즉시 (예 : 생성 된 기능이 완료되거나 cursor다른 값이 할당 됨) 참조 수가 0이되어 삭제되고 연결의 참조 수가 삭제된다는 것입니다. 0으로 설정하면 연결 __del__을 강제 종료하는 연결 의 메서드가 호출됩니다. 위의 코드를 Python 셸에 이미 붙여 넣었다면 이제 다음을 실행하여 시뮬레이션 할 수 있습니다 cursor = 'arbitrary value'. 이 작업을 수행하면 연 연결이 SHOW PROCESSLIST출력 에서 사라집니다 .

그러나 이것에 의존하는 것은 우아하지 않으며 이론적으로 CPython 이외의 Python 구현에서는 실패 할 수 있습니다. 이론적으로 더 깔끔한 .close()것은 연결 을 명시 적으로 만드는 것입니다 (파이썬이 객체를 파괴 할 때까지 기다리지 않고 데이터베이스에서 연결을 해제하는 것입니다). 이보다 강력한 코드는 다음과 같습니다.

import contextlib
import pymysql
with contextlib.closing(pymysql.connect()) as conn:
    with conn as cursor:
        cursor.execute('SELECT 1')

이것은 추악하지만 (사용 가능한 무제한 수의) 데이터베이스 연결을 해제하기 위해 객체를 파괴하는 Python에 의존하지 않습니다.

이미 이와 같이 명시 적으로 연결을 닫고있는 경우 커서 를 닫는 것은 전적으로 의미가 없습니다.

마지막으로 여기에있는 2 차 질문에 답하려면 :

새 커서를 가져 오는 데 많은 오버 헤드가 있습니까? 아니면 큰 문제가 아니십니까?

아니요, 커서를 인스턴스화하는 것은 MySQL에 전혀 영향을 주지 않으며 기본적으로 아무것도하지 않습니다 .

중간 커밋이 필요하지 않은 트랜잭션 집합을 찾아서 각 트랜잭션에 대해 새 커서를 가져올 필요가없는 중요한 이점이 있습니까?

이것은 상황에 따라 다르며 일반적인 답변을 제공하기 어렵습니다. 로 https://dev.mysql.com/doc/refman/en/optimizing-innodb-transaction-management.html 풋를, “이 초당 수천 번 저지른 경우 응용 프로그램의 힘 만남 성능 문제 및 다른 성능 문제가있는 경우 2-3 시간마다 커밋됩니다 . . 모든 커밋에 대해 성능 오버 헤드를 지불하지만 트랜잭션을 더 오래 열어두면 다른 연결이 잠금 대기 시간을 소비하고 교착 상태의 위험이 증가하며 잠재적으로 다른 연결에서 수행되는 일부 조회 비용이 증가 할 가능성이 높아집니다. .


1 MySQL 에는 커서를 호출하는 구조가 있지만 저장 프로 시저 내에 만 존재합니다. PyMySQL 커서와 완전히 다르며 여기서는 관련이 없습니다.


답변

모든 실행에 대해 하나의 커서를 사용하고 코드 끝에서 닫는 것이 더 나을 것이라고 생각합니다. 작업하기가 더 쉽고 효율성도 향상 될 수 있습니다 (저를 인용하지 마십시오).

conn = MySQLdb.connect("host","user","pass","database")
cursor = conn.cursor()
cursor.execute("somestuff")
results = cursor.fetchall()
..do stuff with results
cursor.execute("someotherstuff")
results2 = cursor.fetchall()
..do stuff with results2
cursor.close()

요점은 커서 실행 결과를 다른 변수에 저장할 수 있으므로 커서가 두 번째 실행을 수행 할 수 있도록 할 수 있다는 것입니다. fetchone ()을 사용하는 경우에만 이러한 방식으로 문제가 발생하고 첫 번째 쿼리의 모든 결과를 반복하기 전에 두 번째 커서를 실행해야합니다.

그렇지 않으면 커서에서 모든 데이터를 가져 오는 즉시 커서를 닫으십시오. 이렇게하면 나중에 코드에서 느슨한 끝을 묶는 것에 대해 걱정할 필요가 없습니다.


답변

나는 그것을 PHP와 mysql처럼 할 것을 제안한다. 첫 번째 데이터를 인쇄하기 전에 코드 시작 부분에서 i를 시작하십시오. 따라서 연결 오류가 발생하면 50x(내부 오류가 무엇인지 기억하지 못함) 오류 메시지를 표시 할 수 있습니다 . 그리고 전체 세션 동안 열어두고 더 이상 필요하지 않을 때 닫으십시오.


답변