遊標
在stored Routines調用中開的一個緩衝區,用於存放SQL調用的結果集。(結果集是隻讀的)
意味着我們的查詢可以返回一個文檔也可以返回一個遊標去指向一個結果集,而後通過遊標的切換而獲取每個結果
Python連接數據庫
涉及模塊
mysqldb py3不再更新
pymysql py3用的模塊
安裝pymysql 模塊
pip install pymysql
導入模塊
import pymysql
查看pymysql源碼
默認自動提交是False的
找到 Connection 類中,看如下代碼:
def __init__(self, host=None, user=None, password="",
database=None, port=0, unix_socket=None,
charset='', sql_mode=None,
read_default_file=None, conv=None, use_unicode=None,
client_flag=0, cursorclass=Cursor, init_command=None,
connect_timeout=10, ssl=None, read_default_group=None,
compress=None, named_pipe=None, no_delay=None,
autocommit=False, db=None, passwd=None, local_infile=False,
max_allowed_packet=16*1024*1024, defer_connect=False,
auth_plugin_map={}, read_timeout=None, write_timeout=None,
bind_address=None, binary_prefix=False
):
連接數據庫,判斷數據庫正常與否
使用try或者使用ping方法都可以
通過ping測試連通性
通過判斷是否是None或者異常,則認爲服務是否是存活的
import pymysql
conn = pymysql.connect('ip','root','123456')
print(conn.ping)
print(conn.ping(False))
如果連接失敗我們要對其進行關閉,所以最好加入到try中
import pymysql
try:
conn = pymysql.connect('x.x5.x','root','123456','test110')
print(conn.ping(False))
finally:
if conn:
conn.close()
遊標 Curosr
在stored Routines調用中開的一個緩衝區,用於存放SQL調用的結果集。(結果集是隻讀的)
意味着我們的查詢可以返回一個文檔也可以返回一個遊標去指向一個結果集,而後通過遊標的切換而獲取每個結果
在操作數據庫的時候必須使用cursor類的實例,提供execute()方法,執行sql語句返回成功的行數
執行sql語句
import pymysql
try:
conn = pymysql.connect('.x4.1.1','root','123456','test110')
print(conn.ping(False))
cursor = conn.cursor() # 獲取遊標
insert_sql = "insert into student(name,age) values('jerry',20)"
line = cursor.execute(insert_sql) # 執行
print(line)
finally:
if conn:
conn.close()
返回如下:
None
1
但是數據庫中並無數據,因爲沒有commit
在Connection類中,默認設置的是
一般不需要開啓自動提交,需要手動管理事務並統一提交
事物的管理
有任何異常conn都要回滾確保數據無誤,如果沒有異常則commit()
close和commit 沒有先後關係順序
一個標準的數據庫連接和操作關閉流程
import pymysql
conn = None
try:
conn = pymysql.connect('47.xxx','root','123456','test110')
print(conn.ping(False))
# 獲取遊標
cursor = conn.cursor()
insert_sql = "insert into student(name,age) values('jerry',20)"
line = cursor.execute(insert_sql)
cursor.close()
conn.commit()
except:
conn.rollback()
finally:
if conn:
conn.close()
批量插入
try:
conn = pymysql.connect('111.11.110','root','123456','test110')
print(conn.ping(False))
# 獲取遊標
cursor = conn.cursor()
#插入數據
for i in range(10):
insert_sql = "insert into student(name,age) values('jerry',{})".format(i)
line = cursor.execute(insert_sql)
print('line:',line)
cursor.close()
conn.commit()
往往會將批量的修改全部寫前面,最後統一執行commit,遊標關閉和鏈接提交是可以不分先後的
操作流程
建立連接--> 獲取遊標 --> 執行SQL --> 提交事物 --> 釋放資源
如果出現異常需要回滾再釋放資源
查詢
不需要事物的地方一定不要使用事物,影響效率
查看分別有什麼區別
conn = pymysql.connect('47.4.xx.xx','root','123456','test110')
print(conn.ping(False))
# 獲取遊標
cursor = conn.cursor()
sql = 'select * from student'
line = cursor.execute(sql)
#
獲取前兩個
# print(cursor.fetchaone())
# print(cursor.fetchall())
獲取5個並以元組方式返回
print(cursor.fetchmany(5))
print(cursor.fetchmany(5))
獲取剩下的全部
print(cursor.fetchall())
查看返回結果fethchall之所以獲取很少的數據,因爲都使用的是遊標,類似於一個指針
重置遊標
print(cursor.fetchmany(5))
print(cursor.fetchall())
cursor.rownumber = 0
print(cursor.fetchall())
這樣指針又指向了0,其實是指向的索引
查看fethchall源碼
實際上就是做了一個切片,以切片方式並記錄當前位置返回我們想要的結果
def fetchall(self):
"""Fetch all the rows"""
self._check_executed()
if self._rows is None:
return ()
if self.rownumber:
result = self._rows[self.rownumber:]
else:
result = self._rows
self.rownumber = len(self._rows)
return result
DictCursor 字典遊標,帶一些字段名方式進行返回
Cursor 類有一個Mixin的子類 DictCursor
導入模塊
from pymysql.cursors import DictCursor
cursor = conn.cursor(cursor=DictCursor)
以上是元組和字典的返回差異
返回如下:
[{'name': 'jerry', 'en': None, 'age': 1}, {'name': 'jerry', 'en': None, 'age': 2}]
SQL注入***
在登陸的時候做了一些匹配或者明文匹配所導致
一般情況都需要進行加密
比如這樣的語句:
select * from t where name='ben' and password='ben';
在登陸時要做唯一約束的,用戶在寫提交程序的時候,用戶名需要異步去驗證
這個過程已經是查過數據庫了
在登陸時要做唯一約束的,用戶在寫提交程序的時候,用戶名需要異步去驗證
這個過程已經是查過數據庫了
但是用戶在執行的時候加了這麼一句
select * from t where name='ben' and password='ben' or 1 = 1 ;
or 1=1 是真值,相當於 select *
正常字符串拼接所造成的
正常的拼接:
name = 'jerry'
age = '3 or 1=1'
sql = 'select * from student where name={} and age={}'.format(name,age)
這樣是最原始的拼接字符串的方式,太危險
這樣通過id 或者其他來獲取你的數據,總有一個參數能返回數據,每個參數都會遭到***
SELECT * FROM t where a = 1 and b = 1 or 1 = 1 and id=5 or 1=1;
如果密碼失敗,那麼還可以通過查詢來進行***,這樣也會返回數據
所以,不能使用字符串拼接的方式來拼寫sql
凡是用戶提交的數據都不可信,要做嚴格的檢查,哪怕是調用函數
解決sql注入
參數化查詢,可以有效防止注入***,並提高查詢效率
通過cursor.execute參數進行防注入
cursor.execute(query,args=None)
查看args源碼
def execute(self, query, args=None):
"""Execute a query
:param str query: Query to execute.
:param args: parameters used with query. (optional)
:type args: tuple, list or dict 明確寫明args必須是一個元組列表或者字典
:return: Number of affected rows
:rtype: int
If args is a list or tuple, %s can be used as a placeholder in the query.
If args is a dict, %(name)s can be used as a placeholder in the query.
"""
while self.nextset():
pass
query = self.mogrify(query, args)
result = self._query(query)
self._executed = query
return result
通過拼接字符串的方式是能看到別人所有數據,那麼改進如下:
try:
conn = pymysql.connect('4x.x.x.x','root','123456','test110')
cursor = conn.cursor(DictCursor)
age = '20 or 1=1'
sql = 'SELECT * FROM student where age=%s'
# sql =' SELECT * FROM student where age={}'.format(age)
cursor.execute(sql,(age,)) #將規則寫入到execute中,判斷是否有其他敏感字符
print(cursor.fetchall())
Warning: (1292, "Truncated incorrect DOUBLE value: '3 or 1=1'")
[{'name': 'jerry', 'age': 3, 'en': None}]
self._do_get_result()
儘可能轉爲目標的數據,友好的做了轉換,但是發現有一些非法的字段被攔截掉了
提示非法DOUBLE的類型
更加複雜的例子
sql = 'SELECT * FROM student where age > %(age)s'
# sql =' SELECT * FROM student where age={}'.format(age)
cursor.execute(sql,{'age':10}) #參數化查詢
print(cursor.fetchall())
參數化爲啥可以提高效率?
因爲sql語句緩存
客戶端每發一次sql語句到mysql中都有一個sql編譯過程,只是對sql語句進行編譯
如果不用參數化查詢,那麼id=1 id=2 id=3 來了三條語句,需要重新編譯
mysql服務端會對sql語句編譯和緩存,編譯只對sql部分,只是sql部分並不是結果
編譯過程,需要語法分析,生成AST並優化生成執行計劃等過程 這個過程比較耗費資源
可認爲本身sql語句字符串就是一個key,找到key則直接找到結果
那麼如果使用拼接的方案,每次發過去的sql都不一樣,都需要編譯並緩存
開發時,應該使用參數化查詢
這裏只是查詢的字符串的緩存,並不是查詢結果
遊標的上下文
查看遊標的源碼 __enter__ 和 __exit__
遊標類:
def __enter__(self):
return self #返回自己
def __exit__(self, *exc_info):
del exc_info
self.close() #調用close()關閉自己
連接類進入上下文的時候會返回一個遊標對象,就是遊標自己
遊標類也使用上下文,用完了之後還會調用enter 和 exit
在退出時關閉遊標對象,執行 self.close()
查看close源碼:
def close(self):
"""
Closing a cursor just exhausts all remaining data.
"""
conn = self.connection
if conn is None:
return
try:
while self.nextset():
pass
finally:
self.connection = None
def __enter__(self):
return self
def __exit__(self, *exc_info):
del exc_info
self.close()
創建的時候用的是cursor,連接的時候也可以關閉
最後將連接 = None,說明沒有連接,無法使用
連接類的上下文
有沒有with as xxx ,是定義的問題,在退出with的時候,查看有否異常,如果存在異常則回滾
進入連接類的時候會返回一個遊標
連接類:
def __enter__(self):
"""Context manager that returns a Cursor"""
return self.cursor()
def __exit__(self, exc, value, traceback):
"""On successful exit, commit. On exception, rollback"""
if exc:
self.rollback()
else:
self.commit() #如果沒有異常則提交
創建的時候用的是self.cursor(),通過enter 進來的時候調用self.cursor(),直接調用了遊標
遊標通過調用本地方法獲取
def cursor(self, cursor=None):
"""Create a new cursor to execute queries with"""
if cursor:
return cursor(self)
return self.cursorclass(self)
如果沒有存在,那麼直接調用cursorclass ,那麼cursorclass直接調用遊標類
而cursorclass 就是在Connection 初始化中去獲取
self.cursorclass = cursorclass
cursorclass直接指向了遊標類,通過調用遊標類返回一個自己的實例提供調用
總結
連接:
遊標的上下文是返回自己提供使用的,在close()將遊標關閉,關閉的是自己將其標記爲None
對於連接來講,在with進入之後返回的是cursor()遊標自己的對象
連接類如下,在調用它的時候,上下文先執行,並將遊標類調用,所以調用的是cursor()
def __enter__(self):
"""Context manager that returns a Cursor"""
return self.cursor()
關鍵是關閉的時候並沒有自行關閉連接,因爲連接是共用連接(長連接),所以不會關閉連接的,但是遊標需要關閉,完全由用戶控制
退出:
但是如果退出with語句塊,肯定會檢查是否有異常,提交或者回滾
當離開語句塊的時候會提交或回滾
所以,代碼需要如下改進:
import pymysql
from pymysql.cursors import DictCursor
conn = None
try:
# 建立連接
conn = pymysql.connect('7.94.xx','root','123456','test110')
# cursor = conn.cursor(DictCursor) #註釋遊標獲取,在with中已經獲取了遊標
with conn as cursor: #代替了上一上,在進入上下文的時候,conn已經獲取了遊標
d = {'age':'5'}
sql = 'select * from student where age>%(age)s'
print(sql)
# 執行
line = cursor.execute(sql,d)
print(line)
print(cursor.fetchall())
except:
print('errrrrr')
finally:
if conn:
conn.close()
這樣進入到with 連接對象 as cursor ,的時候直接調用了conn連接對象的上下文,並調用了遊標類
返回了遊標的self,這樣就可以直接在with中調用,免去了開銷
如果使用遊標的上下文則可以:
conn = None
try:
# 建立連接
conn = pymysql.connect('ip','root','123456','test110')
# cursor = conn.cursor(DictCursor)
with conn as cursor:
with cursor:
d = {'age':10}
sql = 'select * from student where age<%(age)s'
cursor.execute(sql,d)
print(cursor.fetchall())
except:
print('errrrrr')
finally:
if conn:
conn.close()
當with conn as的時候, 返回一個新的cursor對象,當退出時,只要提交或者回滾了事物,並沒有關閉
不關閉遊標表示可以繼續反覆使用它,節省了開銷
但是在最後finally中定義了關閉
finally:
if conn:
conn.close()
連接池
數據庫最大的開銷其實是連接,所以引入了連接池的概念
設置一個數據庫連接池,使用者如果需要則get一個
分析
一個連接池,應該是一個可以設置大小容器,存放數據庫的連接,使用者需要連接從池中獲取一個連接,用完歸還
啓動的時候開啓連接到數據庫中,連接數據庫的時候避免了頻繁連接數據庫,也限制了主動連接
連接池中只是存放的連接,具體如何使用是用戶的事情,只需要存放一個正常的連接即可
選型
對於線程、所機制、信號量等無非使用Queue比較合適當前情景
Queue本身就保證了線程的絕對安全性
# coding:utf-8
import pymysql
from pymysql.cursors import DictCursor
from queue import Queue
conn = None
class ConnPool:
def __init__(self,size,*args,**kwargs):
self.size = size
self._pool = Queue
for i in range(size):
conn = pymysql.connect(*args,**kwargs) # 傳入用戶名密碼
self._pool.put(conn) # 傳入到隊列,生產者
def get_conn(self):
return self._pool.get()
def return_conn(self,conn:pymysql.connections.Connection):
self._pool.get(conn) # 消費連接
線程池引入:初始化線程池、初始化之後上面代碼就等於有了連接了
接下來就是如何使用的問題
那麼可否將id放入到集合裏,集合中是不允許重複的,id也是
判斷只要地址不同就可以,那麼也有風險,如果內存被回收,或者在這時間又賦予這個id內存地址
所以是有風險的,也不排除用戶的操作問題;
所以需要做類型的判斷;
判斷類型
import pymysql
from pymysql.cursors import DictCursor
from queue import Queue
conn = None
class ConnPool:
def __init__(self,size,*args,**kwargs):
self.size = size
self._pool = Queue(size)
for i in range(size):
conn = pymysql.connect(*args,**kwargs) # 傳入用戶名密碼
self._pool.put(conn) # 傳入到隊列,生產者
def get_conn(self):
return self._pool.get()
# 判斷類型是否是連接
def return_conn(self,conn:pymysql.connections.Connection):
if isinstance(conn,pymysql.connections.Connection):
self._pool.put(conn)
# 初始化類
pool = ConnPool(5,'4ip','root','123456','test')
#
conn = pool.get_conn()
print(conn)
# 消費連接
pool.return_conn(conn)
那麼對於沒有關閉的連接是需要手動的,所以能否在類中實現關閉,後期可以在當前類的上下文
引入Thread.Local ,前提是隻有一個在用它,而且Thread.Local是順序執行
在每個線程中用,限定在多線程的場景下使用,起碼保證線程內,Thread.Local是安全的
import pymysql
from pymysql.cursors import DictCursor
from queue import Queue
import threading
class ConnPool:
def __init__(self,size,*args,**kwargs):
self.size = size
self._pool = Queue(size)
self.local = threading.local()
for i in range(size):
conn = pymysql.connect(*args,**kwargs) # 傳入用戶名密碼
self._pool.put(conn) # 傳入到隊列,生產者
# 標記self.local.conn 獲取的時候則賦值並返回get到的連接
def get_conn(self):
# return self._pool.get()
conn = self._pool.get()
self.local.conn = conn # 標記當前連接,用於獲取put之後標記None,如果get到了則標記當前連接
return conn
# 線程是順序的,就是說一直用完到還回線程
def return_conn(self,conn:pymysql.connections.Connection):
if isinstance(conn,pymysql.connections.Connection):
self._pool.put(conn)
self.local.conn = None # 用完之後標記爲None,仿照連接類去寫
# 初始化類
pool = ConnPool(5,'47.9.x.x.','root','123456','test')
#
conn = pool.get_conn()
print(conn)
# 消費連接
pool.return_conn(conn)
上下文改進
遊標的上下文帶來了一些列問題,那麼可否自行增加上下文,如果是None則返回一個遊標
如果不是None,那麼就是連接了
enter 用於是否是None,是的話則賦予一個連接
exit 只要有一個退出,那麼就標記當前爲None
所以thread.local 還是可以的,因爲都是在本地線程中使用,內存地址沒有改動,直到關閉
# 線程是順序的,就是說一直用完到還回線程
def return_conn(self,conn:pymysql.connections.Connection):
if isinstance(conn,pymysql.connections.Connection):
self._pool.put(conn)
self.local.conn = None # 用完之後標記爲None,仿照連接類去寫
def __enter__(self):
#剛進來的時候線程不存在則拋異常,肯定是None,所以給一個連接
if getattr(self.local,'conn',None) is None:
self.local.conn = self.get_conn()
return self.local.conn.cursor() #返回一個遊標
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type:
self.local.conn.rollback()
else:
self.local.conn.commit()
self.return_conn(self.local.conn)
self.local.conn = None
# 初始化類
pool = ConnPool(5,'.x.x.x.x','root','123456','test')
with pool as cursor:
with cursor:
sql = 'select * from student'
cursor.execute(sql)
# print(cursor.fetchall())
for x in cursor:
print(x)