1. 程式人生 > >Python下訪問MYSQL的方法總結

Python下訪問MYSQL的方法總結

  在Python下做過伺服器開發的小夥伴對ORM技術一定都不陌生,ORM(Object-Relational Mapping),將關係資料庫的表結構對映到物件上,隱藏了資料庫操作背後的細節,簡化了對資料操作的寫法,使得不懂SQL語法的人也可以快速開發,同時也避免了SQL注入的隱患。目前比較著名的ORM框架有Django中的ORM和SQLAlchemy(Flask中經常使用)。

  本文總結的Python下訪問MySQL的方法是通過原生的MySQL Driver來操作,非ORM,目前主要使用在我們的資料分析和ETL指令碼中。比較常用的兩個Driver是MySQLdb和mysql-connector,前者通過C來實現的,而後者是純Python實現,我沒有具體做過效能測試,但是從查閱的資料來看,MySQLdb要優於mysql-connector(參考:

Python MySQLdb vs mysql-connector query performance),所以下文所述的訪問方法都是通過MySQLdb來實現的,主要總結基礎訪問中的注意事項資料庫連線池使用中的兩個Warning問題

1.基礎訪問及注意事項

  資料庫的訪問無礙乎是"建立資料庫連線-->>執行操作-->>關閉連線"這樣的過程,對於MySQLdb的使用也是如此,基本如下:

import MySQLdb

# 連線資料庫
db = MySQLdb.connect(host="localhost", port=3307, user="joebob", passwd="moonpie", db="thangs")

cursor = db.cursor()

max_price=5
sql = """SELECT spam, eggs, sausage FROM breakfast WHERE price < %s"""

# 執行操作
cursor.execute(sql, (max_price,))

# 關閉連線
cursor.close()
db.close()

  MySQLdb提供了execute(query, args)和executemany(query, args)來執行SQL語句,後者主要用於多行插入。支援Parameterized Query,即將SQL語句與引數分離,在SQL語句中採用佔位符來佔位(關於Parameterized Query和Prepared Statement的說法有很多種)。注意幾點如下:

1) 使用%s來作為佔位符,示例如下:

c.execute("""SELECT spam, eggs, sausage FROM breakfast WHERE price < %s""", (max_price,))
c.executemany(
      """INSERT INTO breakfast (name, spam, eggs, sausage, price)
      VALUES (%s, %s, %s, %s, %s)""",
      [
      ("Spam and Sausage Lover's Plate", 5, 1, 8, 7.95 ),
      ("Not So Much Spam Plate", 3, 2, 0, 3.95 ),
      ("Don't Wany ANY SPAM! Plate", 0, 4, 3, 5.95 )
      ] )
2) 除了上述的'...WHERE name=%s'的格式,還支援'...WHERE name=%(name)s'的格式,此時需要使用map來作為引數。

2.連線池

在上述的操作中,每次訪問資料庫時,都需要發起連線請求,比較浪費資源,且訪問數量較多時,會對mysql的效能會產生較大的影響。因此,在實際使用中,通常會使用連線池技術,來實現資源複用。

  這裡主要使用DBUtils來實現連線池,DBUtils是一套用於管理資料庫連線池的包,為高併發的資料庫訪問提供更好的效能,可以自動管理連線物件的建立和釋放。常用的兩個外部介面是 PersistentDB 和 PooledDB,前者提供了單個執行緒專用的資料庫連線池,後者則是程序內所有執行緒共享的資料庫連線池。下面是一個基於DBUtils和MySQLdb的使用類,有需要者可以參考使用:

# -*- coding: utf-8 -*-

'''
    MySQL的處理庫
    解決兩個問題:1.連線池;2.公共insert/update/query介面
'''

import MySQLdb
from MySQLdb.cursors import DictCursor
from DBUtils.PooledDB import PooledDB

sql_settings = {'mysql': {'host': 'localhost', 'port': 3306, 'user': 'root', 'passwd': '123456', 'db': 'test'}}

class MySqlUtil(object):
    __pool = {}

    def __init__(self, conf_name='mysql'):
        self._conn = MySqlUtil.__get_conn(conf_name)
        self._cursor = self._conn.cursor()

        # Enforce UTF-8 for the connection.
        self._cursor.execute('SET NAMES utf8mb4')
        self._cursor.execute("SET CHARACTER SET utf8mb4")
        self._cursor.execute("SET character_set_connection=utf8mb4")

    @classmethod
    def __get_conn(cls, conf_name):
        if conf_name not in MySqlUtil.__pool:
            print 'create pool for %s' % conf_name
            MySqlUtilV2.__pool[conf_name] = PooledDB(creator=MySQLdb,
                                                     mincached=1, maxcached=20,
                                                     use_unicode=True, charset='utf8',
                                                     cursorclass=DictCursor,
                                                     **sql_settings[conf_name])
        return MySqlUtil.__pool[conf_name].connection()

    def close(self):
        if self._cursor:
            self._cursor.close()
        self._conn.close()

    # insert
    def insert_one(self, sql, value):
        return self._cursor.execute(sql, value)

    def insert_many(self, sql, values):
        return self._cursor.executemany(sql, values)

    # update
    def update(self, sql, param=None):
        return self._cursor.execute(sql, param)

    # query
    def fetch_all(self, sql, param=None):
        if param is None:
            count = self._cursor.execute(sql)
        else:
            count = self._cursor.execute(sql, param)
        if count > 0:
            result = self._cursor.fetchall()
        else:
            result = False
        return result

    def fetch_one(self, sql, param=None):
        if param is None:
            count = self._cursor.execute(sql)
        else:
            count = self._cursor.execute(sql, param)
        if count > 0:
            result = self._cursor.fetchone()
        else:
            result = False
        return result

    def fetch_many(self, sql, num, param=None):
        if param is None:
            count = self._cursor.execute(sql)
        else:
            count = self._cursor.execute(sql, param)
        if count > 0:
            result = self._cursor.fetchmany(num)
        else:
            result = False
        return result

3. 兩個Warning問題

  在訪問MySQL的過程中,遇到過很多問題,不僅有CRUD本身的問題,也有效能的問題等,這裡主要列舉其中兩個問題,也跟上面的程式碼有關係,希望能對遇到同樣問題的人起到幫助。 1) Warning: Incorrect string value [問題]   插入資料時,出現如下Warning,導致這個欄位寫入了空值NULL,沒有寫入預期的字串值。 /usr/local/lib/python2.7/dist-packages/DBUtils/SteadyDB.py:552: Warning: Incorrect string value: '\xF0\x9F\x91\x8F\xF0\x9F...' for column 'description' at row 2889

[原因]

  MySQL預設只支援3個位元組的utf8編碼,而這裡的字串中包括了需要四個位元組來標誌的字元(字串中有一些笑臉之類的符號)。

[解決]

  mysql5.5之後,支援使用utf8mb4來完成4個位元組的插入,官方解釋:


因此,需要做出兩個動作來完成修改: A. 修改table的編碼為utf8mb4,即設定:  DEFAULT CHARACTER SET = utf8mb4  COLLATE = utf8mb4_unicode_ci B. 在Python的連線資料庫程式碼中,增加對client的設定,即上述程式碼中的:
2) Warning: Truncated incorrect DOUBLE value [問題]
sql = 'SELECT `name` FROM `tb_test` WHERE `id` IN (%s);'
id_list = [36, 45, 44, 39, 40, 41, 42, 43, 37]
ids = ','.join(map(lambda x: str(x), id_list))
results = mysql.fetch_all(sql, [ids])
  在使用類似上述程式碼來做IN查詢資料時,發生如下Warning,導致查詢失敗: /usr/local/lib/python2.7/dist-packages/DBUtils/SteadyDB.py:552: Warning: Truncated incorrect DOUBLE value: '36, 45, 44, 39,40, 41, 42, 43, 37' [原因]   發生這個錯誤的根本原因是引數傳遞錯誤導致MySQL嘗試去比較int和string資料,而這裡的比較會統一轉換為double來做。具體的意思是:id是INT資料,而在上述的拼接中,最終的SQL語句變成了: SELECT `name` FROM `tb_test` WHERE `id` IN ('36, 45, 44, 39,40, 41, 42, 43, 37');
注意,多了一個引號。 [解決]   應該儘量避免自己拼接SQL語句,而採用Parameterized Query的方式,可以避開此類問題。針對這裡的問題,可以使用下述程式碼來替換:
sql = 'SELECT `name` FROM `tb_test` WHERE `id` IN (%s);'
id_list = [36, 45, 44, 39, 40, 41, 42, 43, 37]
place_holders = ','.join(map(lambda x: '%s', id_list))
results = mysql.fetch_all(sql % place_holders, [id_list])


Bruce 2016/10/15