zoukankan      html  css  js  c++  java
  • JDBC 看过 没用

    jdbc&连接池&事务

    预备知识:

    一、数据库的基本知识

    1. 数据库概述

    l 什么是数据库

    数据库就是存储数据的仓库,其本质是一个文件系统,数据按照特定的格式将数据存储起来,用户可以对数据库中的数据进行增加,修改,删除及查询操作。

    l 什么是数据库管理系统

    数据库管理系统(DataBase Management System,DBMS):指一种操作和管理数据库的大型软件,用于建立、使用和维护数据库,对数据库进行统一管理和控制,以保证数据库的安全性和完整性。用户通过数据库管理系统访问数据库中表内的数据。

    l 数据库与数据库管理系统的关系

     

    2. 数据库表

    数据库中以表为组织单位存储数据。

    表类似我们的Java类,每个字段都有对应的数据类型。

    那么用我们熟悉的java程序来与关系型数据对比,就会发现以下对应关系。

    类----------表

    类中属性----------表中字段

    对象----------记录

     

    3. 表数据

    根据表字段所规定的数据类型,我们可以向其中填入一条条的数据,而表中的每条数据类似类的实例对象。表中的一行一行的信息我们称之为记录。

     

    l 表记录与java类对象的对应关系

     

    4. 常见数据库

    l 常见的数据库管理系统

     

    MYSQL:开源免费的数据库,小型的数据库.已经被Oracle收购了.MySQL6.x版本也开始收费。

    Oracle:收费的大型数据库,Oracle公司的产品。Oracle收购SUN公司,收购MYSQL。

    DB2 :IBM公司的数据库产品,收费的。常应用在银行系统中.

    SQLServer:MicroSoft 公司收费的中型的数据库。C#、.net等语言常使用。

    SyBase:已经淡出历史舞台。提供了一个非常专业数据建模的工具PowerDesigner。

    SQLite:嵌入式的小型数据库,应用在手机端。

     

    常用数据库:MYSQL,Oracle.

    这里使用MySQL数据库。MySQL中可以有多个数据库,数据库是真正存储数据的地方。

    二、windows中安装mysql

    见 <<MySQL安装图解.pdf>>

    三、JDBC

    1. JDBC概述

    JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API。JDBC是Java访问数据库的标准规范,可以为不同的关系型数据库提供统一访问,它由一组用Java语言编写的接口和类组成。

    JDBC需要连接驱动,驱动是两个设备要进行通信,满足一定通信数据格式,数据格式由设备提供商规定,设备提供商为设备提供驱动软件,通过软件可以与该设备进行通信。 今天我们使用的是mysql的驱动mysql-connector-java-5.1.37-bin.jar

     

    JDBC规范(掌握四个核心对象):

    1) DriverManager:用于注册驱动

    2) Connection: 表示与数据库创建的连接

    3) Statement: 操作数据库sql语句的对象

    4) ResultSet: 结果集或一张虚拟表

    2. jdbc原理

    Java提供访问数据库规范称为JDBC,而生产厂商提供规范的实现类称为驱动。

     

    JDBC是接口,驱动是接口的实现,没有驱动将无法完成数据库连接,从而不能操作数据库!每个数据库厂商都需要提供自己的驱动,用来连接自己公司的数据库,也就是说驱动一般都由数据库生成厂商提供。

    3. Jdbc入门案例

    1) 准备数据

    之前我们学习了sql语句的使用,并创建的分类表category,今天我们将使用JDBC对分类表进行增删改查操作

    #创建数据库

    create database day04;

    #使用数据库

    use day04;

    #创建分类表

    create table category(

    cid int PRIMARY KEY AUTO_INCREMENT  ,

       cname varchar(100)

    );

    #初始化数据

    insert into category (cname) values('家电');

    insert into category (cname) values('服饰');

    insert into category (cname) values('化妆品');

    2) 导入驱动jar包

    创建lib目录,存放mysql的驱动mysql-connector-java-5.1.37-bin.jar

    选中mysql的jar包,右键选择“ Add as Library...” 完成jar导入

     

    3) 代码演示

    操作步骤:

    1.注册驱动.

    2.获得连接.

    3.获得执行sql语句的对象

    4.执行sql语句,并返回结果

    5.处理结果

    6.释放资源.

     

    //查询所有的分类信息

    @Test

    public void demo1() throws Exception{

    // 注意:使用JDBC规范,采用都是 java.sql包下的内容

    //1 注册驱动

    Class.forName("com.mysql.jdbc.Driver");

    //2 获得连接

    String url = "jdbc:mysql://localhost:3306/mydb";

    Connection conn = DriverManager.getConnection(url, "root", "root");

    //3获得执行sql语句的对象

    Statement stmt = conn.createStatement();

    //4执行SQL语句

    ResultSet rs = stmt.executeQuery("select * from category");

     

    //5处理结果集

    while(rs.next()){

    // 获得一行数据

    Integer cid = rs.getInt("cid");

    String cname = rs.getString("cname");

    System.out.println(cid + " , " + cname);

    }

    //6释放资源

    rs.close();

    stmt.close();

    conn.close();

    }

    4. API详解

    l 1) 注册驱动

    DriverManager.registerDriver(new com.mysql.jdbc.Driver())

    不建议使用,原因有2个:

    导致驱动被注册2次

    强烈依赖数据库的驱动jar

    解决办法:

    Class.forName("com.mysql.jdbc.Driver");

    l 2) 获取链接

    static Connection getConnection(String url, String user, String password)试图建立到给定数据库 URL 的连接。

    参数说明

    1. url 需要连接数据库的位置(网址)
    2. user 用户名
    3. password 密码

    扩展说明:

    URL:SUN公司与数据库厂商之间的一种协议。

    jdbc:mysql://localhost:3306/day04

    协议:子协议:/ IP :端口号数据库  

    mysql: jdbc:mysql://localhost:3306/day04 或者 jdbc:mysql:///day04(默认本机连接) ​

    oracle数据库: jdbc:oracle:thin:@localhost:1521:sid

     

    l 3) java.sql.Connection接口: 连接对象

    接口的实现在数据库驱动中。所有与数据库交互都是基于连接对象的

    Statement createStatement();//创建操作sql语句的对象

    l 4)java.sql.Statement 接口:操作sql语句, 并返回相应结果

    String sql = "某SQL语句";

    获取Statement语句执行平台:

    Statement stmt =con.createStatement();

     

    常用方法:

    int executeUpdate(String sql);--执行insert update delete语句.

    ResultSet executeQuery(String sql); --执行select语句.

    boolean execute(String sql); --仅当执行select并且有结果时才返回true,执行其他的语句返回false.

     

    l 5)处理结果集 :resultSet

    注意 : 执行 insert、update、delete无需处理结果集

    ResultSet实际上就是一张二维的表格,我们可以调用其boolean next()方法指向某行记录,当第一次调用next()方法时,便指向第一行记录的位置,这时就可以使用ResultSet提供的getXXX(int col)方法来获取指定列的数据:(与数组索引从0开始不同,这里索引从1开始)

    rs.next();//指向第一行

    rs.getInt(1);//获取第一行第一列的数据

     

    获取数据常用方法:

    Object getObject(int index) / Object getObject(String name) 获得任意对象

    String getString(int index) / String getString(String name) 获得字符串

    int getInt(int index) / int getInt(String name) 获得整形

    double getDouble(int index) / double getDouble(String name)获得双精度浮点型

     

    l 6)释放资源

    与IO流一样,使用后的东西都需要关闭!关闭的顺序是先得到的后关闭,后得到的先关闭。

    rs.close();

    stmt.close();

    con.close();

     

    5. Jdbc工具类

    “获得数据库连接”操作,将在以后的增删改查所有功能中都存在,可以封装工具类JDBCUtils。提供获取连接对象的方法,从而达到代码的重复利用。

    public class JdbcUtils {

     

    private static String driver = "com.mysql.jdbc.Driver";

    private static String url = "jdbc:mysql://localhost:3306/webdb_4";

    private static String user = "root";

    private static String password = "root";

     

    static{

    try {

    //注册驱动

    Class.forName(driver);

    } catch (Exception e) {

    throw new RuntimeException(e);

    }

     

    }

     

    /**

     * 获得连接

     * @return

     * @throws SQLException

     */

    public static Connection getConnection() throws  SQLException{

    //获得连接

    Connection conn = DriverManager.getConnection(url, user, password);

    return conn;

    }

     

    /**

     * 释放资源

     * @param conn

     * @param st

     * @param rs

     */

    public static void closeResource(Connection conn , Statement st , ResultSet rs){

     

    if(rs != null){

    try {

    rs.close();

    } catch (SQLException e) {

    }

    }

     

    if(st != null){

    try {

    st.close();

    } catch (SQLException e) {

    }

    }

     

    if(conn != null){

    try {

    conn.close();

    } catch (SQLException e) {

    }

    }

     

    }

    }

     

    6. Jdbc增删改查操作

    l 插入操作

    @Test

    public void demo01(){

    //添加

     

    Connection conn = null;

    Statement st = null;

    ResultSet rs = null;

     

    try {

    //1 获得连接

    conn = JdbcUtils.getConnection();

     

    //操作

    //1) 获得语句执行者

    st = conn.createStatement();

    //2) 执行sql语句

    int r = st.executeUpdate("insert into category(cname) values('测试')");

     

    //3) 处理结果

    System.out.println(r);

     

    } catch (Exception e) {

    throw new RuntimeException(e);

    } finally{

    //释放资源

    JdbcUtils.closeResource(conn, st, rs);

    }

    }

     

    l 修改操作

    @Test

    public void demo02(){

    //修改

    Connection conn = null;

    Statement st = null;

    ResultSet rs = null;

     

    try {

    conn = JdbcUtils.getConnection();

     

    st = conn.createStatement();

    int r = st.executeUpdate("update category set cname='测试2' where cid = 4");

    System.out.println(r);

     

    } catch (Exception e) {

    throw new RuntimeException(e);

    } finally{

    JdbcUtils.closeResource(conn, st, rs);

    }

    }

    l 删除

    @Test

    public void demo03(){

    //删除

    Connection conn = null;

    Statement st = null;

    ResultSet rs = null;

     

    try {

    conn = JdbcUtils.getConnection();

     

    //操作

    st = conn.createStatement();

    int r = st.executeUpdate("delete from category where cid = 4");

    System.out.println(r);

     

    } catch (Exception e) {

    throw new RuntimeException(e);

    } finally{

    JdbcUtils.closeResource(conn, st, rs);

    }

     

    }

    l 通过id查询详情

    @Test

    public void demo04(){

    //通过id查询详情

    Connection conn = null;

    Statement st = null;

    ResultSet rs = null;

     

    try {

    conn = JdbcUtils.getConnection();

     

    //操作

    st = conn.createStatement();

    rs = st.executeQuery("select * from category where cid = 30");

     

    if(rs.next()){

    String cid = rs.getString("cid");

    String cname = rs.getString("cname");

    System.out.println(cid + " @ " + cname );

    } else {

    System.out.println("没有数据");

    }

     

    } catch (Exception e) {

    throw new RuntimeException(e);

    } finally{

    JdbcUtils.closeResource(conn, st, rs);

    }

    }

    l 查询所有

    @Test

    public void demo05(){

    //查询所有

    Connection conn = null;

    Statement st = null;

    ResultSet rs = null;

     

    try {

    conn = JdbcUtils.getConnection();

     

    //操作

    st = conn.createStatement();

    rs = st.executeQuery("select * from category");

     

    while(rs.next()){

    String cid = rs.getString("cid");

    String cname = rs.getString("cname");

    System.out.println(cid + " @ " + cname );

    }

     

    } catch (Exception e) {

    throw new RuntimeException(e);

    } finally{

    JdbcUtils.closeResource(conn, st, rs);

    }

    }

    四、预编译执行平台

    1. SQL注入问题

    SQL注入:用户输入的内容作为了SQL语句语法的一部分,改变了原有SQL真正的意义。

    假设有登录SQL语句如下:

    SELECT * FROM 用户表 WHERE NAME = 用户输入的用户名 AND PASSWORD = 用户输的密码;

    此时,当用户输入正确的账号与密码后,查询到了信息则让用户登录。但是当用户输入的账号为XXX 密码为:XXX’ OR ‘a’=’a时,则真正执行的代码变为:

    SELECT * FROM 用户表 WHERE NAME = ‘XXX’ AND PASSWORD =’ XXX’  OR ’a’=’a’;

    此时,上述查询语句时永远可以查询出结果的。那么用户就直接登录成功了,显然我们不希望看到这样的结果,这便是SQL注入问题。 为此,我们使用PreparedStatement来解决对应的问题。

    7. API详解:预处理对象

    preparedStatement:预编译对象,是Statement对象的子类。

     

    特点:

    l 性能高

    l 会把SQL语句先编译

    l 能过滤掉用户输入的关键词

     

    PreparedStatement预处理对象,处理的每条sql语句中所有的实际参数,都必须使用占位符?替换。

    String sql = "select * from user where username = ? and password = ?";

    PreparedStatement使用,需要通过以下3步骤完成:

    1. PreparedStatement预处理对象代码:

    // 获得预处理对象,需要提供已经使用占位符处理后的SQL语句

    PreparedStatement psmt = conn.prepareStatement(sql)

    1. 设置实际参数

    void setXxx(int index, Xxx xx) 将指定参数设置指定类型的值

    参数1:index 实际参数序列号,从1开始。

    参数2:xxx 实际参数值,xxx表示具体的类型。

    例如:

    setString(2, "1234") 把SQL语句中第2个位置的占位符?替换成实际参数 "1234"

    1. 执行SQL语句

    int executeUpdate(); --执行insert update delete语句.

    ResultSet executeQuery(); --执行select语句.

    boolean execute(); --执行select返回true 执行其他的语句返回false.

    8. 基于预处理实现CURD操作

    l 1) 插入操作

    @Test

    public void demo01(){

    //添加:向分类表中添加数据

    Connection conn = null;

    PreparedStatement psmt = null;

    ResultSet rs = null;

     

    try {

    //1 获得连接

    conn = JdbcUtils.getConnection();

    //2 处理sql语句

    String sql = "insert into category(cname) values(? )";

    //3获得预处理对象

    psmt = conn.prepareStatement(sql);

    //4设置实际参数

    psmt.setString(1,"预处理");

    //5执行

    int r = psmt.executeUpdate();

     

    System.out.println(r);

     

    } catch (Exception e) {

    throw new RuntimeException(e);

    } finally{

    //6释放资源

    JdbcUtils.closeResource(conn, psmt, rs);

    }

    }

    l 2) 更新操作

    @Test

    public void demo02(){

    //修改

    Connection conn = null;

    PreparedStatement psmt = null;

    ResultSet rs = null;

     

    try {

    conn = JdbcUtils.getConnection();

     

    //1 sql语句

    String sql = "update category set cname = ? where cid = ?";

    //2 获得预处理对象

    psmt = conn.prepareStatement(sql);

    //3设置实际参数

    psmt.setString(1, "测试数据");

    psmt.setInt(2, 4);

    //4执行

    int r = psmt.executeUpdate();

    System.out.println(r);

     

     

    } catch (Exception e) {

    throw new RuntimeException(e);

    } finally{

    JdbcUtils.closeResource(conn, psmt, rs);

    }

    }

    l 3) 通过id查询详情

    @Test

    public void demo05(){

    //通过id查询

    Connection conn = null;

    PreparedStatement psmt = null;

    ResultSet rs = null;

     

    try {

    conn = JdbcUtils.getConnection();

     

    String sql = "select * from category where cid = ?";

    psmt = conn.prepareStatement(sql);

    psmt.setInt(1, 2);

    rs = psmt.executeQuery();

    if(rs.next()){

    System.out.println("查询到");

    } else {

    System.out.println("查询不到");

    }

     

     

    } catch (Exception e) {

    throw new RuntimeException(e);

    } finally{

    JdbcUtils.closeResource(conn, psmt, rs);

    }

    }

    五、使用连接池重写工具类

    1. 连接池原理

    连接池理解为存放多个连接的集合。

     

     

    使用连接池技术的目的:解决建立数据库连接耗费资源和时间很多的问题,提高性能。

    9. 编写标准的数据源(规范)

    Java为数据库连接池提供了公共的接口:javax.sql.DataSource,各个厂商需要让自己的连接池实现这个接口。这样应用程序可以方便的切换不同厂商的连接池!

    常见的连接池:C3P0、DRUID。

    10. C3P0连接池工具类实现

    C3P0开源免费的连接池!目前使用它的开源项目有:Spring、Hibernate等。使用C3P0连接池需要导入jar包,c3p0使用时还需要添加配置文件“c3p0-config.xml”

     

    使用步骤:

    添加jar包

    编写配置文件 c3p0-config.xml,放在src中(注:文件名一定不要写错)

    编写工具类

    l 编写配置文件 c3p0-config.xml

    <c3p0-config>

       <!-- 使用默认的配置读取连接池对象 -->

       <default-config>

         <!--  连接参数 -->

         <property name="driverClass">com.mysql.jdbc.Driver</property>

         <property name="jdbcUrl">jdbc:mysql://localhost:3306/day03</property>

         <property name="user">root</property>

         <property name="password">root</property>

     

         <!-- 连接池参数 -->

         <property name="initialPoolSize">5</property>

         <property name="maxPoolSize">10</property>

         <property name="checkoutTimeout">2000</property>

         <property name="maxIdleTime">1000</property>

       </default-config>

     </c3p0-config>

    说明: c3p0 连接池常用的配置参数说明:

    initialPoolSize : 初始连接数  刚创建好连接池的时候准备的连接数量

    maxPoolSize : 最大连接数 连接池中最多可以放多少个连接

    checkoutTimeout : 最大等待时间 连接池中没有连接时最长等待时间

    maxIdleTime : 最大空闲回收时间 连接池中的空闲连接多久没有使用就会回收

    l 编写C3P0工具类

    public class JdbcUtils {

        //创建一个C3P0的连接池对象(使用c3p0-config.xml中default-config标签中对应的参数)

        public static DataSource ds = new ComboPooledDataSource();

     

        //从池中获得一个连接

        public static Connection getConnection() throws SQLException {

            return ds.getConnection();

        }

     

        //释放资源

        public static void closeAll(ResultSet rs, Statement stmt, Connection conn){

            if (rs != null) {

                try {

                    rs.close();

                } catch (SQLException e) {

                    throw new RuntimeException(e);

                }

                rs = null;

            }

            if (stmt != null) {

                try {

                    stmt.close();

                } catch (SQLException e) {

                    throw new RuntimeException(e);

                }

                stmt = null;

            }

            if (conn != null) {

                try {

                    conn.close();

                } catch (SQLException e) {

                    throw new RuntimeException(e);

                }

                conn = null;

            }

        }

    }

    l C3p0连接池工具类的使用

    public class Demo {

    public static void main(String[] args) throws Exception {

    // 拿到连接

    Connection conn = JdbcUtils.getConnection();

     

    // 执行sql语句

    String sql = "INSERT INTO student VALUES (NULL, ?, ?, ?);";

    PreparedStatement pstmt = conn.prepareStatement(sql);

    pstmt.setString(1, "李四");

    pstmt.setInt(2, 30);

    pstmt.setDouble(3, 50);

    int i = pstmt.executeUpdate();

    System.out.println("影响的函数: " + i);

     

    // 关闭资源

    JdbcUtils.close(conn, pstmt);

    }

    }

    六、事务操作

    事务概述

    事务指的是逻辑上的一组操作,组成这组操作的各个单元要么全都成功,要么全都失败.

    事务作用:保证在一个事务中多次SQL操作要么全都成功,要么全都失败. 安全性,完整性

    1. Mysql事务操作

    sql语句

    描述

    start transaction

    开启事务

    commit

    提交事务

    rollback

    回滚事务

    l 准备数据

    # 创建一个表:账户表.

    create database webdb;

    # 使用数据库

    use webdb;

    # 创建账户表

    create table account(

    id int primary key auto_increment,

    name varchar(20),

    money double

    );

    # 初始化数据

    insert into account values (null,'jack',10000);

    insert into account values (null,'rose',10000);

    insert into account values (null,'tom',10000);

    l 操作

    n MYSQL中可以有两种方式进行事务的管理

    u 自动提交:MySql默认自动提交。及执行一条sql语句提交一次事务。

    u 手动提交:先开启,再提交

    l 方式1:手动提交

    start transaction;

    update account set money=money-1000 where name='jack';

    update account set money=money+1000 where name='rose';

    commit;

    #或者

    rollback;

    l 方式2:自动提交,通过修改mysql全局变量“autocommit”进行控制

    11. 事务特征: ACID

    原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

    一致性(Consistency)事务前后数据的完整性必须保持一致。

    隔离性(Isolation)事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。

    持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

    12. 并发访问问题

    如果不考虑隔离性,事务存在3中并发访问问题。

    l 脏读:一个事务读到了另一个事务未提交的数据.

    l 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。

    l 虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事务,在事务中多次查询的结果不一致。

    13. 隔离级别: 解决问题

    数据库规范规定了4种隔离级别,分别用于描述两个事务并发的所有情况。

    read uncommitted 读未提交,一个事务读到另一个事务没有提交的数据。

    a) 存在:3个问题(脏读、不可重复读、虚读)。

    b) 解决:0个问题

    read committed 读已提交,一个事务读到另一个事务已经提交的数据。

    a) 存在:2个问题(不可重复读、虚读)。

    b) 解决:1个问题(脏读)

    repeatable read:可重复读,在一个事务中读到的数据始终保持一致,无论另一个事务是否提交。

    a) 存在:1个问题(虚读)。

    b) 解决:2个问题(脏读、不可重复读)

    serializable 串行化,同时只能执行一个事务,相当于事务中的单线程。

    a) 存在:0个问题。

    b) 解决:3个问题(脏读、不可重复读、虚读)

    安全和性能对比

    安全性:serializable > repeatable read > read committed > read uncommitted

    性能 : serializable < repeatable read < read committed < read uncommitted

    常见数据库的默认隔离级别:

    MySql:repeatable read

    Oracle:read committed

    14. Jdbc事务操作

    Connection 对象的方法名

    描述

    conn.setAutoCommit(false)

    开启事务

    conn.commit()

    提交事务

    conn.rollback()

    回滚事务

     

    代码操作:

    public class Demo01JDBC {

        public static void main(String[] args) {

            Connection conn = null;

            Statement stat = null;

            try {

                //1.注册驱动

                Class.forName("com.mysql.jdbc.Driver");

                //2.获取数据库连接对象Connection

                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/day04", "root", "root");

     

                //开启事务

                conn.setAutoCommit(false);

     

            //3.获取执行sql语句的执行者对象Statement

            stat = conn.createStatement();

            //4.执行sql语句,获取结果

            int row1 = stat.executeUpdate("update account set money=money-1000 where name='jack';");

            System.out.println(0/0);

            int row2 = stat.executeUpdate("update account set money=money+1000 where name='rose';");

            //5.处理结果

            if(row1>0 && row2>0){

                System.out.println("转账成功!");

     

                //提交事务

                conn.commit();

            }

        } catch (Exception e) {

            e.printStackTrace();

            System.out.println("转账失败!");

     

            //回滚事务

            try {

                conn.rollback();

            } catch (SQLException e1) {

                e1.printStackTrace();

            }

        } finally {

            //6.释放资源

            if(stat!=null){

                try {

                    stat.close();

                } catch (SQLException e) {

                    e.printStackTrace();

                }

            }

            if(conn!=null){

                try {

                    conn.close();

                } catch (SQLException e) {

                    e.printStackTrace();

                }

            }

        }

    }

    }

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

  • 相关阅读:
    经典笑话集1
    什么是无知——郎咸平
    施工导截流方案辅助设计软件的总体功能(首次公开)
    流行水利水电工程施工导截流软件的架构存在的问题
    GoogleEarth二次开发(资料二)
    水利水电工程施工导截流设计现状
    软件开发定律21条[转]
    GoogleEarth二次开发(资料一)
    我们的软件即将要交付成果了!
    javascript 常用小例子收集
  • 原文地址:https://www.cnblogs.com/shan13936/p/13904377.html
Copyright © 2011-2022 走看看