【6.0】数据库知识点小结数据库升华

【6.0】数据库知识点小结数据库升华

【一】pymysql模块的进阶知识

【1】对数据库数据的增删改查

因为在增删改查中,只有查的权限是最简单的,因此查无需过多的权限

但是增删改都涉及到数据库数据的变动,需要额外的确认才行(即提交事务)

  • 主动提交事务
# -*-coding: Utf-8 -*-
# @File : 04 进阶方法 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/2
import pymysql


def create_mysql_connection():
    conn = pymysql.connect(
        # 指定 IP 和 PORT
        host='127.0.0.1',
        port=3306,

        # 指定用户名和密码
        user='root',
        password='1314521',

        # 指定默认编码
        charset='utf8',

        # 指定数据库
        database='day05',
    )

    # 创建游标对象
    cursor = conn.cursor(pymysql.cursors.DictCursor)

    # 返回游标对象
    return cursor, conn


cursor, conn = create_mysql_connection()


# 增加数据
def add_data():
    # (1) 拿到想要处理的数据
    username = input("Username:>>>").strip()
    password = input("Password:>>>").strip()

    # (2) 创建SQL语句
    sql = "INSERT INTO user(name,password) values (%s,%s);"

    # (3)查看SQL语句执行状态
    rows = cursor.execute(sql, (username, password))
    print(rows)

    # (4) 提交事务 :确认此次数据库更改操作
    conn.commit()


# 更改数据
def change_data():
    username = input("Username:>>>").strip()
    password = input("Password:>>>").strip()
    sql = "update user set name=%s, password=%s where id=1;"

    rows = cursor.execute(sql, (username, password))
    print(rows)

    conn.commit()


# 删除数据
def delete_data():
    sql = "delete from user where id =1;"

    rows = cursor.execute(sql)
    print(rows)

    conn.commit()


# 查询数据
def check_data():
    sql = "select * from user"
    cursor.execute(sql)
    print(cursor.fetchall())


if __name__ == '__main__':
    # add_data()
    # change_data()
    # delete_data()
    check_data()

  • 自动提交事务
# -*-coding: Utf-8 -*-
# @File : 04 进阶方法 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/2
import pymysql


def create_mysql_connection():
    conn = pymysql.connect(
        # 指定 IP 和 PORT
        host='127.0.0.1',
        port=3306,

        # 指定用户名和密码
        user='root',
        password='1314521',

        # 指定默认编码
        charset='utf8',

        # 指定数据库
        database='day05',

        # 自动提交事务
        autocommit=True
    )

    # 创建游标对象
    cursor = conn.cursor(pymysql.cursors.DictCursor)

    # 返回游标对象
    return cursor, conn


cursor, conn = create_mysql_connection()


# 增加数据
def add_data():
    # (1) 拿到想要处理的数据
    username = input("Username:>>>").strip()
    password = input("Password:>>>").strip()

    # (2) 创建SQL语句
    sql = "INSERT INTO user(name,password) values (%s,%s);"

    # (3)查看SQL语句执行状态
    rows = cursor.execute(sql, (username, password))
    print(rows)




# 更改数据
def change_data():
    username = input("Username:>>>").strip()
    password = input("Password:>>>").strip()
    sql = "update user set name=%s, password=%s where id=1;"

    rows = cursor.execute(sql, (username, password))
    print(rows)



# 删除数据
def delete_data():
    sql = "delete from user where id =1;"

    rows = cursor.execute(sql)
    print(rows)




# 查询数据
def check_data():
    sql = "select * from user"
    cursor.execute(sql)
    print(cursor.fetchall())


if __name__ == '__main__':
    # add_data()
    # change_data()
    # delete_data()
    check_data()

【2】一次性插入多条数据

# -*-coding: Utf-8 -*-
# @File : 04 进阶方法 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/7/2
import pymysql


def create_mysql_connection():
    conn = pymysql.connect(
        # 指定 IP 和 PORT
        host='127.0.0.1',
        port=3306,

        # 指定用户名和密码
        user='root',
        password='1314521',

        # 指定默认编码
        charset='utf8',

        # 指定数据库
        database='day05',

        # 自动提交事务
        autocommit=True
    )

    # 创建游标对象
    cursor = conn.cursor(pymysql.cursors.DictCursor)

    # 返回游标对象
    return cursor, conn


cursor, conn = create_mysql_connection()


# 增加数据
def add_data():
    # (1) 拿到想要处理的数据
    username = input("Username:>>>").strip()
    password = input("Password:>>>").strip()

    # (2) 创建SQL语句
    sql = "INSERT INTO user(name,password) values (%s,%s);"

    # (3)查看SQL语句执行状态
    rows = cursor.execute(sql, (username, password))
    print(rows)


# 更改数据
def change_data():
    username = input("Username:>>>").strip()
    password = input("Password:>>>").strip()
    sql = "update user set name=%s, password=%s where id=1;"

    rows = cursor.execute(sql, (username, password))
    print(rows)


# 删除数据
def delete_data():
    sql = "delete from user where id =1;"

    rows = cursor.execute(sql)
    print(rows)



# 查询数据
def check_data():
    sql = "select * from user"
    cursor.execute(sql)
    print(cursor.fetchall())


def add_many_data():
    sql = "INSERT INTO user(name,password) values (%s,%s);"

    cursor.executemany(sql,
                       [("name1", "password1"), ("name2", "password2"), ("name3", "password3"), ("name4", "password4")])


if __name__ == '__main__':
    # add_data()
    # change_data()
    # delete_data()
    check_data()

【二】视图

【1】什么是视图

视图就是通过查询得到一张虚拟表,然后保存下来,下次可以直接使用

视图也是一张表

  • 在计算机科学中,视图(View)是一种虚拟表,其内容是一个或多个基本表的查询结果。
    • 视图基于数据库中的数据,通过定义查询语句来构建,并在需要时动态地计算数据。
    • 与基本表不同,视图不存储实际的数据,而是根据查询语句在使用时进行实时计算。
  • 视图可以用于简化复杂查询、隐藏敏感数据、实现数据安全性和完整性约束等。
    • 通过定义视图,用户可以从一个或多个基本表中选择所需的列和行,以满足特定的查询需求。
    • 用户可以对视图执行与表相同的查询操作,如SELECT、INSERT、UPDATE 和 DELETE。
  • 视图可以是虚拟的,也可以是物化的。
    • 虚拟视图是基于查询结果的定义,每次查询时都会计算最新的结果。
    • 物化视图是基于查询结果的缓存,用于提高查询性能。
    • 虚拟视图适用于经常变动的数据,而物化视图适用于查询频繁但数据变动较少的情况。
  • 总之,视图是一种查询结果的抽象表示,它提供了一种灵活和安全的方式来访问和操作数据库中的数据。
    • 通过使用视图,用户可以根据自己的需求获取所需的数据,而无需直接访问底层的基本表。

【2】为什么要用视图

如果要频繁操作一张虚拟表(拼表组成),就可以制作成视图,后续直接操作

使用视图有以下几个主要的优点和用途:

  • 简化复杂查询:

    • 视图可以对基本表进行复杂的操作,包括连接多个表、过滤条件、聚合函数等。

    • 通过定义视图,可以将复杂的查询逻辑封装到一个简单的视图中,使用户能够以更简洁明了的方式进行数据检索。

  • 数据安全性:

    • 视图可以限制用户对数据的访问权限,通常用于隐藏敏感数据或只提供部分数据给特定的用户。

    • 通过定义视图并设置相应的权限,可以保护数据的安全性,防止未经授权的用户访问敏感信息。

  • 数据完整性:

    • 视图可以用于实现数据完整性约束,即对数据的有效性进行验证。

    • 通过定义视图并添加计算列、过滤条件等约束,可以确保所返回的数据满足一定的条件,提高数据的准确性和一致性。

  • 逻辑数据独立性:

    • 视图使得应用程序与数据之间解耦,即应用程序不需要了解底层表结构的细节。

    • 这样,当底层数据库发生变化时(如表结构修改),只需调整底层视图的定义而无需修改应用程序,从而提高系统的可维护性和扩展性。

  • 性能优化:

    • 物化视图是一种缓存机制,可以将视图的查询结果存储在磁盘上,以提高查询性能。
    • 当基本表的数据频繁变动时,物化视图可以减少查询的计算开销和响应时间,提升系统的性能。

总而言之

  • 使用视图可以简化查询操作、保护数据安全性、实现数据完整性、提高系统的可维护性和性能优化。
  • 视图提供了一种灵活且安全的数据访问方式,使用户能够根据自身需要方便地获取和操作数据。

【3】如何使用视图

语法结构:

create view 视图名(表名) 虚拟表的查询SQL语句

创建视图:

  • 首先,您需要使用CREATE VIEW语句创建一个视图。
    • 在创建视图时,您需要指定视图的名称以及要查询的源表和过滤条件。
    • 例如,下面是一个创建视图的示例语句:
CREATE VIEW my_view AS SELECT column1, column2 FROM my_table WHERE condition;
  • 在上述语句中,my_view是视图的名称,my_table是源表的名称,column1column2是要选择的列,condition是过滤条件。

使用视图进行查询:

  • 一旦视图创建成功,您可以像查询普通表一样使用视图进行数据检索操作。
    • 例如,下面是一个使用视图查询数据的示例语句:
SELECT * FROM my_view;
  • 上述语句将返回视图my_view中的所有数据行。

更新视图:

  • 如果需要对视图进行更新操作,您可以使用UPDATE语句。
    • 注意,不是所有的视图都可以进行更新,只有满足一定条件的视图才支持更新操作。
    • 以下是一个更新视图的示例语句:
UPDATE my_view SET column1 = value1 WHERE condition;
  • 在上述语句中,column1是要更新的列,value1是要设置的值,condition是更新条件。

删除视图:

  • 如果您不再需要某个视图,可以使用DROP VIEW语句将其删除。示例如下:
DROP VIEW my_view;
  • 上述语句将删除名为my_view的视图。

【4】注意

  • 创建视图咋硬盘上只会有表结构,没有表数据

    • 表数据还是来自之前的表
  • 视图一般只用来查询

    • 不建议对视图内的数据进行更改,有可能会对原始数据产生影响
  • 视图的使用频率并不高

    • 当创建了较多视图后,会造成数据的难以维护

【三】触发器

【1】什么是触发器

在满足对表数据进行增删改的情况下,自动触发的功能,称为触发器

  • 触发器是数据库管理系统中的一个概念,它是一种在数据库中定义的特殊对象,用于在满足特定条件时触发自动化的操作或逻辑。
    • 触发器主要用于监视数据库表的增删改操作,并在满足指定条件时触发相关的响应行为。
  • 触发器通常与数据库表关联,当数据库表发生特定的数据变化时,触发器会自动执行相关的操作
    • 比如插入、更新、删除或查询数据等。
  • 触发器可以作为一种数据库的约束,用于保证数据的完整性和一致性。

触发器的特点包括:

  1. 触发器通常与表一起创建、修改和删除。
  2. 触发器可以在特定的数据操作之前或之后触发执行。
  3. 触发器可以根据用户定义的条件判断是否执行相应的逻辑。
  4. 触发器可以调用存储过程、函数、触发其他触发器等,实现更复杂的业务逻辑。

【2】为何使用触发器

可以帮助我们实现日志、监控、处理等操作

  • 使用触发器可以实现很多功能
    • 比如数据验证、数据补全、数据同步、日志记录等。
  • 但需要注意,触发器的使用也需要谨慎,过多或不当的触发器可能会对数据库性能产生负面影响,因此在设计和使用触发器时应考虑到业务需求和性能方面的平衡。

【3】如何使用触发器

  • 触发器的六种使用情况
    • 增前
    • 增后
    • 删前
    • 删后
    • 改前
    • 改后

语法结构:

create trigger 触发器的名字 
before/after insert/update/delete 
on 表名 for each row 
begin
	SQL语句
end

针对触发器的名字,我们要见名知意

创建触发器:

  • 使用数据库管理系统提供的语法,创建一个新的触发器对象。
    • 在创建触发器时,您需要指定触发器的名称、触发时机(例如在插入、更新或删除之前或之后)、触发的表以及触发时执行的逻辑。

定义触发器逻辑:

  • 在创建触发器时,您需要定义触发器在触发时所执行的逻辑。
    • 这可以是任何数据库支持的操作
  • 比如插入数据、更新数据、删除数据、查询数据等。
    • 您可以使用SQL语句或调用存储过程、函数来实现触发器的逻辑。

定触发条件:

  • 根据您的需求,您可以为触发器指定触发条件。
    • 触发条件是一个逻辑表达式,当表中的数据满足该表达式时,触发器才会被激活执行相关的逻辑。
    • 例如,您可以指定只有当某列的值大于特定值时才触发触发器。

绑定触发器:

  • 将触发器绑定到相应的表上。一般情况下,触发器会与指定的表相关联,当该表发生特定的数据操作时,触发器才会被触发执行。

测试触发器:

  • 在绑定触发器后,您可以进行一些测试操作,验证触发器的逻辑是否按照预期执行。
    • 可以插入、更新或删除数据,观察触发器是否正确地处理这些操作。

示例

create trigger tri_before_insert_t1 before insert on t1 
for each row
begin
	sql语句
end
create trigger tri_before_insert_t1 after insert on t1 
for each row
begin
	sql语句
end

演示

创建触发器示例(在插入数据前触发):

CREATE TRIGGER trigger_name
BEFORE INSERT ON table_name
FOR EACH ROW
BEGIN
    -- 触发器逻辑
END;

定义触发器逻辑示例(在插入数据前触发):

CREATE TRIGGER trigger_name
BEFORE INSERT ON table_name
FOR EACH ROW
BEGIN
    -- 您可以在这里执行各种操作,比如插入另一张表、更新其他表、计算值等。
    -- 下面是一个示例,将新插入的数据中的某一列设置为当前时间。
    SET NEW.column_name = NOW();
END;

绑定触发器示例:

-- 将名为 "trigger_name" 的触发器绑定到名为 "table_name" 的表上
ALTER TABLE table_name
ADD CONSTRAINT trigger_name
AFTER INSERT ON table_name
FOR EACH ROW
BEGIN
    -- 触发器逻辑
END;

【4】特别注意

在写触发器的语法时,会遇到特殊情况,需要修改默认的语句结束符

  • 创建触发器
# 创建语句前修改默认语句结束符
delimiter $$
# 主要是为了区分错误执行语句和全局结束语句
create trigger tri_after_insert_cmd after insert on cmd 
for each row
begin
	if NEW.success = "no" then
		insert into errlog(err_cmd,err_time)
	values(NEW.cmd,NEW.sub_time);
		end if;
end $$
# 使用完以后要修改回原来的默认语句
delimiter ;
  • 删除触发器
drop tri_after_insert_cmd;

【5】触发器的参考模板

在表中插入新记录时,自动生成唯一标识符:

CREATE TRIGGER generate_uuid_trigger BEFORE INSERT ON table_name
FOR EACH ROW
BEGIN
    SET NEW.uuid = UUID();
END;

在表中更新记录时,更新最后修改时间:

CREATE TRIGGER update_last_modified_trigger BEFORE UPDATE ON table_name
FOR EACH ROW
BEGIN
    SET NEW.last_modified = NOW();
END;

在表中删除记录时,将记录添加到历史记录表:

CREATE TRIGGER archive_deleted_record_trigger AFTER DELETE ON table_name
FOR EACH ROW
BEGIN
    INSERT INTO history_table (id, deleted_at)
    VALUES (OLD.id, NOW());
END;

在表中插入新记录时,检查是否满足某个条件,若不满足则取消插入操作:

CREATE TRIGGER check_condition_trigger BEFORE INSERT ON table_name
FOR EACH ROW
BEGIN
    IF NEW.column_name < 10 THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Value must be greater than 10';
    END IF;
END;

上述模板中的"table_name"和"column_name"应替换为您实际使用的表名和列名。

日志模板

# 创建语句前修改默认语句结束符
delimiter $$
# 主要是为了区分错误执行语句和全局结束语句
create trigger tri_after_insert_cmd after insert on cmd 
for each row
begin
	if NEW.success = "no" then
		insert into errlog(err_cmd,err_time)
	values(NEW.cmd,NEW.sub_time);
		end if;
end $$
# 使用完以后要修改回原来的默认语句
delimiter ;

【补充】修改MySQL的默认语句结束符

将默认的 分号 ; 改为 $$

delimiter $$ 
delimiter ;

只作用于当前窗口

退出则改回默认

【四】事务

【1】什么是事务

开启一个事务可以包含多条语句,这些语句要么同时成功,要么都不成功

这成为事务的原子性

  • 事务是指一系列相关操作的集合,这些操作被视为一个不可分割的工作单元。
    • 事务的目标是确保在多个操作中的每一个都要么全部成功执行,要么全部失败回滚。

【2】事务的四大特性(ACID):

原子性(Atomicity):

一个事务是一个不可分割的单位,事务中包含诸多操作

要么同时成功,要么同时失败

  • 事务被视为一个原子操作,不可再分割。
  • 要么所有的操作都成功执行,要么所有的操作都会被回滚到事务开始前的状态,确保数据的一致性。

一致性(Consistency):

事务必须使数据库从一个一致性的状态变成另外一个一致性的状态

一致性和原子性是密切相关的

  • 事务执行前后,数据库应保持一致的状态。
  • 在事务开始之前和结束之后,数据库必须满足所有的完整性约束,如数据类型、关系等。

隔离性(Isolation):

一个事务的执行不能被其他事务干扰

(即一个事务内部的操作及使用到的数据对并发的其他事务是隔离的,并发执行的事务之间是互不干扰的)

  • 事务的执行结果对其他并发执行的事务是隔离的。
  • 即一个事务的执行不应受到其他事务的干扰,各个事务之间应该相互独立工作,从而避免数据的不一致性。

持久性(Durability):

也叫永久性

一个事务一旦提交成功并执行成功,那么它对数据库中数据的修改应该是永久的

接下来的其他操作或者故障不应该对其产生任何影响

  • 一旦事务被提交,其结果应该永久保存在数据库中,并且可以被系统故障恢复。
  • 即使系统发生宕机或崩溃,事务提交后的更改也应该是永久性的。

【3】事务的作用

在操作多条数据的时候,可能会出现某几条操作不成功的情况

数据一致性:

  • 事务可以确保数据库的一致性。
    • 在一个事务中,要么所有的操作都成功执行,要么全部回滚,保证了数据的完整性和一致性。
    • 例如,在一个转账操作中,如果转出账户扣款成功而转入账户未能成功接收资金,事务可以将操作全部回滚,以确保资金的一致性。

并发控制:

  • 事务提供了并发控制机制,确保多个并发执行的事务不会相互干扰,并避免数据的混乱和冲突。
  • 通过隔离级别的设置,事务可以控制不同事务之间的可见性和影响范围,保证并发执行时的数据一致性和隔离性。

故障恢复:

  • 事务的持久性特性确保了在事务提交后,即使系统发生故障或崩溃,提交的结果仍然可以被恢复。
  • 数据库管理系统通过使用日志文件等机制,将事务的操作记录下来,以便在需要时进行恢复和重放。

高效运行:

  • 通过组织多个操作为一个事务,可以减少与数据库交互的次数,从而提高数据库的操作效率和性能。事务可以减少磁盘I/O、锁的竞争等操作开销,提高数据库的并发处理能力。

数据完整性和安全性:

  • 事务可以保护数据的完整性和安全性。
  • 通过在事务中定义一些条件和约束,可以确保数据的有效性和准确性。
  • 例如,在一个银行系统中,事务可以检查账户余额是否足够以及转账金额是否合法,从而保证数据的安全性和正确性。

示例

当用户提交订单时,系统需要执行以下操作来处理订单:

  • 检查库存:

    • 系统需要检查所需商品的库存是否足够。

    • 如果库存不足,系统会提示用户库存不足,无法完成订单。

  • 扣减库存:

    • 如果库存充足,系统会将所购商品对应的库存数量减少。
  • 生成订单:

    • 系统会生成一个新的订单,包括订单号、商品信息、购买数量、价格等相关信息。
  • 计算总价:

    • 根据订单中的商品信息和购买数量,系统会计算出订单的总价格。
  • 更新用户账户:

    • 根据用户选择的支付方式,在扣除相应金额后,系统会更新用户账户余额或积分。
  • 生成支付信息:

    • 系统会生成相应的支付信息,以便用户完成支付。
  • 通知物流部门:

    • 系统会通知物流部门准备配送相关商品。
  • 发送订单确认邮件/短信:

    • 系统会向用户发送订单确认的邮件或短信,包括订单详细信息、配送信息等。
  • 监控商品配送:

    • 系统会跟踪订单的配送情况,并向用户提供订单状态更新。
  • 完成订单:

    • 当用户收到商品并确认满意后,订单状态会被更新为“已完成”。

【4】如何使用事务

事务相关的关键字

# 开启事务
start transaction;

# 回滚(回到事务执行之前的操作)
rollback;

# 二次确认(确认之后无法回滚)
commit;
  • 事务(Transaction)是数据库管理系统(DBMS)中的一种机制,用于保证数据库操作的一致性和可靠性。
  • 当多个数据库操作需要作为一个整体进行提交或回滚时,可以使用事务来实现。

(1)以下是使用事务的基本步骤:

开始事务:

  • 使用数据库提供的特定语句或命令来开始一个事务。
# 通常使用以下语句开始一个新的事务。
BEGIN、START TRANSACTION
# 或
BEGIN TRANSACTION

执行事务操作:

  • 在事务中执行需要的数据库操作,包括插入、更新、删除等操作。
  • 这些操作可以是单个SQL语句,也可以是多个SQL语句的组合。

提交事务或回滚事务:

  • 提交事务:

    • 当所有的操作都执行成功且符合业务逻辑要求时,使用COMMIT语句来提交事务。
    • 提交后,系统会把数据库中的修改永久保存下来。
  • 回滚事务:

    • 如果在事务执行过程中发生错误或违反了业务规则,需要撤销当前事务所做的所有更改
    # 使用以下语句来回滚事务。
    ROLLBACK
    

结束事务:

  • 不论是提交还是回滚事务,都需要通过特定的语句来结束事务。
# 在大部分关系型数据库中,可以使用以下语句来结束事务。
END
# 或
COMMIT

以下是一个使用事务的示例(使用SQL语言):

BEGIN TRANSACTION; -- 开始事务

-- 执行事务操作
INSERT INTO 表名 (列1, 列2, 列3) VALUES (值1, 值2, 值3);
UPDATE 表名 SET 列1 = 值1 WHERE 条件;
DELETE FROM 表名 WHERE 条件;

COMMIT; -- 提交事务,将修改永久保存
OR
ROLLBACK; -- 回滚事务,撤销所有修改

END; -- 结束事务
  • 值得注意的是,事务不仅仅适用于关系型数据库,很多NoSQL数据库也支持事务操作。

(2)模拟转账过程中的事务

  • 在模拟转账过程中使用事务机制可以确保转账操作的一致性和可靠性。
    • 事务的原子性(Atomicity)特性可以确保转账过程中的数据库操作要么全部成功提交,
    • 要么全部回滚。
  • 以下是一个简单的代码演示,使用Python和MySQL数据库来模拟转账过程中的事务机制:
import mysql.connector

def transfer_funds(sender_id, receiver_id, amount):
    try:
        # 创建数据库连接
        connection = mysql.connector.connect(
            host="数据库主机名",
            user="用户名",
            password="密码",
            database="数据库名称"
        )

        # 开始事务
        connection.start_transaction()

        # 建立游标
        cursor = connection.cursor()

        # 查询发送者的余额
        cursor.execute("SELECT balance FROM accounts WHERE id = %s FOR UPDATE", (sender_id,))
        sender_balance = cursor.fetchone()[]

        # 检查发送者余额是否足够
        if sender_balance < amount:
            raise Exception("Insufficient funds")

        # 更新发送者余额
        cursor.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", (amount, sender_id))

        # 更新接收者余额
        cursor.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", (amount, receiver_id))

        # 提交事务
        connection.commit()

        # 关闭游标和连接
        cursor.close()
        connection.close()

        print("Transfer successful")
    except Exception as e:
        # 回滚事务
        connection.rollback()

        # 关闭游标和连接
        cursor.close()
        connection.close()

        print("Transfer failed:", str(e))

# 使用示例
transfer_funds(1, 2, 100)  # 将ID为1的账户转账100元给ID为2的账户
  • 在上述代码中
    • 首先建立数据库连接,并使用start_transaction()方法开始一个新的事务。
    • 接下来,通过游标执行数据库操作,包括查询发送者的余额、检查余额是否足够、更新发送者和接收者的余额。
    • 如果所有操作成功执行,就调用commit()方法提交事务,并关闭游标和连接。
    • 如果在任意一个操作中出现异常,就调用rollback()方法回滚事务,并关闭游标和连接。
  • 需要注意的是
    • 在查询发送者的余额时,使用了FOR UPDATE语句锁定了该行数据,以确保在事务结束前其他事务不能修改这条数据。
    • 这样可以避免并发情况下的数据一致性问题。

(3)模拟转账功能

  • 创建表
create table user(
	id int primary key auto_increment,
    name varchar(16),
    balance int
);
  • 插入数据
insert into user(name,balance) values
("dream",1000),
("chimeng",1000),
("mengmeng",1000);
  • 事务流程
# 开启事务
start transaction;

# 多条SQL语句
update user set balance=900 where name="dream";
update user set balance=1010 where name="chimeng";
update user set balance=1090 where name="mengmeng";

# 事务回滚(取消交易)
rollback

# 二次确认(确认交易)
commit;

【5】总结

  • 当让多条SQL语句保持一致性的时候(要么同时成功,要么同事失败),可以考虑使用事务

【五】存储过程

【1】什么是存储过程

存储过程就类似于Python中的自定义函数

内部包含了一系列可以执行的SQL语句,存储过程存储在MySQL服务端中,可以通过调用存储过程触发内部的SQL语句

  • 存储过程是在关系型数据库中存储的一组预定义的SQL语句集合,可以接收参数并返回结果。
    • 它们被封装在数据库服务器中,并由应用程序通过调用存储过程来执行特定的数据库操作。

存储过程具有以下特点:

  • 预编译:

    • 存储过程在首次创建时会被编译和优化,之后每次执行时都不需要再进行编译,这样可以提高数据库的执行效率。
  • 数据库端执行:

    • 与应用程序中直接执行SQL语句相比,存储过程在数据库服务器端执行,减少了网络传输开销,提高了数据访问性能。
  • 代码重用:

    • 存储过程可以被多个应用程序共享和重用,避免了重复编写相同的SQL语句,提高了开发效率。
  • 安全性:

    • 通过存储过程,可以将对数据库的访问权限限制在一定范围内,从而提高数据的安全性。
  • 事务支持:

    • 存储过程可以包含事务处理逻辑,保证数据库操作的一致性和完整性。
  • 简化复杂操作:

    • 存储过程可以执行复杂的数据操作和计算,简化了应用程序的开发和维护过程。

【2】如何使用存储过程

基本使用

create procedure 存储过程的名字(形参1,形参2...)
begin
	sql 代码
end

调用

call 存储过程的名字();

(1)三种开发模式

第一种

  • 应用程序

    • 程序员写代码开发
  • MySQL

    • 提前编好存储过程,供应用程序调用
  • 优点

    • 开发效率提升、执行效率提升
  • 缺点

    • 考虑到人为因素、跨部门沟通等问题
    • 后续的存储过程的扩展性差

第二种

  • 应用程序
    • 程序员写代码开发之前
    • 涉及到数据库操作需要自己动手写
  • 优点
    • 扩展性高
  • 缺点
    • 开发效率低
    • 编写SQL语句繁琐,并且后续还需要考虑优化问题

第三种

  • 应用程序
    • 只写程序代码
    • 不写SQL语句
    • 基于别人写好的操作MySQL的Python的框架直接调用即可(ORM框架)
  • 优点
    • 开发效率比上面的两种高
  • 缺点
    • 语句的扩展性差
    • 可能会出现效率低下的问题

(2)存储过程的三种开发模式参考版

命令式开发模式(Imperative Development Model):

  • 在这种模式下,存储过程以类似编程语言的方式编写,包括条件语句、循环结构和异常处理等。
  • 这种模式下的存储过程逻辑较为灵活,可以使用各种流程控制语句,但也容易导致存储过程复杂难懂且维护困难。

声明式开发模式(Declarative Development Model):

  • 在这种模式下,存储过程的逻辑将更多地关注数据操作语句,如查询、插入、更新和删除等。
  • 存储过程的设计主要通过声明数据操作的目标和条件来实现,而不是依赖于具体的流程控制语句。
  • 这种模式下的存储过程简洁明了,更易于理解和维护。

触发器开发模式(Trigger Development Model):

  • 触发器是一种特殊的存储过程,它与数据库表相关联,并在预定义的事件发生时自动触发执行。
  • 触发器可以用于实现数据一致性、数据验证、日志记录等功能。
  • 触发器的开发模式与其他存储过程略有不同,因为触发器的执行时机和上下文是由数据库事件触发的,而不是由外部调用。

(3)开启存储过程的参考步骤

创建存储过程:

  • 使用数据库管理工具(如MySQL Workbench、SQL Server Management Studio等),编写存储过程的SQL语句,并在数据库中创建该存储过程。
  • 存储过程的创建可以包括参数的定义和逻辑的实现。

参数定义:

  • 如果存储过程需要接收参数,可以在存储过程的创建语句中定义参数类型、名称和属性。
  • 参数可以分为输入参数(用于传递数据给存储过程)和输出参数(用于存储过程返回结果给调用者)。

存储过程逻辑:

  • 在存储过程中编写SQL语句或其他处理逻辑,用于完成特定的数据库操作。
  • 这些逻辑可以包括查询、插入、更新、删除等操作,也可以包括条件判断、循环、异常处理等控制结构。

调用存储过程:

  • 在应用程序中通过SQL语句调用存储过程。
  • 调用存储过程时,提供必要的参数,并获取可能的输出结果。
  • 调用存储过程可以使用特定的SQL语法,也可以通过数据库连接的API来实现。

处理存储过程返回的结果:

  • 根据存储过程的设计,处理从存储过程返回的结果。
  • 这可能包括读取查询结果、解析输出参数值等。
  • 处理结果的方式与具体的应用程序和数据库访问框架有关。

(4)具体演示

在cmd中

delimiter $$
create procedure p1(
	in m int, # 只进不出 ,m 不能返回去
    in n int.
    out res int, # 该形参可以返回出去
)
begin
	select name from emp where dep_id > m and dep_id <m;
	set res = 666 # 将res变量修改,用来标识当前的存储过程代码确实执行了
end $$
delimiter ;

在Python中

在Python中,我们通常使用数据库来存储和管理数据。存储过程是一组预定义的SQL语句集合,它们以特定的名称存储在数据库中,并可以在需要时被调用执行。在Python代码中演示存储过程的示例,我们将使用MySQL数据库。

首先,确保你已安装mysql-connector-python库,可以使用以下命令进行安装:

pip install mysql-connector-python

接下来,可以使用以下示例代码来演示在Python中调用MySQL存储过程:

import mysql.connector

# 连接到数据库
connection = mysql.connector.connect(
    host='<YOUR_HOST>',
    user='<YOUR_USERNAME>',
    password='<YOUR_PASSWORD>',
    database='<YOUR_DATABASE>'
)

# 调用存储过程
def call_stored_procedure():
    try:
        cursor = connection.cursor()
        cursor.callproc('<YOUR_STORED_PROCEDURE_NAME>')
        for result in cursor.stored_results():
            print(result.fetchall())
        cursor.close()

    except (mysql.connector.Error) as e:
        print(f"Error calling stored procedure: {e}")

# 调用存储过程函数
call_stored_procedure()

# 关闭数据库连接
connection.close()

在上述示例代码中,你需要替换以下参数:

  • <YOUR_HOST>: 数据库主机名
  • <YOUR_USERNAME>: 数据库用户名
  • <YOUR_PASSWORD>: 数据库密码
  • <YOUR_DATABASE>: 数据库名称
  • <YOUR_STORED_PROCEDURE_NAME>: 存储过程的名称

call_stored_procedure函数中,我们使用cursor.callproc命令来调用存储过程。然后,通过cursor.stored_results方法获取存储过程的结果,并使用fetchall方法打印结果。

(5)代码演示参考

  • 存储过程(Stored Procedure)是预先编译好并存储在数据库中的一组SQL语句。
    • 它可以接受参数、执行复杂的逻辑和业务操作,并可以返回结果集或输出参数。
  • 下面是一个简单的存储过程代码演示及详解:
CREATE PROCEDURE GetEmployeeCountByDepartment
    @DepartmentName NVARCHAR(50),
    @EmployeeCount INT OUTPUT
AS
BEGIN
    SELECT @EmployeeCount = COUNT(*) 
    FROM Employees
    WHERE Department = @DepartmentName
END
  • 这个存储过程名为"GetEmployeeCountByDepartment",有两个参数:
    • @DepartmentName和@EmployeeCount。
    • @DepartmentName是输入参数,表示要查询的部门名称;
    • @EmployeeCount是输出参数,用于存储查询结果。
  • 在存储过程体内,我们使用了SELECT语句来查询Employees表中符合条件的记录数量,并将结果赋值给@EmployeeCount。
    • 具体来说,COUNT(*)用于统计符合条件的记录数量,WHERE子句过滤部门名称等于输入参数@DepartmentName的记录。
  • 使用存储过程时,可以通过以下示例代码来调用它并获取结果:
DECLARE @Result INT
EXEC GetEmployeeCountByDepartment 'IT', @Result OUTPUT
SELECT @Result AS 'EmployeeCount'
  • 在这段示例代码中,首先声明了一个叫做@Result的整型变量。
    • 然后使用EXEC关键字调用存储过程,并将部门名称参数设置为'IT',同时指定输出参数@Result。
    • 最后使用SELECT语句输出@Result的值,以获取查询结果。

存储过程的优点:

代码复用:
  • 存储过程只需要编写一次,可以在多个地方被调用,提高了代码的复用性。
数据库性能优化:
  • 存储过程在数据库中被编译、优化和缓存,可以减少网络传输开销,提高查询性能。
安全性增强:
  • 存储过程可以限制直接对表的访问权限,通过存储过程来进行数据操作,可以为数据库提供更好的安全性。

【六】函数

跟存储过程是有区别的,存储过程是自定义函数,函数就类似于内置函数

字符串函数:

  • CONCAT(str1, str2, ...): 将多个字符串连接成一个字符串。
  • SUBSTRING(str, start, length): 返回字符串的子串。
  • UPPER(str): 将字符串转换为大写。
  • LOWER(str): 将字符串转换为小写。
  • LENGTH(str): 返回字符串的长度。

数值函数:

  • ROUND(num, decimals): 对数值进行四舍五入。
  • FLOOR(num): 返回不大于给定数值的最大整数。
  • CEILING(num): 返回不小于给定数值的最小整数。
  • ABS(num): 返回给定数值的绝对值。

日期和时间函数:

  • NOW(): 返回当前日期和时间。
  • CURDATE(): 返回当前日期。
  • CURTIME(): 返回当前时间。
  • DATE_FORMAT(date, format): 格式化日期。

聚合函数:

  • COUNT(column): 计算符合条件的行数。
  • SUM(column): 对指定列的值求和。
  • AVG(column): 计算指定列的平均值。
  • MIN(column): 找到指定列的最小值。
  • MAX(column): 找到指定列的最大值。

条件函数:

  • IF(condition, value_if_true, value_if_false): 如果条件成立,则返回value_if_true,否则返回value_if_false。
  • CASE WHEN condition THEN value END: 根据满足的条件返回相应的值。

【七】流程控制

IF语句:

  • IF语句用于在满足条件时执行一组语句,否则执行另一组语句。它的基本语法如下:
IF condition THEN
    statements;
ELSE
    statements;
END IF;
  • 其中,condition是一个条件表达式,如果为真,则执行第一组语句;否则执行第二组语句。

CASE语句:

  • CASE语句用于基于多个条件执行不同的操作。它分为两种形式:
    • 简单CASE和搜索CASE。

简单CASE语句的基本语法如下:

CASE expression
    WHEN value1 THEN
        statements;
    WHEN value2 THEN
        statements;
    ...
    ELSE
        statements;
END CASE;
  • 其中,expression是待比较的表达式,当它等于某个值时,执行相应的语句。

搜索CASE语句的基本语法如下:

CASE
    WHEN condition1 THEN
        statements;
    WHEN condition2 THEN
        statements;
    ...
    ELSE
        statements;
END CASE;
  • 在搜索CASE语句中,每个WHEN子句后面跟着一个条件表达式,当某个条件为真时,执行相应的语句。

WHILE~语句:

  • WHILE语句允许你在满足条件的情况下反复执行一组语句。它的基本语法如下:
WHILE condition DO
    statements;
END WHILE;
  • 只要condition为真,statements将被不断执行。

LOOP语句:

  • LOOP语句用于无限循环执行一组语句,直到遇到LEAVE语句或满足特定条件时才退出循环。它的基本语法如下:
LOOP
    statements;
    IF condition THEN
        LEAVE;
    END IF;
END LOOP;

你可以在循环中使用LEAVE语句来手动退出循环。

模板参考

# if 判断
delimiter //
create procedure proc_if()
begin
	delimiter i int default 0;
	if i = 1 then
		select 1;
	elseif i = 2 then
		select 2;
	else 
		select 7;
	end if;
end //
delimiter ;
# while 循环
delimiter //
create procedure proc_while()
begin
	declare num int;
	set num = 0;
	while num < 10 do
		select 
			num;
		set num = num +1;
	end while;
delimiter ;

【八】索引

【1】什么是索引

  • 以前的数据都是存在于硬盘上的,查询数据不可避免的需要进行IO操作
  • 索引:
    • 就是一种数据结构,类似于书的目录。
    • 意味着以后再查询数据的时候应该先找目录再找数据,而不是一页一页的翻书,从而提升查询速度降低IO操作

【2】索引都有哪些

  • 索引在MySQL中也叫键,是存储引擎用于快速查找记录的一种数据结构

    • primary key

    • unique key

    • index key

  • foreign key

    • 不要是用来加速查询的
  • primary key/unique key

    • 不仅可以加速查询速度,还具有对应的约束条件
  • index key

    • 只有加速查询速度的功能

【3】本质

  • 通过不蹲的缩小想要的数据范围筛选出最终的结果

    • 同时将随机事件(一页一页的翻)变成顺序时间(先找目录再找数据)
  • 也就是说我们有了索引机制,我们可以总是用一种固定的方式查询数据

【4】索引的缺点

  • 当表中有大量数据存在的前提下,创建索引的速度回非常慢
  • 在索引创建完毕后,对表的查询性能会大幅度的上升,但是写的性能也会大幅度下降

不要随意地创建索引

【5】b+树

  • 只有叶子结点存放的是真实的数据,其他节点存放的都是虚拟的数据

  • 树的层级越高,查询数据索经历的步骤就越多(树有几层就查询数据几次)

  • 一个磁盘块存储的数据是有限的,我们应该让他存储数据越多还是越少

    • 越多越好
  • 为什么建议将 id 字段作为索引

    • 占用空间少,一个磁盘块存储的数据多
    • 降低了树的高度,从而减少查询次数

【6】聚集索引(primary key)

聚集索引指的就是主键

  • Innodb
    • 只有两个文件,直接将主键存放在了 idb 表中
  • MyIsam
    • 三个文件 , 单独将索引存放在一个文件

【7】辅助索引(unique,index)

  • 查询数据的时候不可能一直使用主键,也有可能使用到其他字段
    • 这个时候没有办法利用聚集索引
    • 这时可以根据情况给其他字段设置辅助索引(也是一个B+树)

【8】覆盖索引

在辅助索引的叶子节点就已经拿到了想要的数据

select name from user where name="dream";
  • 从 user 表中拿到了辅助索引为name对应的数据并且前面也是name字段
    • 这个现象称之为覆盖索引

【9】非覆盖索引

select age from user where name="dream";
  • 从user表中拿到了辅助索引为name对应的数据但是前面是age字段
    • 这种现象称之为非覆盖索引

【索引的介绍】

  • 索引(Index)是数据库中用于加快对数据库表中数据检索速度的数据结构。它类似于书籍的目录,能帮助我们更快地找到需要的信息。在访问数据库中的数据时,通过使用索引,数据库可以直接跳转到索引所指向的位置,而不需要逐一扫描所有数据记录,从而提高查询效率。

  • 索引通常是在数据库表中某个或多个列上创建的,它会按照预定义的排序方式存储数据,这样数据库就可以利用二分查找等算法来快速定位所需的数据。通过创建适当的索引,可以极大地提高数据库的检索性能,特别是在处理大量数据时。

  • 然而,索引也并非完全没有副作用。索引会占用额外的存储空间,并且在插入、更新或删除数据时需要维护索引结构,这可能会带来一定的性能开销。此外,过多或不恰当地创建索引也可能导致查询性能下降,因为数据库需要花费更多的时间来维护和更新索引。

【B+树详解】

【1】什么是B+树

  • B+树是一种平衡查找树的数据结构,被广泛应用于数据库系统和文件系统中的索引结构。
  • 它相比于其他树状结构(如二叉搜索树)具有更高的查询性能和更好的存储利用率。

【2】B+树的特点

  1. 平衡性:B+树是一棵平衡树,即每个节点的左子树和右子树的高度差不超过1。这样可以保持树的高度相对较小,使得查找操作的复杂度保持在O(log n)级别。

  2. 多路搜索:B+树的每个内部节点可以拥有多个子节点,这使得B+树能够在每个节点上存储更多的关键字,提高了存储的效率。

  3. 顺序访问性:B+树的所有叶子节点按照关键字的大小顺序形成一个有序链表,可以支持范围查询和排序等操作。同时,由于内部节点只包含关键字而不包含具体数据,可以在内存中存放更多的节点,从而提高了缓存的命中率。

  4. 存储利用率高:B+树的内部节点不存储数据,只存储关键字,因此相比于其他树结构(如B树),B+树可以存储更多的关键字。这使得B+树在磁盘或内存中的存储利用率更高。

【3】B+树的应用

  • B+树的应用主要包括数据库系统和文件系统中的索引结构。
  • 在数据库系统中,B+树常被用作数据库表的索引结构,能够高效地支持查找、插入、删除等操作。
  • 在文件系统中,B+树常被用来维护文件的目录结构,可以快速定位和访问文件。

【4】小结

  • 总之,B+树是一种高效的数据结构,具有平衡性、多路搜索、顺序访问性和存储利用率高等特点,适用于需要高效查询和排序的场景。

【聚集索引详解】

【1】什么是聚集索引

  • 聚集索引(Clustered Index)是关系型数据库中的一种索引类型,它决定了表中数据的物理存储顺序。
  • 在一个表中,只能有一个聚集索引。
  • 聚集索引对表进行了重新组织,使得数据按照聚集索引的键值顺序存储在磁盘上。
  • 由于聚集索引决定了数据的物理存储顺序,因此通过聚集索引可以快速地找到特定范围内的数据。

【2】聚集索引的主要特点包括:

  1. 数据的逻辑顺序和物理顺序是一致的,通过聚集索引可以直接访问特定行,因此聚集索引的查询速度很快。
  2. 聚集索引的键值必须是唯一的,不允许重复值存在。
  3. 当表中的数据发生插入、删除或更新操作时,聚集索引需要进行相应的调整以保持数据的有序性,这可能会对性能产生一定影响。
  4. 如果表中没有定义聚集索引,那么表的数据存储顺序将按照物理地址来存储。

【3】小结

  • 聚集索引适用于经常需要按照某个特定的列或列组进行查询的情况。
    • 例如,在一个订单表中,如果根据订单号频繁地进行查询,那么可以将订单号作为聚集索引,这样可以提高订单查询的效率。
  • 需要注意的是,聚集索引的选择需要根据具体的业务需求和数据访问模式进行权衡。
    • 在一些特定情况下,聚集索引可能并不适合或者不符合最佳实践,此时可以考虑使用非聚集索引等其他索引类型。

【辅助索引详解】

【1】什么是辅助索引详解

  • 辅助索引(Non-clustered Index)是关系型数据库中的一种索引类型,与聚集索引相对应。
    • 辅助索引的键值并不决定数据在磁盘上的物理存储顺序,而是创建一个额外的数据结构来帮助加快查询速度。一个表可以有多个辅助索引。

【2】辅助索引的主要特点包括:

  1. 辅助索引的建立并不直接影响数据的物理存储顺序,它是基于聚集索引(如果存在)或表本身的物理存储结构进行构建的。
  2. 辅助索引的键值可以重复,允许在同一个键值上有多行数据。
  3. 辅助索引存储着键值和指向实际数据行的指针,通过辅助索引可以快速定位到具体的数据行。
  4. 辅助索引的维护对数据的插入、删除、更新操作的性能有一定影响,因为每次操作都需要更新辅助索引。

【3】小结

  • 辅助索引适用于类似于搜索或排序等需要频繁进行的查询操作。
    • 当查询涉及到的列不是聚集索引的键值时,辅助索引可以提供更高效的查询性能。
    • 例如,在一个学生信息表中,如果经常按照学生的姓名进行查询,那么可以在姓名列上创建一个辅助索引,这样可以加速姓名查询的速度。
  • 需要注意的是,在设计数据库时,应根据实际情况综合考虑聚集索引和辅助索引的选择。
    • 聚集索引适用于经常以特定顺序扫描整个表或者频繁按照某个列进行范围查找的场景,而辅助索引则适用于单列或多列的等值或范围查找场景。
    • 理想情况下,辅助索引应当能够满足大部分查询需求,从而避免全表扫描。

【覆盖索引详解】

【1】什么是覆盖索引

  • 覆盖索引(Covering Index)是指在关系型数据库中,创建一个辅助索引包含了查询所需要的所有列,从而避免了对主表进行额外的数据检索操作。

【2】覆盖索引的主要特点包括:

  1. 覆盖索引包含了查询所需要的所有列,无需再通过辅助索引定位到实际的数据行。这样,数据库可以直接从索引中获取到查询所需的数据,而不需要再访问主表,提高了查询性能。
  2. 覆盖索引减少了磁盘I/O操作的次数。相比于使用辅助索引定位到数据行后再读取数据,覆盖索引可以直接从索引中读取所需数据,减少了磁盘读取的次数,从而加快了查询速度。
  3. 覆盖索引在一些特定的查询场景下非常有效,特别是当查询只需要返回索引包含的列时。例如,如果需要查询一个学生表中的学生姓名和年龄,而这两个列在一个名为"student_idx"的辅助索引上都有覆盖,那么查询时就可以直接使用该覆盖索引,而无需再去读取主表中的其他列,从而提高查询效率。

【3】小结

  • 根据具体的查询需求,我们可以通过创建适当的覆盖索引来提高查询性能。
    • 需要考虑的因素包括查询的列、查询的条件、表的大小以及对数据插入和更新操作的影响。
    • 创建过多的覆盖索引可能增加了存储空间的占用,并对数据的插入、删除和更新操作性能产生负面影响。
  • 因此,在创建覆盖索引时需要权衡索引的选择,避免过度索引化的情况。

【非覆盖索引详解】

【1】什么是非覆盖索引

非覆盖索引是指在数据库中的索引结构中,存储了对应的键值(例如:主键、唯一键、普通索引)以及相应的行的定位信息(如物理存储位置或行标识),但没有包含查询所需的其他列数据。当执行一个查询时,使用非覆盖索引需要通过索引定位到对应的行,并进一步访问主表来获取所需的列数据。

与覆盖索引相比,非覆盖索引需要进行额外的查询操作来检索主表中的其他列数据,因此在某些情况下可能会导致性能下降。

【2】非覆盖索引也有其适用的场景和优势:

  1. 提供高效的筛选能力:非覆盖索引可以根据所建索引的键值快速定位到满足查询条件的行,这可以减少需要扫描的数据量,提供高效的筛选能力。

  2. 减少磁盘I/O操作:虽然非覆盖索引需要额外的访问主表来获取数据,但是相对于全表扫描或者需要访问大量数据页的情况,非覆盖索引仍然可以减少磁盘的I/O操作次数,从而提升查询性能。

  3. 降低内存消耗:非覆盖索引通常比较小,占用的内存空间相对较少。这对于有限的内存资源来说,可以更好地利用内存空间。

【3】在使用非覆盖索引时,需要注意以下几点:

  1. 列选择性:索引的列选择性是指该列上不同值的数量与总行数之间的比率。当列具有较高的选择性时,非覆盖索引的效果通常会更好。因为高选择性的列能够更快地筛选出满足查询条件的行。

  2. 查询性能评估:在设计索引时,需要仔细评估查询的频率和性能需求。如果某个查询经常执行,而且对性能要求很高,那么建立合适的非覆盖索引可以提升查询效率。

【4】小结

  • 总结而言,非覆盖索引是一种常见的索引类型,在特定场景下可以提供高效的筛选能力和降低磁盘I/O操作的优势。
  • 但在选择索引类型时,需要综合考虑查询需求、列选择性以及数据表大小等因素,以选择最合适的索引优化方案。
posted @ 2023-07-02 20:57  Chimengmeng  阅读(23)  评论(0编辑  收藏  举报