Python与MySQL交互【精通MySQL系列四】

一、回顾事务与约束

  1. 事务(DTL)

    begin; commit; rollback;

  2. 事务的四个特性(ACID)

    • A 原子性

    • C 一致性

    • I 隔离性 (Isolation)

    ​ 四个级别:

    ​ 读未提交(read uncommitted)、

    ​ 读已提交 (read committed)、

    ​ 可重复读(repeatable read)

    ​ 串行化(serializable)

    ​ 查看当前会话session(连接)的隔离性:

    select @@tx_isolation;
    

    设置当前会话的隔离级别:

    set session transaction isolation level read committed; 
    
    • D 持久性
  3. 约束

    • 查询表中的约束

      show create table 表名;
      
      select constraint_name, constraint_type
      from information_schema.table_constraints
      where table_schema='数据库名' 
      and table_name= '表名';
      
    • 添加约束

      create table 表名(
         ...,
         constraint [约束名] constraint_type (约束的字段1 [, 约束的字段2] )
      );
      
      alter table 表名
      add constraint  [约束名] constraint_type (约束的字段1 [, 约束的字段2] );
      
    • 删除约束

      alter table 表名
      drop constraint_type [约束名];
      
    • 外键约束

      -- 普通的外键约束
      alter table card
      add constraint user_id_fk foreign key (user_id) references user(id);
      
      -- 级联的外键约束
      alter table card
      add constraint user_id_fk foreign key (user_id) references user(id)
      on delete cascade|set null;
      
  4. 连表查询

    • 等值条件查询

      select a.*, b.*
      from a, b
      where a.id = b.aid;  -- 外键与主键的连接
      

​ 如果有n张表,应该有n-1等值条件连接。

二、高级查询

2.1 join连表查询

2.1.1 内连接

内连接是两个表之间都具有的数据连接,连接字段的值有交集的部分。

使用 join on 关键字:

-- 查看所有用户在所有银行中的开户信息: 姓名、身份证号、银行卡号、银行名称、余额
-- user(id, number,name, phone)
-- back(id, name, address)
-- card(id, number, money, passwd, user_id, back_id)
select u.name as user_name,
       u.number as user_number,
       replace(c.number,' ', '') as card_number,
       b.name as back_name,
       c.money 
from card c
join user u on (c.user_id=u.id)
join back b on (c.back_id=b.id);

user和back两张表是没有直接关系, 都是通过card表进行关联。from 关键字后 表名应该是连接三张表中最核心的表。

2.1.2 左外连接

左外连接是指定除内连接的数据之外,增加左边表中的其它的数据。即左表选择全部数据,右表选择交集(都存在的)的数据。

使用的关键字: left join on

-- 查看所有用户的银行卡及余额信息
-- user(id, number,name, phone)
-- card(id, number, money, passwd, user_id, back_id)
select name, c.number as card_number, money
from user u
left join card c on (c.user_id = u.id);

如何选择左表, 应该根据查询的要求。 上面的要求是查询"所有用户", 选择user表作为左表是最合适的。

2.1.3 右外连接

同左外连接相反,除内连接的数据之外,额外选择右表中的其它数据。

使用关键字: right join on

-- 查看用户的所有头像信息: 用户名、头像url
-- user(id, number,name, phone)
-- user_photo(photo_id, url, user_id)
select name, url, photo_id
from user u
right join user_photo p on (u.id=p.user_id);

2.1.4 全外连接

将内连接、左外连接、右外连接的全部数据选择出来。

使用的关键字: full [outer] join on

-- 全部的用户信息和头像信息
select name, url, photo_id
from user u
full join user_photo p on (u.id=p.user_id);

注意: MySQL中不支持全外连接。

可以尝试使用union的合并查询关键字,将左外和右外查询的结果合并到一块:

select name, url, photo_id
from user u
left join user_photo p on (u.id=p.user_id)
union
select name, url, photo_id
from user u
right join user_photo p on (u.id=p.user_id);

union查询,要求两个查询的列名保持一致。对于重复的行会自动去重。

2.2 分组查询

  • 根据某一列(有重复值)进行分组 group by
  • 对于分组的列可以进行聚合操作, 统计数量、求和、求平均、最小、最大。
  • 分组之后,select语句的未分组字段且未使用聚合函数, 应该出现在group by 关键字中

生成样本数据

-- 教师表 teacher
create table teacher(
    id varchar(20) primary key,
    name varchar(20) not null,
    tel varchar(11)
) default charset=utf8;

insert into teacher values
('120001','张之梅','15611789900'),
('120002','刘小庆','17881918181'),
('120003','沈小阳','19929109991');

-- 班级表
create table cls(
    id integer primary key auto_increment,
    name varchar(20) unique,
    teacher_id varchar(20),
    constraint teacher_id_fk foreign key(teacher_id) references teacher(id)
    on delete set null
) default charset=utf8;

insert into cls(name, teacher_id)
values
('1901','120001'),
('1902','120002'),
('1903','120001');

-- 学生表
create table student(
   stu_id varchar(20) primary key,
   stu_name varchar(50) not null,
   sex varchar(1) default '男',
   city varchar(10) default '西安',
   cls_id integer comment '班级ID',
   constraint cls_id_fk foreign key (cls_id) references cls(id)
   on delete cascade
) default charset=utf8;

insert into student(stu_id,stu_name,cls_id,sex,city) 
values
('1001','jack',1,'男','西安'),
('1002','disen',1,'男','西安'),
('1003','rose',2,'女',	'咸阳'),
('1004',	'lucy',	1,'女','咸阳'),
('1005',	'jerry',	2,	'女',	'开封'),
('1006',	'pety',3,'男'	,'开封'),
('1007',	'judy',1		,'女',	'洛阳'),
('1008',	'mack',3,		'男',	'洛阳');

2.2.1 聚合函数

-- sum() 求合
-- avg() 平均值
-- min() 最小值
-- max() 最大值
-- count() 统计数量

以上聚合函数, 除了count()用于统计数量的,其它函数都用于数值计算。

-- 查看最富有的用户
-- card 表
select max(money) from card;
+------------+
| max(money) |
+------------+
|      40000 |
+------------+
select name, c.number as card_number, money
from user u
join card c on (c.user_id = u.id)
where money = ( select max(money) from card );
+--------+---------------------+-------+
| name   | card_number         | money |
+--------+---------------------+-------+
| 李成   | 8765 2134 4455 6719  | 40000 |
+--------+---------------------+-------+

如果聚合查询的结果只有一个,可以作为其它查询的where条件。另外,子查询放在一个小括号中。

-- 统计已开户的银行卡总数
select count(id) from card;
-- 查看最小、平均、最大的银行卡的余额值和银行卡的数量
select min(money) as min_money,
       round(avg(money),2) as avg_money,
       max(money) as max_money,
       count(id) as card_cnt
from card;

+-----------+-----------+-----------+----------+
| min_money | avg_money | max_money | card_cnt |
+-----------+-----------+-----------+----------+
|         0 |  12857.14 |     40000 |        7 |
+-----------+-----------+-----------+----------+

2.2.2 分组语句

关键字: group by

-- 统计学生表的男生和女生的人数
-- stu.student表
select sex, count(*) 
from student;

ERROR 1140 (42000): In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'stu.student.sex'; this is incompatible with sql_mode=only_full_group_by

错误的说明, sex列不是聚合列, 需要将sex字段放在group by子句中。

select sex, count(*)
from student
group by sex;

+------+----------+
| sex  | count(*) |
+------+----------+
||        4 |
||        4 |
+------+----------+
-- 查看每个城市的学生人数
select city, count(stu_id) 
from student
group by city;

+--------+---------------+
| city   | count(stu_id) |
+--------+---------------+
| 咸阳   |             2 |
| 开封   |             2 |
| 洛阳   |             2 |
| 西安   |             2 |
+--------+---------------+
-- 查看每个城市不同性别的学生人数
select city, sex, count(*)
from student
group by city, sex;

group by的子句中可以有多个分组的字段。

-- 查看每个班级的人数
select cls.name, count(*)
from student s
join cls on (cls.id = s.cls_id)
group by cls.name;

一般在连接join on之后,使用group by分组语句。

-- 查询带班人数最多的教师的信息: 教师的名称、教师的电话和带班的班级名和人数
select t.name,t.tel,c.name,A.stu_cnt
from teacher t
join cls c on (c.teacher_id = t.id)
join (
  select cls.id as cls_id, count(*) as stu_cnt
  from student s
  join cls on (cls.id = s.cls_id)
  group by  cls.id 
 ) A on (A.cls_id =c.id)
where A.stu_cnt = (
  select max(stu_cnt) max_stu_cnt
  from 
    ( select cls.id as cls_id, count(*) as stu_cnt
    from student s
    join cls on (cls.id = s.cls_id)
    group by  cls.id ) B
);
-- 创建统计班级人数的视图
-- 视图不是一张表,而是保存查询的SQL语句。
-- 视图的查询同表的查询是一样的
create view stu_cnt_view 
AS
    select cls_id, count(*) as stu_cnt
    from student s
    group by s.cls_id;
  select max(stu_cnt) max_stu_cnt
  from stu_cnt_view;  -- 视图在查询SQL中同表的用法一样
-- 优化查询语句
-- 查询带班人数最多的教师的信息: 教师的名称、教师的电话和带班的班级名和人数
select t.name,t.tel,c.name,A.stu_cnt
from teacher t
join cls c on (c.teacher_id = t.id)
join stu_cnt_view A on(A.cls_id=c.id)
where A.stu_cnt = (
  select max(stu_cnt)
  from stu_cnt_view
);
-- 查询班级人数小于4人的班级名称和带课老师的姓名
select c.name as cls_name,
       t.name as teacher_name
from cls c
join teacher t on (t.id=c.teacher_id)
where c.id in (
    select cls_id 
    from student
    group by cls_id
    having count(stu_id) < 4  -- 聚合的结果作为条件使用时,必须放在having中
);

having 子句是在聚合函数的结果作为查询条件时,不能将条件放在where中,只能在having中。

2.4 分页查询

分页查询是解决数据量大不易全部加载和客户端异步请求方式读取数据的情况。

语法: LIMIT offset, rows

offset是起始位置,offset从0开始的, offset的公式: (page-1)*size

rows 一次性读取的行数, 通过size表示一页的数据大小或个数。

-- 分页查询: 每一页显示2条记录, 查询第1页
select * from student
limit 0, 2;
-- 查询第4页
select * from student
limit 6, 2;

三、 pymysql库的应用

3.1 pycharm中创建项目

  1. 项目的名称 dbs_mysql

    存放在 项目目录中, Window: d:/codes/

  2. Python环境的名称 dbs

    存放的位置: window: d:/venvs

    python环境的完整路径: d:/venvs/dbs

  3. 激活Python环境

    在PyCharm低版本中的Terminal终端里显示的路径默认没有激活Python环境

    d:\codes\dbs_mysql > d:\venvs\dbs\Scripts\activate.bat
    (dbs) d:\codes\dbs_mysql > pip install pymysql 
    

    如果发现安装很慢,或超时,可以通过 -i 参数指定安装源,如阿里的源:

    pip install pymysql -i https://mirrors.aliyun.com/pypi/simple
    

3.2 pymysql库的使用

pymysql 主要提供了mysql数据库连接池,用于连接数据库和执行SQL语句。pymysql还支持事务,如在执行DML语句后,需要提交事务,如果不提交事务,则不会影响数据库的数据的。

3.2.1 连接数据库

import pymysql

conn = pymysql.Connect(host, port=3306, user, password, db, charset)
print('Connected!')

对于pymysql.Connect()的参数,通常 ***config方式,即config是一个dict字典,将host/port/user/password/db/charset等关键参数作为dict的key和value封装起来,使用的通过**config 将dict转成关键参数的结构传入Connect()方法中。

import pymysql
from pymysql.cursors import Cursor, DictCursor

config = {
    'host': '10.36.174.19',  # 可以更成域名 mysql903
    'port': 3306,
    'user': 'root',
    'password': 'root',  # 可以写成passwd
    'db': 'back',        # 可以写成database
    'charset': 'utf8',
    'connect_timeout': 10, # 默认10秒
    'read_timeout': 10,
    'write_timeout': 10,
    'cursorclass': Cursor  # 指定操作数据库的游标类,类似于文件流对象
}

conn = pymysql.Connect(**config)
print('Connected!')

3.2.2 查询数据表

必须先连接数据库,然后再打开cursor对象,通过cursor对象的execute()方法来执行查询SQL语句。

cursor = conn.cursor()

sql = 'select number,money from card'
cursor.execute(sql)  # 执行sql语句

再通过cursor对象的fetchone()或fetchall()获取 一条或所有条的查条记录。

results = cursor.fetchall()
print(results) # (('191992029', 8000), ...)

返回是可迭代的元组,内容又是的元组数据(按select中选择字段的顺序)

cursor = conn.cursor(cursor=DictCursor)
results = cursor.execute(sql)
print(results)  # [{'number': '', 'money':}, ...]

如果cursorclass指定的DictCursor, 则list中包含是每条记录的dict格式的数据。

在execute(sql, args)方法执行时,如果sql中使用 %s或%(key)s占位符号,则args必须指定相应的参数,如果sql使用是%s,则args指定是tuple类型, 如果sql中使用%(key)s,则args指定是dict类型。

-- sql 中使用 %s
sql = 'select * from card where user_id=%s'
cursor.execute(sql, args=(1,))
results = cursor.fetchall()
print(results)
sql = 'select * from card where user_id=%(uid)s'
cursor.execute(sql, args={'uid': 1})
results = cursor.fetchall()
print(results)

3.2.3 DML操作

当execute()执行了DML语句时,需要提交事务, 可以通过cursor.rowcount 属性获取DML语句影响的行数。

sql = 'insert into user(name, number, phone) values' \
      '(%(name)s , %(number)s, %(phone)s)'

user_data = {
    'name': '刘呈迁',
    'number': '2999992999292',
    'phone':  '17766655252'
}

cursor.execute(sql, args=user_data)
if cursor.rowcount >= 1:
   print('添加成功')
else:
   print('添加失败')
   
conn.commit()  # 提交事务

3.2.4 with上下文的应用

  1. Python中支持上下文环境的应用,主要使用with关键字,任何实现了__enter__()和__exit__()这两个函数的对象都可以在with关键字中使用。当对象进入上下文时,会调用对象__enter__(),当退出上下文时,会调用对象的__exit__()__

    class A:
        def __enter__(self):
            return [1,2,3]
            
        def __exit__(self, exc_type, exc_value, exc_tb):
            print('---释放资源--')
            if exc_type:
               print('Error:', exc_value)
            
            # True 如果exc_type不为None,则表示有异常但不继续抛出, False会抛出异常
            return False  
    
    a = A()
    with a as nums:  # nums接收__enter__()返回的结果
        print(nums)
        raise Exception('我的异常,我做主!')
    
    print('可以正常输出吗?')
    
  2. pymysql的conn连接对象已经实现了上下文相关的两个函数。意味着conn可以在with中使用。

    with conn as c:
         c.execute('select * from user')
         print(c.fetchall())
    

    如果conn在with中使用,则进入上下文时,调用conn的__enter__(),这个方法会返回cursor对象,当退出上下文时,检查 是否有异常,如果没有异步则提交事务,反之回滚事务。

    Connect类的源码如下:

    class Connection:   
       ....
       def __enter__(self):
            """Context manager that returns a Cursor"""
            warnings.warn(
                "Context manager API of Connection object is deprecated; Use conn.begin()",
                DeprecationWarning)
            return self.cursor()
            
       ...
       def __exit__(self, exc, value, traceback):
        """On successful exit, commit. On exception, rollback"""
        if exc:
            self.rollback()
        else:
            self.commit()
    

补充知识点:

排序问题

关键字: order by 字段名 [ DESC|ASC ]

DESC 表示字段的值按从大到小的顺序排列的; 降序

ASC 表示字段的值从小到大的顺序 排列的; 升序, 默认

-- order by 语句是在select查询语句的最后位置使用
-- 仅比 limit 语句高一层
-- order by 中可以使用 列的别名
-- 查询用户的银行存款信息,且按升序排列
select u.name, money
from user u 
join card c on (c.user_id=u.id)
order by money asc;
-- 查看用户预取款5000元之后的银行存款信息,且按降序排列
select u.name, money-5000 as y_money
from user u 
join card c on (c.user_id=u.id)
order by y_money desc;
-- 查看最富有的前3位用户
select u.name, money
from user u 
join card c on (c.user_id=u.id)
order by money DESC
limit 0, 3;

四、练习

  1. 设计DB类,实现上下文的两个方法,管理数据库的连接
  2. 设计操作Back/User/Card三张表的类,如 BackDB, UserDB,实现数据表的DQL和DML等操作
  3. 实现银行开户业务,业务流程如下:
    1. 查询银行
    2. 输入用户信息,建议用户档案
    3. 随机生成20位银行卡号
    4. 输入银行卡密码,使用md5加密( hashlib.md5() )也可以使用mysql的password()函数加密
    5. 存入开户费用,如10 元。
    6. 所有操作完成,提示用户开户成功
    7. 走用户取款业务。

你可能感兴趣的:(数据存储,Python实现技术)