七、JDBC 与 DAO 模式

七、JDBC 与 DAO 模式

7.1 JDBC 介绍

7.1.1 什么是 JDBC

JDBC(Java Database Conectivity)

Java数据库连接技术的简称,提供连接各种常用数据库的能力

7.1.2 JDBC 的工作原理

JDBC API

内容:供程序员调用的接口与类,集成在java.sql和javax.sql包中,如

  • DriverManager类
  • Connection接口
  • Statement接口
  • ResultSet接口

DriverManager

  • 作用:管理各种不同的JDBC驱动

JDBC 驱动

  • 提供者:数据库厂商
  • 作用:负责连接各种不同的数据库

7.1.3 JDBC API

JDBC API 主要功能

  • 与数据库建立连接、执行SQL 语句、处理结果

  • DriverManager :依据数据库的不同,管理JDBC驱动

  • Connection :负责连接数据库并担任传送数据的任务

  • Statement :由 Connection 产生、负责执行SQL语句

  • esultSet:负责保存Statement执行后的查询结果


7.2 使用 JDBC 连接数据库

7.2.1 导入 JDBC 驱动 JAR 包

数据库版本:MySQL5.7

MySQL官网下载对应的JDBC驱动JAR包

  • mysql-connector-java-8.0.19.jar

驱动类

  • com.mysql.cj.jdbc.Driver

7.2.2 纯 Java 驱动方式

使用纯Java方式连接数据库

  • 由JDBC驱动直接访问数据库

  • 优点:完全Java代码,快速、跨平台

  • 缺点:访问不同的数据库需要下载专用的JDBC驱动

  • JDBC驱动由数据库厂商提供

7.2.3 JDBC编程模板

try {
      Class.forName(JDBC驱动类);
      # 1.加载JDBC驱动 
} catch (ClassNotFoundException e) {
    //异常输出代码
} //… …
try {
      Connection con=DriverManager.getConnection(数据连接字符串,数据库用户名,密码);
      // 2.与数据库建立连接 
      
      Statement stmt = con.createStatement();
      ResultSet rs = stmt.executeQuery("SELECT a, b, c FROM table1;");
	// 3.发送SQL语句,并得到返回结果 
      while (rs.next()) {
             int x = rs.getInt("a");
             String s = rs.getString("b");
             float f = rs.getFloat("c");
      }
      // 4.处理返回结果 
      rs.close();
      stmt.close();   
      con.close();
      // 5.释放资源
} //… …

7.2.4 数据库连接字符串

jdbc:数据库://ip:端口/数据库名称[连接参数=参数值]
  • 数据库:表示JDBC连接的目标数据库
  • ip: 表示JDBC所连接的目标数据库地址,如果是本地数据库,可为localhost,即本地主机名
  • 端口:连接数据库的端口号
  • 数据库名称:是目标数据库的名称
  • 连接参数:连接数据库时的参数配置

连接本地MySQL中hospital数据库

jdbc:mysql://localhost:3306/hospital?serverTimezone=GMT-8
// 我国处于东八区,时区设置为GMT-8

7.2.5 Connection 接口

Connection是数据库连接对象的类型

方法 作用
Statement createStatement() 创建一个Statement对象将SQL语句发送到数据库
PreparedStatement prepareStatement(String sql) 创建一个PreparedStatement对象,将参数化的SQL语句发送到数据库
boolean isClosed() 查询此Connection对象是否已经被关闭。如果已关闭,则返回true;否则返回false
void close() 立即释放此Connection对象的数据库和JDBC资源

7.2.6 连接本地 hospital 数据库

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.*;

public class HospitalConn {
    private static Logger logger = LogManager.getLogger(HospitalConn.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC","root", "root");
            System.out.println("数据库连接成功");
        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 3、关闭数据库连接
            try {
                if (null != conn) {
                    conn.close();
                    System.out.println("数据库连接断开");
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.2.7 常见异常

使用JDBC连接数据库时,经常出现的错误

  • JDBC驱动类的名称书写错误,导致ClassNotFoundException异常
  • 数据连接字符串、数据库用户名、密码错误,导致SQLException异常
  • 数据库操作结束后,没有关闭数据库连接,导致仍旧占有系统资源
  • 关闭数据库连接语句没有放到finally语句块中,导致语句可能没有被执行

7.3 Statement 操作数据库

Java执行数据库操作的一个重要接口,在已经建立数据库连接的基础上,向数据库发送要执行的SQL语句

  • Statement对象:执行不带参数的简单SQL语句
  • PreparedStatement对象:执行带或不带In参数的预编译SQL语句
方法 作用
ResultSet executeQuery(String sql) 可以执行SQL查询并获取ResultSet对象
int executeUpdate(String sql) 可以执行插入、删除、更新的操作,返回值是执行该操作所影响的行数
boolean execute(String sql) 可以执行任意SQL语句。如果结果为 ResultSet 对象,则返回 true;如果其为更新计数或者不存在任何结果,则返回false

使用 executeQuery() 和 executeUpdate() 方法都需要啊传入 SQL 语句,因此,需要在 Java 中通过字符串拼接获得 SQL 字符串

7.3.1 Java 的字符串操作

String类

  • 字符串常量一旦声明则不可改变
  • String类对象可以改变,但改变的是其内存地址的指向
  • 使用“+”作为数据的连接操作
  • 不适用频繁修改的字符串操作

StringBuffer类

  • StringBuffer类对象能够被多次修改,且不产生新的未使用对象
  • 使用append()方法进行数据连接
  • 适用于字符串修改操作
  • 是线程安全的,支持并发操作,适合多线程

如果使用StringBuffer 生成了 String 类型字符串,可以通过 toString( ) 方法将其转换为一个 String 对象

  • 需要拼接的字符串
String patientName="李明";
String gender="男";
String birthDate="2010-09-03";
  • 使用+拼接字符串
//使用+拼接字符串
String sql = "insert into patient (patientName,gender,birthDate) values('"+
	patientName+"','"+
	gender+"','"+
	birthDate+"');";
System.out.println(sql);
  • 使用StringBuffer拼接字符串
//使用StringBuffer拼接字符串
StringBuffer sbSql = new StringBuffer("insert into patient (patientName,gender,birthDate)" +
      "  values('");sbSql.append(patientName+"','");
sbSql.append(gender+"','");
sbSql.append(birthDate+"');");
sql = sbSql.toString();
System.out.println(sql);
  • SQL语句中,字符"'是等效的
  • 但在Java代码中拼接字符串时使用字符'会使代码更加清晰
  • 也不容易出错引号、逗号或括号等符号必须成对出现
  • 可在控制台输出拼接后的字符串,检查SQL语句是否正确

7.3.1 Statement 插入数据

使用Statement接口执行插入数据的操作的方法

  • executeUpdate()方法
  • execute()方法

如果希望得到插入成功的数据行数,可以使用executeUpdate()方法;否则,使用execute()方法

实现步骤

  • 声明Statement变量
  • 创建Statement对象
  • 构造SQL语句
  • 执行数据插入操作
  • 关闭Statement对象
  • 关闭顺序是后创建的对象要先关闭释放资源

演示案例

使用JDBC,向hospital数据库病人表中添加一个新的病人记录关键代码

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class HospitalInsert {
    private static Logger logger = LogManager.getLogger(HospitalInsert.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        String name = "张菲";
        // 姓名
        String gender = "女";
        // 性别
        String birthDate = "1995-02-12";
        // 出生日期
        String phoneNum = "13887676500";
        // 联系电话
        String email = "[email protected]";
        //邮箱
        String password = "909000";
        //密码
        String identityNum = "610000199502126100";
        //身份证号
        String address = "北京市";
        //地址
        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC", "root", "root");
            // 创建Statement对象
            stmt = conn.createStatement();
            //构造SQL
            StringBuffer sbSql = new StringBuffer("insert into patient (patientName,gender,birthDate,phoneNum,email,password,identityNum,address) values ( '");
            sbSql.append(name + "','");
            sbSql.append(gender + "','");
            sbSql.append(birthDate + "','");
            sbSql.append(phoneNum + "','");
            sbSql.append(email + "','");
            sbSql.append(password + "','");
            sbSql.append(identityNum + "','");
            sbSql.append(address + "');");
            System.out.println(sbSql.toString());
            //3、执行插入操作
            stmt.execute(sbSql.toString());
        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 4、关闭数据库连接
            try {
                if (null != stmt) {
                    stmt.close();
                }
                if (null != conn) {
                    conn.close();
                    System.out.println("数据库连接断开");
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

为了避免可能出现的乱码问题,可将指定数据库连接的编码集为UTF8,多个参数间使用字符&进行分隔

jdbc:mysql://localhost:3306/hospital?serverTimezone=GMT-8&useUnicode=true&characterEncoding=utf-8

7.3.2 Statement 更新数据

  • 使用executeUpdate()方法或execute()方法实现更新数据的操作
  • 使用Statement接口更新数据库中的数据的步骤与插入数据类似

实现步骤

  • 声明Statement变量
  • 创建Statement对象
  • 构造SQL语句
  • 执行数据更新操作
  • 关闭Statement对象

需关注拼接的SQL字符串,以避免出错

演示案例

使用JDBC,将hospital数据库中patientID为13的病人电话更新为13627395833

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class HospitalUpdate {
    private static Logger logger = LogManager.getLogger(HospitalUpdate.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        int patientID = 13;
        // 病人编号
        String phoneNum = "13627395833";
        // 联系电话
        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC", "root", "root");
            // 创建Statement对象
            stmt = conn.createStatement();
            //构造SQL
            StringBuffer sbSql = new StringBuffer("update patient ");
            sbSql.append("set phoneNum='" + phoneNum + "' ");
            sbSql.append("where patientID=" + patientID + ";");
            System.out.println(sbSql.toString());
            //3、执行插入更新操作
            int effectRowNum = stmt.executeUpdate(sbSql.toString());
            System.out.println("更新数据的行数:" + effectRowNum);
        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 4、关闭数据库连接
            try {
                if (null != stmt) {
                    stmt.close();
                }
                if (null != conn) {
                    conn.close();
                    System.out.println("数据库连接断开");
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.3.3 ResultSet 接口

保存和处理Statement执行后所产生的查询结果

  • 由查询结果组成的一个二维表
  • 每行代表一条记录
  • 每列代表一个字段
方法 说明
boolean next() 将游标从当前位置向下移动一行
void close() 关闭ResultSet 对象
int getInt(int colIndex) 以int形式获取结果集当前行指定列号值
int getInt(String colLabel) 以int形式获取结果集当前行指定列名值
float getFloat(int colIndex) 以float形式获取结果集当前行指定列号值
float getFloat(String colLabel) 以float形式获取结果集当前行指定列名值
String getString(int colIndex) 以String形式获取结果集当前行指定列号值
String getString(String colLabel) 以String形式获取结果集当前行指定列名值
  • 要从中获取数据的列号或列名可作为方法的参数
  • 根据值的类型选择对应的方法

ResultSet 接口 getXxx() 方法

  • 获取当前行中某列的值
  • 要从中获取数据的列号或列名可作为方法的参数
  • 根据值的类型选择对应的方法
int类型       ->   getInt()
float类型    ->   getFloat()
String类型  ->   getString()

假设结果集的第一列为patientID,存储类型为int类型,能够获得该列值的两种方法

//使用列号提取数据
int id = rs.getInt(1);
//使用列名提取数据
int id = rs.getInt("patientID");
  • 列号从1开始计数,与数组下标从0开始计数不同
  • 采用列名来标识列可读性强,且不容易出错

7.3.4 Statement 和 ResultSet 查询数据

使用 JDBC 从 hospital 数据库中查询前3个病人的编号、姓名、性别、住址信息并输出到控制台上

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.*;

public class HospitalQuery {
    private static Logger logger = LogManager.getLogger(HospitalQuery.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        int patientID = 13;
        // 病人编号
        String phoneNum = "13627395833";
        // 联系电话
        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC", "root", "root");
            System.out.println("建立连接成功 !");
            // 创建Statement对象
            stmt = conn.createStatement();
            //构造SQL
            String sql = "select patientID,patientName,gender,address from patient limit 3;";
            //3、执行查询更新操作
            rs = stmt.executeQuery(sql);
            //4、移动指针遍历结果集并输出查询结果
            while (rs.next()) {
                System.out.println(rs.getInt("patientID") + "\t" +
                        rs.getString("patientName") + "\t" +
                        rs.getString("gender") + "\t" +
                        rs.getString("address"));
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != stmt) {
                    stmt.close();
                }
                if (null != conn) {
                    conn.close();
                    System.out.println("数据库连接断开");
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.4 PreparedStatement 操作数据库

7.4.1 SQL 注入攻击

通过提交一段SQL代码,执行超出用户访问权限的数据操作称为SQL注入(SQL Injection),SQL注入攻击是应用安全领域的一种常见攻击方式,会造成的数据库安全风险包括:刷库、拖库和撞库等,主要是没有对用户输入数据的合法性进行判断,导致应用程序存在安全隐患

使用JDBC实现医院管理系统用户登录验证功能

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.*;
import java.util.Scanner;

public class HospitalLogin {
    private static Logger logger = LogManager.getLogger(HospitalLogin.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        //根据控制台提示输入用户身份证号和密码
        Scanner input = new Scanner(System.in);
        System.out.println("用户登录");
        System.out.print("请输入身份证号:");
        String identityNum = input.next();
        System.out.print("请输入密码:");
        String password = input.next();

        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC", "root", "123456");
            // 创建Statement对象
            stmt = conn.createStatement();
            //构造SQL
            StringBuffer sbSql = new StringBuffer("SELECT patientName FROM patient WHERE ");
            sbSql.append("password='" + password + "'");
            sbSql.append(" and identityNum='" + identityNum + "';");
            //3、执行查询更新操作
            rs = stmt.executeQuery(sbSql.toString());
            System.out.println(sbSql.toString());
            //4、验证用户名和密码
            if (rs.next()) {
                System.out.println("欢迎" + rs.getString("patientName") + "登录系统!");
            } else {
                System.out.println("密码错误!");
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != stmt) {
                    stmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LWZU5FBo-1680617691979)(./assets/%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20230404172438.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oFRlcayJ-1680617691982)(./assets/%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20230404172502.png)]

修改查询结构

7.4.2 PreparedStatement 接口

使用PreparedStatement 接口

  • 继承自 Statement接口
  • 与Statement对象相比,使用更加灵活,更有效率

PreparedStatement接口 (预编译的 SQL 语句)

  • 提高代码可读性和可维护性

  • 提高安全性

  • 提高SQL语句执行的性能

方 法 作 用
boolean execute() 执行SQL语句,可以是任何SQL语句。如果结果是Result对象,则返回true。如果结果是更新计数或没有结果,则返回false
ResultSet executeQuery() 执行SQL查询,返回该查询生成的ResultSet对象
int executeUpdate() 执行SQL语句,该语句必须是一个DML语句,比如:INSERT、UPDATE或DELETE语句;或者是无返回内容的SQL语句,比如DDL语句。返回值是执行该操作所影响的行数
void setXxx(int index,xxx x) 方法名Xxx和第二个参数的xxx均表示(如int,float,double等)基本数据类型,且两个类型需一致,参数列表中的x表示方法的形式参数。把指定数据类型(xxx)的值x设置给index位置的参数。根据参数类型的不同,常见方法有:setInt(int index,int x) 、setFloat(int index,float x)、setDouble(int index,double x)等
void setObject(int index,Object x) 除基本数据类型外,参数类型也可以是Object,可以将Object对象x设置给index位置的参数

7.4.3 PreparedStatement 操作数据

创建PreparedStatement对象

  • 使用Connection接口prepareStatement(String sql)方法创建PreparedStatement对象
  • 需要提前设置该对象将要执行的SQL语句
  • SQL语句可具有一个或多个输入参数

设置输入参数的值

  • 调用setXxx()方法完成参数赋值

执行SQL语句

  • 调用PreparedStatement接口
  • executeQuery()
  • executeUpdate()
  • execute()
  • 方法执行SQL语句

验证用户输入的身份证号和密码

  • 如果通过验证,则输出“欢迎[姓名]登录系统!”的信息;
  • 否则输出“密码错误!”
package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.*;
import java.util.Scanner;

public class HospitalLogin {
    private static Logger logger = LogManager.getLogger(HospitalLogin.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        //根据控制台提示输入用户身份证号和密码
        Scanner input = new Scanner(System.in);
        System.out.println("用户登录");
        System.out.print("请输入身份证号:");
        String identityNum = input.next();
        System.out.print("请输入密码:");
        String password = input.next();

        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC", "root", "123456");

            //3、构造PreparedStatement对象
            pstmt = conn.prepareStatement("SELECT patientName FROM patient WHERE identityNum=? and password=?");
            pstmt.setString(1, identityNum);
            pstmt.setString(2, password);
            rs = pstmt.executeQuery();
            //4、验证用户名和密码
            if (rs.next()) {
                System.out.println("欢迎" + rs.getString("patientName") + "登录系统!");
            } else {
                System.out.println("密码错误!");
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != pstmt) {
                    pstmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.4.4 PreparedStatement 的优势

实际开发中,推荐使用PreparedStatement接口执行数据库操作

  • PreparedStatement与Statement接口相比,具有的优势
  • 可读性和可维护性高
  • SQL语句执行性能高
  • 安全性更高

7.5 Properties 配置文件

7.5.1 为什么使用 Properties 类

使用JDBC技术访问数据库数据的关键代码

private String driver = "com.mysql.jdbc.Driver";
private String url = "jdbc:mysql://localhost:3306/hospital?serverTimezone=GMT-8";
private  String user = “root"; 	
private  String password=123456"; 
// 修改后需重新编译
Connection conn = null;
public Connection getConnection() {
    if(conn==null) {
        try {
            Class.forName(driver);
            conn = DriverManager.getConnection(url, user, password);
        } catch (Exception e) {//省略代码……}
    }	
    return conn;// 返回连接对象
}

让用户脱离程序本身修改相关的变量设置——使用配置文件

7.5.2 properties配置文件

Java的配置文件常为properties文件

  • 后缀为.properties
  • 以“键=值”格式储存数据
  • 使用“#”添加注释

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vLwQh0OR-1680617691982)(./assets/%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20230404204942.png)]

通常,为数据库访问添加的配置文件是database.properties

7.5.3 读取配置文件信息

使用java.util包下的Properties类读取配置文件

方法 描述
String getProperty(String key) 用指定的键在此属性列表中搜索属性,通过参数key得到其所对应的值
Object setProperty(String key, String value) 通过调用基类Hashtable的put()方法设置键-值对
void load(InputStream inStream) 从输入流中读取属性列表 (键和元素对),通过对指定文件进行装载获取该文件中所有键-值对
void clear() 清除所装载的键-值对,该方法由基类Hashtable提供

使用Properties配置文件的方式改造医院管理系统

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;

public class HospitalSystem {
    private static Logger logger = LogManager.getLogger(HospitalSystem.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        String patientID = null;
        boolean isExist = false;
        //根据控制台提示输入用户身份证号和密码
        Scanner input = new Scanner(System.in);
        System.out.println("用户登录");
        System.out.print("请输入身份证号:");
        String identityNum = input.next();
        System.out.print("请输入密码:");
        String password = input.next();


        Properties params = new Properties();
        String configFile = "database.properties";
        //配置文件路径
        String url = null;
        String username = null;
        String pwd = null;
        //加载配置文件到输入流中
        try {
            InputStream is = HospitalSystem.class.getClassLoader().getResourceAsStream(configFile);
            params.load(is);
            //根据指定的获取对应的值
            String driver = params.getProperty("driver");
            url = params.getProperty("url");
            username = params.getProperty("username");
            pwd = params.getProperty("password");

            // 1、加载驱动
            Class.forName(driver);
        } catch (IOException e) {
            logger.error(e);
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }

        try {
            // 2、建立连接
            conn = DriverManager.getConnection(url, username, pwd);

            //3、构造PreparedStatement对象
            pstmt = conn.prepareStatement("SELECT patientID, patientName FROM patient WHERE identityNum=? and password=?");
            pstmt.setString(1, identityNum);
            pstmt.setString(2, password);
            rs = pstmt.executeQuery();
            //4、验证用户名和密码
            if (rs.next()) {
                patientID = rs.getString("patientID");
                System.out.println("欢迎" + rs.getString("patientName") + "登录系统!");
                while (!isExist) {
                    System.out.println("1.查询检查记录\t 0.退出");
                    System.out.print("请输入要执行的操作:");
                    String action = input.next();
                    if (action.equals("1")) {
                        pstmt = conn.prepareStatement("SELECT depName, checkItemName, checkResult, checkItemCost, examDate FROM prescription p  INNER JOIN department d ON p.depID = d.depID INNER JOIN checkitem c ON p.checkItemID = c.checkItemID WHERE p.patientID=?;");
                        pstmt.setString(1, patientID);
                        rs = pstmt.executeQuery();
                        System.out.println("检查科室\t检查项目\t检查结果\t检查费用\t检查时间");
                        while (rs.next()) {
                            System.out.println(rs.getString("depName") + "\t" + rs.getString("checkItemName") + "\t" + rs.getString("checkResult") + "\t" + rs.getString("checkItemCost") + "\t" + rs.getString("examDate") + "\t");
                        }
                    } else if (action.equals("0")) {
                        isExist = true;
                        System.out.println("再见");
                    } else {
                        System.out.println("输入错误,请重新输入");
                    }
                }
            } else {
                System.out.println("密码错误!");
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != pstmt) {
                    pstmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.6 DAO 模式

7.6.1 什么是 DAO

非常流行的数据访问模式——DAO模式

  • Data Access Object(数据存取对象)
  • 位于业务逻辑和持久化数据之间
  • 实现对持久化数据的访问

DAO起着转换器的作用,把实体类转换为数据库中的记录

7.6.2 DAO 模式的组成

组成部分

  • DAO接口
  • DAO实现类
  • 实体类
  • 数据库连接和关闭工具类

优势

  • 隔离了数据访问代码和业务逻辑代码
  • 隔离了不同数据库实现

7.6.3 使用实体类传递数据

数据访问代码和业务逻辑代码之间通过实体类来传输数据

业务逻辑代码
数据访问代码

实体类特征

  • 属性一般使用private修饰
  • 提供public修饰的getter/setter方法
  • 实体类提供无参构造方法,根据业务提供有参构造
  • 实现java.io.Serializable接口,支持序列化机制

7.6.4 实体类

实体类(Entity)是Java应用程序中与数据库表对应的类

  • 用于存储数据,并提供对这些数据的访问
  • 通常,实现类是持久的,需要存储于文件或数据库中
  • 访问操作数据库时,以实体类的方式组织数据库中的实体及关系
  • 通常,在Java工程中创建一个名为entity的Package,用于集中保存实体类
  • 一个数据库表对应一个实体类

7.6.5 定义实体类

package XaunZiShare;

import java.io.Serializable;

public class Patient implements Serializable {
    private static final long serialVersionUID = -8762235641468472877L;
    private String patientID;  //病人编号
    private String password; //登录密码
    private String birthDate; //出生日期
    private String gender; //性别
    private String patientName; //姓名
    private String phoneNum; //联系电话
    private String email; //邮箱
    private String identityNum; //身份证号
    private String address; //地址

    /**
     * 无参构造方法
     */
    public Patient() {

    }

    /**
     * 有参构造方法,根据需要提供
     *
     * @param identityNum 身份证号
     * @param name        姓名
     */
    public Patient(String identityNum, String name) {
        this.identityNum = identityNum;
        this.patientName = name;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public String getPatientID() {
        return patientID;
    }

    public void setPatientID(String patientID) {
        this.patientID = patientID;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getBirthDate() {
        return birthDate;
    }

    public void setBirthDate(String birthDate) {
        this.birthDate = birthDate;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getPatientName() {
        return patientName;
    }

    public void setPatientName(String patientName) {
        this.patientName = patientName;
    }

    public String getPhoneNum() {
        return phoneNum;
    }

    public void setPhoneNum(String phoneNum) {
        this.phoneNum = phoneNum;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getIdentityNum() {
        return identityNum;
    }

    public void setIdentityNum(String identityNum) {
        this.identityNum = identityNum;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

7.6.6 使用实体类传递数据

package XaunZiShare;

import com.javamysql.entity.Patient;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;

public class HospitalSystem {
    private static Logger logger = LogManager.getLogger(HospitalSystem.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        Patient patient = new Patient();
        boolean isExist = false;
        //根据控制台提示输入用户身份证号和密码
        Scanner input = new Scanner(System.in);
        System.out.println("用户登录");
        System.out.print("请输入身份证号:");
        patient.setIdentityNum(input.next());
        System.out.print("请输入密码:");
        patient.setPassword(input.next());

        Properties params = new Properties();
        String configFile = "database.properties";
        //配置文件路径
        String url = null;
        String username = null;
        String pwd = null;
        //加载配置文件到输入流中
        try {
            InputStream is = HospitalSystem.class.getClassLoader().getResourceAsStream(configFile);
            params.load(is);
            //根据指定的获取对应的值
            String driver = params.getProperty("driver");
            url = params.getProperty("url");
            username = params.getProperty("username");
            pwd = params.getProperty("password");

            // 1、加载驱动
            Class.forName(driver);
        } catch (IOException e) {
            logger.error(e);
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }

        try {
            // 2、建立连接
            conn = DriverManager.getConnection(url, username, pwd);

            //3、构造PreparedStatement对象
            pstmt = conn.prepareStatement("SELECT * FROM patient WHERE identityNum=? and password=?");
            pstmt.setString(1, patient.getIdentityNum());
            pstmt.setString(2, patient.getPassword());
            rs = pstmt.executeQuery();
            //4、验证用户名和密码
            if (rs.next()) {
                //从MySQL读取用户信息,并加载到patient对象中
                patient.setPatientID(rs.getString("patientID"));
                patient.setAddress(rs.getString("address"));
                patient.setBirthDate(rs.getString("birthDate"));
                patient.setEmail(rs.getString("email"));
                patient.setGender(rs.getString("gender"));
                patient.setPatientID(rs.getString("patientName"));
                patient.setPhoneNum(rs.getString("phoneNum"));

                System.out.println("欢迎" + patient.getPatientName() + "登录系统!");
                while (!isExist) {
                    System.out.println("1.查询检查记录\t2.查询病人信息\t 0.退出");
                    System.out.print("请输入要执行的操作:");
                    String action = input.next();
                    if (action.equals("1")) {
                        pstmt = conn.prepareStatement("SELECT depName, checkItemName, checkResult, checkItemCost, examDate FROM prescription p  INNER JOIN department d ON p.depID = d.depID INNER JOIN checkitem c ON p.checkItemID = c.checkItemID WHERE p.patientID=?;");
                        pstmt.setString(1, patient.getPatientID());
                        rs = pstmt.executeQuery();
                        System.out.println("检查科室\t检查项目\t检查结果\t检查费用\t检查时间");
                        while (rs.next()) {
                            System.out.println(rs.getString("depName") + "\t" + rs.getString("checkItemName") + "\t" + rs.getString("checkResult") + "\t" + rs.getString("checkItemCost") + "\t" + rs.getString("examDate") + "\t");
                        }
                    } else if (action.equals("2")) {
                        System.out.println(patient.getPatientID() + "\t" + patient.getPatientName() + "\t" + patient.getGender() + "\t" + patient.getBirthDate() + "\t" + patient.getIdentityNum() + "\t" + patient.getPhoneNum() + "\t" + patient.getEmail() + "\t" + patient.getAddress());
                    } else if (action.equals("0")) {
                        isExist = true;
                        System.out.println("再见");
                    } else {
                        System.out.println("输入错误,请重新输入");
                    }
                }
            } else {
                System.out.println("密码错误!");
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != pstmt) {
                    pstmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.6.7 实体类的特征

实体类特征

  • 属性一般使用private修饰
  • 提供public修饰的getter/setter方法
  • 实体类提供无参构造方法,根据业务提供有参构造
  • 实现java.io.Serializable接口,支持序列化机制,可以将该对象转换成字节序列而保存在磁盘上或在网络上传输

如果实体类实现了java.io.Serializable接口,应该定义属性serialVersionUID,解决不同版本之间的序列化问题

为serialVersionUID赋值的方法

  • 手动

  • 使用IDEA生成

  • private static final long serialVersionUID = -8762235641468472877L;
    

一旦为一个实体类的serialVersionUID赋值,就不要再修改;否则,在反序列化之前版本的数据时,会报java.io.InvalidClassException异常


7.7 实现 JDBC 封装

7.7.1 JDBC

将程序中数据在瞬时状态和持久状态间转换的机制为数据持久化

JDBC

  • 读取
  • 删除
  • 查找
  • 修改
  • 保存

7.7.2 持久化的实现方式

数据库

普通文件

XML文件

7.7.3 为什么进行 JDBC 封装

Scanner input = new Scanner(System.in);
System.out.print("请输入登录名:");
String name=input.next();
System.out.print("请输入登录密码:");
String password=input.next();
// 业务相关代码
// ……省略加载驱动
try {
    conn = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/hospital?serverTimezone=GMT-8",
                "root", "123456");
    // … …省略代码 … …
    if(rs.next())
        System.out.println("登录成功,欢迎您!");
    else
	System.out.println("登录失败,请重新输入!");	
    // ……省略代码
} catch (SQLException e) {			
    // ……省略代码
} finally {}
// 数据访问代码

业务代码和数据访问代码耦合

  • 可读性差
  • 不利于后期修改和维护
  • 不利于代码复用

采用面向接口编程,可以降低代码间的耦合性

采用面向接口编程,可以降低代码间的耦合性

业务逻辑代码
数据访问代码
MySQL
SQLServer
Oracle

业务逻辑代码调用数据访问接口

7.7.4 使用 DAO 模式改造 Hospital

将HospitalSystem中对病人的所有数据库操作抽象成接口

对病人的数据库操作包括修改病人信息、通过身份证号和密码验证登录

设计接口时,尽量以对象为单位,给调用者提供面向对象的接口

  • 使用实体类作为接口的参数和返回值,可以让接口更加清晰简洁
  • 如果以Patient类的各个属性为形参进行传递,不仅会导致参数个数很多,还会增加接口和实现类中方法的数量等

接口的命名,应以简明为主

  • “实体类名+Dao”格式如
  • PatientDao
  • 作为工程中相对独立的模块
  • 所有DAO接口文件都放在dao包中

接口由不同数据库的实现类分别实现

PatientDao 接口

package XaunZiShare;

import com.javamysql.entity.Patient;

public interface PatientDao {
    /**
     * 更新病人信息
     *
     * @param patient 病人
     */
    int update(Patient patient);

    /**
     * 根据身份证号和登录密码返回病人信息
     *
     * @param identityNum 身份证号
     * @param pwd         登录密码
     * @return 病人
     */
    Patient getPatientByIdNumAndPwd(String identityNum, String pwd);
}

PatientDao实现类的方法:update()方法

package XaunZiShare;

import com.javamysql.HospitalSystem;
import com.javamysql.dao.PatientDao;
import com.javamysql.entity.Patient;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

public class PatientDaoMySQLImpl implements PatientDao {
    private static Logger logger = LogManager.getLogger(HospitalSystem.class.getName());

    @Override
    public int update(Patient patient) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        int result = 0;

        Properties params = new Properties();
        String configFile = "database.properties";//配置文件路径
        String url = null;
        String username = null;
        String password = null;
        //加载配置文件到输入流中
        try {
            InputStream is = HospitalSystem.class.getClassLoader().getResourceAsStream(configFile);
            params.load(is);
            //根据指定的获取对应的值
            String driver = params.getProperty("driver");
            url = params.getProperty("url");
            username = params.getProperty("username");
            password = params.getProperty("password");

            // 1、加载驱动
            Class.forName(driver);
        } catch (IOException e) {
            logger.error(e);
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }

        try {
            // 2、建立连接
            conn = DriverManager.getConnection(url, username, password);

            //3、构造PreparedStatement对象
            pstmt = conn.prepareStatement("UPDATE patient SET address=?, birthDate=?, email=?, gender=?, patientName=?, phoneNum=?, identityNum=?,password=? WHERE patientID=?");
            pstmt.setString(1, patient.getAddress());
            pstmt.setString(2, patient.getBirthDate());
            pstmt.setString(3, patient.getEmail());
            pstmt.setString(4, patient.getGender());
            pstmt.setString(5, patient.getPatientName());
            pstmt.setString(6, patient.getPhoneNum());
            pstmt.setString(7, patient.getIdentityNum());
            pstmt.setString(8, patient.getPassword());
            pstmt.setString(9, patient.getPatientID());
            result = pstmt.executeUpdate();
        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != pstmt) {
                    pstmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
        return result;
    }

    @Override
    public Patient getPatientByIdNumAndPwd(String identityNum, String pwd) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        Patient patient = null;
        Properties params = new Properties();
        String configFile = "database.properties";
        //配置文件路径
        String url = null;
        String username = null;
        String password = null;
        //加载配置文件到输入流中
        try {
            InputStream is = HospitalSystem.class.getClassLoader().getResourceAsStream(configFile);
            params.load(is);
            //根据指定的获取对应的值
            String driver = params.getProperty("driver");
            url = params.getProperty("url");
            username = params.getProperty("username");
            password = params.getProperty("password");

            // 1、加载驱动
            Class.forName(driver);
        } catch (IOException e) {
            logger.error(e);
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }

        try {
            // 2、建立连接
            conn = DriverManager.getConnection(url, username, password);

            //3、构造PreparedStatement对象
            pstmt = conn.prepareStatement("SELECT * FROM patient WHERE identityNum=? and password=?");
            pstmt.setString(1, identityNum);
            pstmt.setString(2, pwd);
            rs = pstmt.executeQuery();
            //4、验证用户名和密码
            if (rs.next()) {
                //从MySQL读取用户信息,并加载到patient对象中
                patient = new Patient();
                patient.setPatientID(rs.getString("patientID"));
                patient.setAddress(rs.getString("address"));
                patient.setBirthDate(rs.getString("birthDate"));
                patient.setEmail(rs.getString("email"));
                patient.setGender(rs.getString("gender"));
                patient.setPatientName(rs.getString("patientName"));
                patient.setPhoneNum(rs.getString("phoneNum"));
                patient.setIdentityNum(rs.getString("identityNum"));
                patient.setPassword(rs.getString("password"));
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != pstmt) {
                    pstmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
        return patient;
    }
}

通用的操作是否能够进一步简化?


7.8 BaseDao基类

7.8.1 将通用的操作(打开、关闭连接等)封装到基类

package XaunZiShare;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

public class BaseDao {
    private static String driver;
    // 数据库驱动字符串
    private static String url;
    // 连接URL字符串
    private static String user;
    // 数据库用户名
    private static String password;
    // 用户密码

    // 数据连接对象
    static {//静态代码块,在类加载的时候执行
        init();
    }

    Connection conn = null;

    /**
     * 初始化连接参数,从配置文件里获得
     */
    public static void init() {
        Properties params = new Properties();
        String configFile = "database.properties";
        //配置文件路径
        //加载配置文件到输入流中
        InputStream is = BaseDao.class.getClassLoader().getResourceAsStream(configFile);
        try {
            //从输入流中读取属性列表
            params.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //根据指定的获取对应的值
        driver = params.getProperty("driver");
        url = params.getProperty("url");
        user = params.getProperty("username");
        password = params.getProperty("password");
    }

    /**
     * 获取数据库连接对象
     */
    public Connection getConnection() {
        try {
            if (conn == null || conn.isClosed()) {
                // 获取连接并捕获异常
                try {
                    Class.forName(driver);
                    conn = DriverManager.getConnection(url, user, password);
                } catch (Exception e) {
                    e.printStackTrace();
                    // 异常处理
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
        // 返回连接对象
    }

    /**
     * 关闭数据库连接
     *
     * @param conn 数据库连接
     * @param stmt Statement对象
     * @param rs   结果集
     */
    public void closeAll(Connection conn, Statement stmt, ResultSet rs) {
        // 若结果集对象不为空,则关闭
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 若Statement对象不为空,则关闭
        if (stmt != null) {
            try {
                stmt.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 若数据库连接对象不为空,则关闭
        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

7.8.2 基类 BaseDao:增、删、改的通用方法

    /**
     * 增、删、改的操作
     *
     * @param preparedSql 预编译的 SQL 语句
     * @param param       参数的字符串数组
     * @return 影响的行数
     */
    public int exceuteUpdate(String preparedSql, Object[] param) {
        PreparedStatement pstmt = null;
        int num = 0;
        conn = getConnection();
        try {
            pstmt = conn.prepareStatement(preparedSql);
            if (param != null) {
                for (int i = 0; i < param.length; i++) {
                    //为预编译sql设置参数
                    pstmt.setObject(i + 1, param[i]);
                }
            }
            num = pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            closeAll(conn, pstmt, null);
        }
        return num;
    }

7.8.3 实现类实现接口并继承 BaseDao 基类

package XaunZiShare;

import com.javamysql.HospitalSystem;
import com.javamysql.dao.BaseDao;
import com.javamysql.dao.PatientDao;
import com.javamysql.entity.Patient;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class PatientDaoMySQLImpl extends BaseDao implements PatientDao {
    private static Logger logger = LogManager.getLogger(HospitalSystem.class.getName());

    @Override
    public int update(Patient patient) {
        //构造SQL语句
        String preparedSQL = "UPDATE patient SET address=?, birthDate=?, email=?, gender=?, patientName=?, phoneNum=?, identityNum=?,password=? WHERE patientID=?";
        //构造SQL执行参数数组
        List<String> params = new ArrayList<String>();
        params.add(patient.getAddress());
        params.add(patient.getBirthDate());
        params.add(patient.getEmail());
        params.add(patient.getGender());
        params.add(patient.getPatientName());
        params.add(patient.getPhoneNum());
        params.add(patient.getIdentityNum());
        params.add(patient.getPassword());
        params.add(patient.getPatientID());
        //调用BaseDao中的更新
        return exceuteUpdate(preparedSQL, params.toArray());
    }

    @Override
    public Patient getPatientByIdNumAndPwd(String identityNum, String pwd) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        Patient patient = null;

        conn = getConnection();

        try {
            //构造PreparedStatement对象
            pstmt = conn.prepareStatement("SELECT * FROM patient WHERE identityNum=? and password=?");
            pstmt.setString(1, identityNum);
            pstmt.setString(2, pwd);
            rs = pstmt.executeQuery();
            //验证用户名和密码
            if (rs.next()) {
                //从MySQL读取用户信息,并加载到patient对象中
                patient = new Patient();
                patient.setPatientID(rs.getString("patientID"));
                patient.setAddress(rs.getString("address"));
                patient.setBirthDate(rs.getString("birthDate"));
                patient.setEmail(rs.getString("email"));
                patient.setGender(rs.getString("gender"));
                patient.setPatientName(rs.getString("patientName"));
                patient.setPhoneNum(rs.getString("phoneNum"));
                patient.setIdentityNum(rs.getString("identityNum"));
                patient.setPassword(rs.getString("password"));
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            //关闭数据库连接
            closeAll(conn, pstmt, rs);
        }
        return patient;
    }
}

此种封装JDBC的模式称为DAO模式


7.9 项目结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X6RO8XGh-1680617691983)(./assets/SRC.png)]

7.9.1 项目名

项目名其实就是反过来的网站网址

7.9.2 enrity(实体类)

这个包里面放的是都是,数据表的实体类,(POJO)。即具有,基础属性,属性封装(getter/setter),构造函数,和Tostring()的普通类

7.9.3 dao(接口类)

dao 包里面还有一层包叫``impl即实现类,dao 包下的接口类只定义方法,具体实现由impl`包下的实现类实现

7.9.4 service (业务类)

service 为业务层,将基础实现类,整合为复杂业务,与dao 包一样包内部还有一层包impl service 包下的接口类只定义方法,具体实现由impl包下的实现类实现

7.9.5 Main (运行类)

代码运行类

7.9.6 lib(依赖包)

项目所需的 JDBC 驱动 放置在此包内

7.9.7 database (连接数据库配置文件)

项目连接数据库所需要的配置文件


7.10 命名规范

直接参考阿里巴巴发布的《阿里巴巴Java开发手册(终极版)v1.3版本》

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YTAOLKOH-1680617691983)(./assets/image-20230404213509656.png)]

你可能感兴趣的:(MySQL,一篇文章入门编程系列,数据库,sql,mysql)