JDBC API 万字详解(通俗易懂)

目录

一、前言

二、JDBC API概述

三、获取连接的三种方式

        0.朝花夕拾 : 

        1.方式一 —— 通过new关键字 : 

        2.方式二 —— 通过反射机制 : 

        3.方式三 —— 通过DriverManager

                Δ方式三简化版

                Δ方式三优化版

四、 ResultSet

        1.简介 : 

        2.代码演示 : 

        3.底层实现 : 

五、SQL注入

        1.什么是SQL注入?

        2.SQL注入演示 : 

        3.PreparedStatement : 

                ①简介

                ②牛逼之处

                ③使用演示

六、总结 : 


一、前言

  • 第二节内容,up主要和大家分享一下JDBC——API方面的内容。
  • 注意事项——代码中的注释也很重要;不要眼高手低;点击文章的侧边栏目录或者文章开头的目录可以进行跳转。
  • 良工不示人以朴,所有文章都会适时补充完善。大家如果有问题都可以在评论区进行交流或者私信up。感谢阅读!

二、JDBC API概述

        JDBC API是一系列的接口,它统一和规范了应用程序与数据库的连接,执行SQL语句并得到返回结果等各类操作,相关类和接口在java.sqljavax.sql包下

                相关体系图如下(建议阅读完毕后返回来细品☕) : 


三、获取连接的三种方式

        0.朝花夕拾 : 

                上一小节内容中,我们提到了编写JDBC程序的核心四部曲,这里再来回顾一下——

  •         1° 注册驱动
  •         2° 获取连接
  •         3° 执行SQL
  •         4° 释放资源

                这里我们要重点再说一下第二个步骤——即获取数据库的连接

        1.方式一 —— 通过new关键字 : 

                这也是我们在第一小节中,演示第一个JDBC程序时用到的方法。即先通过com.mysql.cj.jdbc.Driver()来获取到Driver类对象,然后再通过Driver类中的connect方法来获取连接。connect方法的详细信息如下:

Connection connect(String url, Properties info) :需要传入一个包含数据库信息的url字符串对象,以及一个包含登录用户信息的Properties对象。

                这种方法有什么弊端?

                通过new的方法获取到Driver对象,Driver对象属于第三方,并且是静态加载,导致灵活性低,依赖性强

                up以JdbcConn类为演示类,来给大家演示一下第一种方式获取连接,其实就是把第一小节的程序演示再来一遍罢了(当然这里我们不会像第一小节讲那么细了)。
                代码如下 : 

package api.connection;

import com.mysql.cj.jdbc.Driver;
import org.testng.annotations.Test;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 */
public class JdbcConn {
//演示JDBC连接数据库的三种方式
    //1.方式一 —— new关键字静态加载
    @Test
    public  void connection_1() throws SQLException {
        Driver driver = new Driver();

        String url = "jdbc:mysql://localhost:3306/jdbc_ex";
        Properties info = new Properties();
        info.setProperty("user","root");
        info.setProperty("password","RA9_Cyan");

        Connection connect = driver.connect(url, info);
        System.out.println("方式一获取到的连接 = " + connect);

        connect.close();
        System.out.println("--------------------------------------------------");

    }
}

                运行结果 : 

        2.方式二 —— 通过反射机制 : 

                提到了灵活性和依赖性,我们就不由得想到了反射机制。反射机制可以动态的加载和构建对象,属于动态加载,相比new关键字的方式具有更高的灵活性,同时也减低了依赖性。我们可以使用 Class.forName("com.mysql.cj.jdbc.Driver"); 来获取Driver类实例。

                up仍然以JdbcConn类为演示类代码如下 : 

package api.connection;

import com.mysql.cj.jdbc.Driver;
import org.testng.annotations.Test;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

public class JdbcConn {
//演示JDBC连接数据库的三种方式
    //2.方式二 —— 反射机制
    @Test
    public void connection_2() throws ClassNotFoundException, InstantiationException, IllegalAccessException, SQLException {
        Class<?> clazz = Class.forName("com.mysql.cj.jdbc.Driver");

        Driver driver = (Driver) clazz.newInstance();

        String url = "jdbc:mysql://localhost:3306/jdbc_ex";
        Properties info = new Properties();
        info.setProperty("user", "root");
        info.setProperty("password", "RA9_Cyan");

        Connection connect = driver.connect(url, info);
        System.out.println("方式二获取到的连接 = " + connect);
        System.out.println("--------------------------------------------------");
    }
}

                运行结果 : 

        3.方式三 —— 通过DriverManager

                在反射机制的基础上,使用DriverManager替代Driver,进行统一管理,具有更好的拓展性。并且,单独定义url, user, password也具有更高的灵活性。
                需要用到DriverManager类的两个方法,如下——

  1. static void registerDriver(Driver driver) : 根据传入的Driver类对象,注册Driver驱动。
  2. static Connection getConnection(String url, String user, String password) : 根据传入的数据库URL,获取数据库连接。

                up仍然以JdbcConn类为演示类代码如下 : 

package api.connection;

import com.mysql.cj.jdbc.Driver;
import org.testng.annotations.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

public class JdbcConn {
//演示JDBC连接数据库的三种方式
    //方式三 —— 通过DriverManager
    @Test
    public void connection_3() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, SQLException {
        //使用反射机制加载Driver类
        Class<?> clazz = Class.forName("com.mysql.cj.jdbc.Driver");
        Constructor<?> constructor = clazz.getConstructor();
        Driver driver = (Driver) constructor.newInstance();

        //创建url,user,password
        String url = "jdbc:mysql://localhost:3306/jdbc_ex";
        String user = "root";
        String password = "RA9_Cyan";

        //注册Driver驱动
        DriverManager.registerDriver(driver);

        //获取连接
        Connection connection = DriverManager.getConnection(url, user, password);
        System.out.println("方式三获取到的连接= " + connection);
    }
}

                运行结果 : 

                Δ方式三简化版

                PS_1 :
                其实,在方式三的基础上,可以进行简化——
                通过Class.forName()方法动态加载Driver类后,不需要接收Class对象,也不需要获取构造器对象再得到Driver类对象。
                不需要通过DriverManager类的registerDriver方法来注册Driver驱动,即不需要注册驱动,而是直接通过getConnection方法来获取连接。

                仍然以JdbcConn类为演示类,代码如下 : 

package api.connection;

import com.mysql.cj.jdbc.Driver;
import org.testng.annotations.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

public class JdbcConn {
//演示JDBC连接数据库的三种方式
    //方式三 —— DriverManager(简化版)
    @Test
    public void connection_3() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        
        String url = "jdbc:mysql://localhost:3306/jdbc_ex";
        String user = "root";
        String password = "RA9_Cyan";

        Connection connection = DriverManager.getConnection(url, user, password);
        System.out.println("方式三简化后得到的连接 = " + connection);
    }
}

                运行结果 : 

                可以看到, 简化后,整个代码简洁了许多。
                但是,这时候可能就要有p小将(Personable小将,指风度翩翩的人)出来bb问了:👴把编写JDBC程序的核心四部曲背的比家谱都熟,第一步就是注册驱动,好家伙,隔你这儿直接给省略了?给👴爬!

                p哥先息怒,其实这里之所以能顺利获取连接,是因为jvm底层做了优化,当Driver类被动态加载时,会自动帮我们注册Driver驱动,我们查看com.mysql.cj.jdbc.Driver类的源码,可以找到一个静态代码块如下 : 

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }

                答案很明显了——当Driver类被动态加载时,静态代码块被执行。而静态代码块里的try语句中,调用了DriverManager类的registerDriver方法,完成了“注册驱动”的操作
                还要说明一点,这种“简化版”的第三种方式,是实际开发中用到最多的。

                PS_2 : 

                其实,在上述“简化版”的第三种方式中,就连调用forName的语句都可以省略。MySQL 5.1.6及以上版本无需使用forName语句;从JDK1.5以后使用了JDBC4,不再需要显示调用Class.forName(...)注册驱动,而是自动调用驱动,根据jar包下META-INF\services\java.sql.Driver文本中的类名称去注册,如下图所示 :

                但是,就像我们上面说的那样,“简化版”的方式三是实际开发中用到最多的方式,因此还是建议大家写上,以更明确。

                Δ方式三优化版

                在简化版的基础上,我们可以将url, user,以及password中的各种信息,诸如端口,数据库,用户名和用户密码等保存到properties配置文件中,使得我们的操作更加快捷和灵活。

                up先在JdbcConn类本包下,创建一个mysql.properties文件,如下图所示 : 

                JdbcConn类代码如下 : 

package api.connection;

import com.mysql.cj.jdbc.Driver;
import org.testng.annotations.Test;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

public class JdbcConn {
//演示JDBC连接数据库的三种方式
    //方式三 —— DriverManager
    @Test
    public void connection_3() throws ClassNotFoundException, SQLException, IOException {
        //通过Properties对象获取配置文件信息
        Properties properties = new Properties();
        properties.load(new FileInputStream("src/api/connection/mysql.properties"));

        //通过获取到的配置文件信息,得到对应的值
        String driver = properties.getProperty("driver");
        String url = properties.getProperty("url");
        String user = properties.getProperty("user");
        String password = properties.getProperty("password");

        //注册驱动
        Class.forName(driver);

        //获取连接
        Connection connection = DriverManager.getConnection(url, user, password);
        System.out.println("方式三优化后得到的连接 = " + connection);
    }
}

四、 ResultSet

        1.简介 : 

        ResultSet表示数据结果集的数据表,通常通过DQL(Data Query Language)来生成。ResultSet对象保持一个光标,该光标指向其当前的数据行最初,光标位于第一行之前,next方法会使光标移动到下一行,并且当ResultSet对象中没有更多行时返回false,因此可以使用While循环来遍历结果集。

        默认的ResultSet对象不可更新,并且只有一个向前移动的光标。因此,默认只能从第一行到最后一行迭代一次。但是,可以手动生成可滚动/可更新的ResultSet对象。

        PS_1 : 若有需求让光标向上移动一行,可以使用previous()方法;如果再往上没有行可以返回时,返回false。

        PS_2 : 使用getXxx()方法返回获得的记录(一行数据)中指定的字段,需要传入要获取的字段的索引(从1开始);或者也可以直接传入字段名。

        PS_3 : 若有需求以对象的形式来接收返回的字段,可以使用getObject(...)方法,传入的实参与getXxx方法一致。

        2.代码演示 : 

                根据对ResultSet的描述,我们不难会联想到迭代器的执行原理。只不过相比迭代器来说,ResultSet的next方法是把两件事都干了——判断和移动指针。

                现有一张学生表如下 :

                根据ResultSet结果集的简介,当我们通过while循环遍历结果集时,一开始ResultSet保持的光标位置会指在学生表第一条记录的上面,如下图所示 : 

                现在我们通过JDBC的方式查询这张表,up以ResultSet_Demo类为演示类,代码如下 : 

package api.resultSet;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;

public class ResultSet_Demo {
    public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
    //编写JDBC程序核心四部曲:
        Properties properties = new Properties();
        properties.load(new FileInputStream("src/api/connection/mysql.properties"));
        String driver = properties.getProperty("driver");
        String url = properties.getProperty("url");
        String user = properties.getProperty("user");
        String password = properties.getProperty("password");

        //1.注册驱动
        Class.forName(driver);

        //2.获取连接
        Connection connection = DriverManager.getConnection(url, user, password);

        //3.执行SQL
        Statement statement = connection.createStatement();
        String sql = "SELECT * FROM stus;";

        ResultSet resultSet = statement.executeQuery(sql);
        /**
            注意 : 执行DQL(数据查询语句)要使用Statement类中的executeQuery方法。
         */
        while (resultSet.next()) {  //使用while循环来遍历结果集
            //获取当前光标指向的记录的第一个字段
            int id = resultSet.getInt(1);
            //获取第二个字段
            String name = resultSet.getString(2);
            //获取第三个字段
            String sex = resultSet.getString(3);
            //获取第四个字段
            double score = resultSet.getDouble(4);
            /*打印获取的字段*/
            System.out.println(String.format("%d\t%5s\t%s\t%.2f", id,name,sex,score));
        }

        //4.释放资源
        resultSet.close();      //结果集也需要关闭!
        statement.close();
        connection.close();
    }
}

                运行结果 : 

        3.底层实现 : 

                接下来,我们通过Debug的方式看一下ResultSet类的源码,看看它底层到底是如何实现的。

                在返回结果集的代码行设置断点,进入Debug,如下图所示 : 

                可以发现ResultSet对象其实是一个ResultSet接口的实现类(JDBC规定要实现的接口),如下图所示 : 

                该实现类又继承了NativeResultset类,如下图所示 : 

                至于为什么要说这个事儿呢?接着往下看你就明白了。 

                在该实现类的众多成员中,存放数据的成员是rowData,我们可以在ResultSetImpl类中找到这个rowData,如下图所示 : 

                但是,当我们使用Ctrl + b/B快捷键访问rowData源码时,会发现rowData其实不是ResultSetImpl类的成员,而是它的父类NativeResultset中的成员,如下图所示 : 

                可以看到,rowData本身是ResultsetRows类型(是个接口),此处使用protected访问权限修饰符,表示其可以被子类访问。 

                但在实际使用中,rowData的类型其实是一个实现了ResultsetRows接口的ResultsetRowsStatic类的对象。而ResultsetRowsStatic类的成员rows才是真正存放表中数据的地方,rows本身是List接口类型,如下图所示 :

                但实际使用中,它是一个实现了List接口的ArrayList类对象,其中存放了表中所有行的数据(所有记录)。

                可以看到,仍然是我们熟悉的elementData数组(up之前出过ArrayList类的源码分析,大家有兴趣可以去看看)。现在elementData数组中有四个元素,对应我们要查询的学生表中共四条记录。

                继续,elementData数组中元素的类型实际是ByteArrayRow类型,而ByteArrayRow类中有包含一个成员internalRowData,是一个byte类型的数组,如下图所示 : 

                这个byte数组中又有四个元素,是对应了我们学生表中的四个字段(id,name,sex,score),此处存放的是字段的值对应的ASCII码值


五、SQL注入

        1.什么是SQL注入?

        Statement也是JDBC规范的接口之一。用于执行静态SQL语句并返回其生成的结果的对象。

        在建立连接后,需要对数据库进行访问,执行SQL语句,可以通过Statement, PreparedStatement(预处理), 或者CallableStatement(存储过程)三种途径。

        但是,使用Statement会存在SQL注入的风险。所谓SQL注入,指的是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的SQL语句段或命令,恶意攻击数据库

        防范SQL注入可以使用PreparedStatement来取代Statement

        2.SQL注入演示 : 

                举一个简单的SQL注入的栗子,输入用户的用户名为:1' OR,输入用户的密码为:OR '1'  = '1。因为我们在WHERE子句中确定name和password时,会使用单引号。那么当我们以上述的用户名和密码来登录时,就会造成如下效果 : 

        ...WHERE name = '1' OR' AND password = 'OR '1' = '1';

        ...WHERE name = '1' OR' AND password = 'OR '1' = '1';

                可以看到,由于输入的用户名和密码中恶意使用了单引号,使得原来的条件验证被改成了条件1 OR 条件2 OR 条件3的格式,并且这里的条件3 —— '1' = '1'是永真式。 

                up以用户表users来演示(表示可登录的用户),创建表的代码如下 : 

CREATE TABLE IF NOT EXISTS `users`(
		`name` VARCHAR(32) NOT NULL,
		`password` VARCHAR(32) NOT NULL
) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin ENGINE INNODB;

INSERT INTO users
		VALUES
		('Ice', '12345'),
		('Bob', 'bbbbb');
		
SELECT * FROM users;

                users表效果如下 : 

                测试SQL注入,如下: 

SELECT * FROM users
		WHERE `name` = '1' OR'
		AND password = 'OR '1' = '1';

                查询结果如下 :  

                如果登录程序以“能否查询到表中的内容”为判定管理员是否存在,那么SQL注入的方式就可以顺利侵入数据库
                接下来我们使用Java程序来演示一下SQL注入
                up以Sumulation类为演示类,代码如下:

package api.sql_injection;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;

public class Simulation {
    public static void main(String[] args) throws ClassNotFoundException, SQLException, IOException {
    //核心四部曲
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要登录用户的用户名:");
        String name = scanner.nextLine();
        System.out.println("请输入要登录用户的密  码:");
        String password_ex = scanner.nextLine();

        Properties properties = new Properties();
        properties.load(new FileInputStream("src/api/connection/mysql.properties"));
        String driver = properties.getProperty("driver");
        String url = properties.getProperty("url");
        String user = properties.getProperty("user");
        String password = properties.getProperty("password");

        //1.注册驱动
        Class.forName(driver);

        //2.获取连接
        Connection connection = DriverManager.getConnection(url, user, password);

        //3.执行SQL
        String sql = "SELECT * FROM users " +
                        "WHERE `name` = '" + name + "'" +
                        "AND password = '" + password_ex + "';";
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery(sql);

        /**
         * 认为 ———— 只要查询到表中的内容,就说明当前管理员是存在的,判定登录成功。
         */
        if (resultSet.next()) {
            System.out.println("Log on successfully!");
        } else {
            System.out.println("Failed to log on!");
        }

        //4.释放资源
        resultSet.close();
        statement.close();
        connection.close();
        scanner.close();
    }
}

                运行结果 : 

        3.PreparedStatement : 

                ①简介

        PreparedStatement也是一个接口,并且是Statement接口的子接口,因此也可以使用Statement接口中的一些方法。 

        PreparedStatement执行的SQL语句中的参数用?来表示(?表示占位符),通过调用该类的setXxx方法来设置这些参数。如下图所示 : 

        可以看到,这些setXxx方法均有两个形参。其中,第一个形参均为int类型,代表了要设置的参数在对应SQL语句中存在的位置(从1开始)第二个形参便是具体要设置的值

        PS : 

        1>同Statement类似,调用executeQuery()方法来执行DQL(查),返回ResultSet对象;而调用executeUpdate()来执行DML(增,删,改),返回int类型的受影响的行数

        2>获取PreparedStatement时,直接传入要执行的SQL字符串,使两者关联;之后调用executeQuery和executeUpdate方法时,不再需要传入形参。

                ②牛逼之处

  • ----->不再需要使用+拼接SQL语句,减少了编程时的语法错误;
  • ----->有效解决了SQL注入的问题;
  • ----->大大减少了编译次数,执行效率较高。 

                ③使用演示

                up以Prepared_Demo类作为演示类,代码如下 : 

package api.sql_injection;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 */
public class PreparedStatement_Demo {
    public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入用户名:");
        String name = scanner.nextLine();
        System.out.println("请输入密  码:");
        String password_ex = scanner.nextLine();

        Properties properties = new Properties();
        properties.load(new FileInputStream("src/api/connection/mysql.properties"));
        String driver = properties.getProperty("driver");
        String url = properties.getProperty("url");
        String user = properties.getProperty("user");
        String password = properties.getProperty("password");

    //JDBC核心四部曲
        //1.注册驱动
        Class.forName(driver);

        //2.获取连接
        Connection connection = DriverManager.getConnection(url, user, password);
        String sql = "SELECT * FROM users " +
                        "WHERE `name` = ? " +
                        "AND password = ? ;";
        PreparedStatement ps = connection.prepareStatement(sql);
        ps.setString(1, name);
        ps.setString(2, password_ex);

        //3.执行SQL
        ResultSet resultSet = ps.executeQuery();
        if (resultSet.next()) {
            System.out.println("Log on successfully!");
        } else {
            System.out.println("Failed to log on!");
        }

        //4.释放资源
        resultSet.close();
        ps.close();
        connection.close();
        scanner.close();
    }
}

                运行效果 : 
                我们先来测试一下输入正确的用户 :

                再来测试一下SQL注入,如下图所示 : 

                可以看到,使用PreparedStatement代替Statement后,SQL注入被成功拦截
                对于PreparedStatement执行DML的情况,很简单,大家可以自己去试试,改用executeUpdate方法,把ResultSet去掉,用int类型的变量做接收。非常容易,这里不做演示。


六、总结 : 

  • 🆗,以上就是JDBC 第二节的全部内容了。
  • 总结一下,我们在日常开发中最终要使用的JDBC连接方式,就是方式三(DriverManager)的简化版的优化版,以核心四部曲为框架,即——直接使用Class.forName(...)的反射形式动态加载Driver类,底层自动完成注册驱动的操作;使用DriverManager类的getConnection方法来获取连接(传入的参数从properties配置文件获得);使用PreparedStatement来执行SQL;释放资源。
  • 下一节内容——JDBC Utils,我们不见不散。感谢阅读!

        System.out.println("END------------------------------------------------------------------------------"); 

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
PostgreSQL JDBC API 是用于连接和操作 PostgreSQL 数据库的一种 Java APIJDBCJava Database Connectivity)是 Java 提供的一种用于访问关系型数据库的标准接口,在许多 Java 应用程序中广泛使用。 通过 PostgreSQL JDBC API,开发人员可以通过编写 Java 代码来连接到 PostgreSQL 数据库,并执行各种数据库操作,如查询、插入、更新和删除数据。 使用 PostgreSQL JDBC API 的第一步是获取数据库的连接。通过指定数据库的 URL、用户名和密码,可以使用 DriverManager 类来获取与数据库的连接。一旦连接成功,就可以创建 Statement 对象,用于执行 SQL 查询和更新语句。 PostgreSQL JDBC API 支持多种查询方式。可以使用 Statement 对象执行简单的 SQL 查询语句,也可以使用 PreparedStatement 对象执行参数化的查询语句,以提高查询效率和安全性。此外,还可以使用 CallableStatement 对象执行存储过程和函数。 在查询数据时,可以使用 ResultSet 对象来获取查询结果。ResultSet 对象是一个指向查询结果的游标,可以用于逐行迭代结果集,获取每行的数据。 除了查询,PostgreSQL JDBC API 还支持更新数据库。可以使用 Statement 或 PreparedStatement 对象来执行 INSERT、UPDATE 和 DELETE 操作,以修改数据库中的数据。 此外,PostgreSQL JDBC API 还提供了一些高级功能,如事务处理、连接池和元数据访问。通过使用这些功能,开发人员可以更好地管理数据库连接、处理并发操作,并获得关于数据库结构和元数据的信息。 总之,PostgreSQL JDBC API 提供了一种方便灵活的方式来连接和操作 PostgreSQL 数据库,使开发人员能够轻松地在 Java 应用程序中集成 PostgreSQL 数据库

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyan_RA9

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值