JDBC(Java DataBases Connectivity):Java语言连接数据库
JDBC的本质:一套接口
JDBC简介:
JDBC时SUN公司指定的一套接口(interface)
java.sql.*;(这个包下有很多接口)
为什么SUN要制定一套JDBC接口?
因为每一个数据库的底层实现原理都不一样。Oracle数据库、MySQL数据库、微软SqlService数据库都有自己独特的实现原理
SUN公司提供JDBC接口,各大数据库厂商负责编写JDBC接口的实现类。我们只需要下载驱动就行。
驱动:所有的数据库驱动都是以jar包的形式存在,jar包当中有很多.class文件。这些.class文件就是对JDBC接口的实现。驱动不是SUN公司提供的,是各大数据库厂家负责提供,下载驱动jar包需要去数据库官网下载。
为什么要面向接口编程?
解耦合:降低程序的耦合度,提高程序的扩展力
JDBC编程6步概述(重点):
第一步:注册驱动:告诉Java程序即将连接的是哪种数据库
第二步:获取连接:表示JVM的进程和数据库进程之间的通道打开了。这属于进程间的通信(重量级),使用完一定要关闭
第三步:获取数据库操作对象:专门执行sql语句的对象
第四步:执行sql语句:
executeQuery(select)专门执行DQL语句、executeUpdate(insert、delete、update)专门执行DML语句
第五步:处理查询结果集:只有当第四步执行的是select语句时,才能有第五步
第六步:释放资源:使用完资源之后一定要关闭资源
第一步:注册驱动
//注册驱动的第一种写法:
Driver driver = new com.mysql.cj.jdbc.Driver();
DriverManager.registerDriver(driver);
//注册驱动的第二种写法:
class.forName("com.mysql.cj.jdbc.Driver");
- 第二种写法常用!因为参数是字符串,可以写到配置文件中
第二步:获取连接:
url:统一资源定位符(网络中某个资源的绝对路径)
url包括:
协议——jdbc:mysql://
ip——127.0.0.1和localhost都是本机ip地址
端口号——3306(MySQL数据库的端口号)
资源名(数据库名)——myjdb
oracle的url:
jdbc:oracle:thin:@localhost:1521:数据库名
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc";
String user="root";
String password="abc1234.";//MySQL登录密码
Connection conn=DriverManager.getConnection(url,user,password);
System.out.println("数据库连接对象="+conn);
第三步:获取数据库操作对象
Statement stmt = conn.createStatement();
Statement专门执行sql语句
第四步:执行sql语句
JDBC的sql语句不需要写分号(;)结尾
String sql="insert into school(name,age) values('张三',20)";
int count=stmt.executeUpdate(sql);
System.out.println(count==1?"保存成功":"保存失败");
executeUpdate专门执行DML语句的(insert、delete、update)
返回值是单个ResultSet对象
executeQuery专门执行DQL语句(select)
返回值是“影响数据库中的记录条数”
第五步:处理查询结果集
查询结果集next():将光标从当前位置向前移一行,并返回boolean类型。
取数据getString():不管数据库中数据类型是什么,都以String的形式取出。
除了可以以String类型取出之外,还可以以特定的类型取出(得与底层数据类型一致)。如int类型getInt()、double类型的getDouble()。
JDBC中所有下标从1开始
executeQuery(select)专门执行DQL语句
executeUpdate(insert、delete、update)专门执行DML语句
注意:列名不是表中的列名称,而是查询结果集的列名称
第六步:释放资源
为了保证资源一定释放,在finally语句块中关闭资源并且要遵循从小到大(先开后关)依次关闭分别对其try...catch。
在try语句中定义的对象无法关闭,需要把定义放到try语句外。
try{
if(stmt!=null){
stmt.close();
System.out.println("关闭stmt");
}
}catch (Exception e){
e.printStackTrace();
}
try{
if(conn!=null){
conn.close();
System.out.println("关闭conn");
}
}catch (Exception e){
e.printStackTrace();
}
全过程:
将连接数据库的所有信息配置到配置文件中
实际开发中不建议把连接数据库的信息写死到Java程序中
项目实战-用户登录功能
实现功能:
1.模拟用户登录功能的实现
2.业务描述:
程序运行的时候,提供一个输入的入口,可以让用户输入用户名和密码。用户输入用户名和密码之后,提交信息,Java收集并验证用户名和密码是否合法。
合法:显示登陆成功
不合法:显示登陆失败
3.数据的准备:
在实际开发中,表的设计会使用专业的建模工具如:Power Designer来进行数据库表的设计。参见user_login.sql脚本
1.用户输入界面
private static Map<String, String> initUI() {
//获取用户名
Scanner s =new Scanner(System.in);
System.out.print("用户名:");
String loginName=s.nextLine();
System.out.print("密码:");
String loginPwd=s.nextLine();
//创建一个 Map<String,String>类型的哈希表,名为userLoginInfo
Map<String,String> userLoginInfo = new HashMap<>();
//利用put方法把用户输入保存在哈希表中
userLoginInfo.put("loginName",loginName);
userLoginInfo.put("loginPwd",loginPwd);
//返回用户输入,用以下一步验证
return userLoginInfo;
}
- 知识点小结:
1.Scanner类可以帮助我们接收从键盘输入的数据
2.Scanner类next()与nextLine()的区别
1)next()
①一定要读取到有效字符后才可以结束输入。
②对输入有效字符之前遇到的空白,next() 方法会自动将其去掉。
③只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。
④next() 不能得到带有空格的字符串。
2)nextLine()
①以Enter为结束符,即nextLine()方法返回的是输入回车之前的所有字符。
②可以获得空白。
3.将对象保存在Map<String,Sring>容器中
Map是存储键和值这样的双列数据集合,但存储的数据是没有顺序的,其键不能重复,但其值是可以重复的,可以通过每一个键找到每一个对应的值
Map集合的特点:
1)Map集合一次存储两个对象,一个键对象,一个值对象
2)键对象在集合中是唯一的,可以通过键来查找值
HashMap特点:
1)使[哈希算法对键去重复,效率高,但无序
2)HashMap是Map接口的主要实现类
2.验证用户输入
private static boolean login(Map<String, String> userLoginInfo) {
//使用资源绑定器绑定属性配置文件
ResourceBundle bundle = ResourceBundle.getBundle("resource");
//绑定后传值
String driver = bundle.getString("driver");
String url = bundle.getString("url");
String user = bundle.getString("user");
String password = bundle.getString("password");
Connection conn = null;
Statement stmt=null;
ResultSet rs=null;
//保存登录结果,默认为false
boolean loginJieGuo = false;
try{
//1.注册驱动
Class.forName(driver);
//2.获取连接
conn=DriverManager.getConnection(url,user,password);
//3.获取数据库操作对象
stmt=conn.createStatement();
//4.执行sql语句
String sql="select * from t_user where loginName='"+userLoginInfo.get("loginName")+"' " +
"&& loginPwd='"+userLoginInfo.get("loginPwd")+"'";
rs=stmt.executeQuery(sql);
//5.处理查询结果集
if (rs.next()){
//登录成功,将登录结果改为true
loginJieGuo=true;
}
}catch (Exception e){
e.printStackTrace();
}finally {
//6.关闭资源
if (rs != null) {
try{
rs.close();
}catch (Exception e){
e.printStackTrace();
}
}
if (stmt != null) {
try{
stmt.close();
}catch (Exception e){
e.printStackTrace();
}
}
if (conn != null) {
try{
conn.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
//执行完后返回登陆结果
return loginJieGuo;
}
//resource.properties配置文件
driver =com.mysql.cj.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/myjdbc
user=root
password=abc1234.
3.主函数调用方法
public static void main(String[] args) {
//初始化一个界面
Map<String,String> userLoginInfo=initUI();
//验证用户名和密码
boolean loginSuccess = login(userLoginInfo);
//输出验证结果
System.out.println(loginSuccess?"登陆成功!":"登陆失败!");
}
以上程序存在SQL注入问题
用户名:fdsa
密码:fdsa' or '1'='1
登陆成功
这种现象被成为SQL注入。(黑客经常使用)
导致sql注入的根本原因是?
用户输入的信息中含有sql语句的关键字,并且这些关键字参与sql语句的编译过程,导致sql语句的愿意被扭曲,进而达到sql注入。
解决sql注入问题:
只要用户提供的信息不参与sql语句的编译过程,问题就解决了。即使用户提供的信息中含有sql语句的关键字,但没有参与编译,不起作用。
要想用户信息不参与sql语句的编译,那么必须使用
java.sql.PreparedStatement
PreparedStatement接口继承了java.sql.Statement
PreparedStatement是属于预编译的数据库操作对象。
PreparedStatement的原理 是:预先对sql语句的框架进行编译,然后在给sql语句传值。
-
1.将原第三步:获取数据库操作对象中
Statement stmt=null;修改为 PreparedStatement stmt=null;
2.修改原sql语句
String sql="select * from t_user where loginName='"+loginName+"'&& loginPwd='"+loginPwd+"'";
改为
String sql="select * from t_user where loginName= ? && loginPwd= ? ";
?为填充符,一个?将来接受一个“值”。且占位符不能用单引号括起来
-
3.将原第四步:执行sql语句放在第三步:获取预编译的数据库操作对象中进行
-
4.在第四步:执行sql前,给占位符传值
数据是什么类型的,就是set什么类型的方法。如int,setInt()。
setXxxx(int parameterIndex,Xxxx x )
parameterIndex:占位符的下标
x:值
JDBC中下标从1开始
Statement与prepareStatement对比:
1.Statement存在sql注入现象,prepareStatement解决了SQL注入问题
2.prepareStatement效率较高一些
MySQL数据库中,第一条指令编、译执行后,如果第二条指令与第一条完全一致,则不会再编译,直接执行。
因为用户名与密码每次都不一样,故Statement每次都要先编译再执行。
因为整个sql语句是固定的,故prepareStatement只有第一次需要编译,而后可以直接执行。(?也不会变,因为传值时间在编译之后)
3.prepareStatement会在编译阶段做类型的安全检查
什么情况下必须使用Statement呢?
业务方面要求必须支持SQL注入时(即业务要求是需要进行SQL语句拼接的)
如:select * from t_user order by xxx desc;降序排列,此语句为select * from t_user order by xxx 拼接desc。
若:select * from t_user order by xxx ?再调用setString(1,desc)进行赋值。
结果为:select * from t_user order by xxx ‘desc’;这不符合sql语句的规范。
故当业务必须支持sql注入(sql拼接)时,必须使用Statement。如果只是给sql语句传值,使用prepareStatement。
JDBC的事务自动提交机制
1.将自动提交机制改为手动提交
conn.setAutoCommit(false);
应在预编译前开启事务
2.测试是否手动提交事务
String s=null;
s.toString();
3.事务结束,手动提交数据
conn.conmmit();
4.手动设置回滚
//如果有异常,则在catch里设置回归
try{
......
}catch(Exception e){
if(conn!=null){
try{
conn.rollback();//设置回滚
}catch(SQLException e1){
e1.printStackTrace();
}
}
}
JDBC工具类的封装及使用
/*
* 工具类中的构造方法都是私有的,
* 因为工具类当中的方法都是静态的,
* 不需要new对象,直接采用类名调用。
* */
private DBuntil(){}
/*
* 注册驱动只需要执行一次
* 静态代码块在类加载时执行,且只执行一次
* */
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/*
* 获取数据库连接对象
* 返回连接对象
* */
public static Connection getConnection() throws SQLException{
return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/myjdbc","root","abc1234.");
}
/*
* 执行DQL语句
* conn连接对象
* ps数据库操作对象
* rs结果集
* */
public static void close(Connection conn, Statement ps, ResultSet rs){
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/*
* 执行DML语句
* conn连接对象
* ps数据库操作对象
* */
public static void close(Connection conn, Statement ps){
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
锁的概念
-
行级锁(for update)又被称为悲观锁
select * from school where age>20 for update;
在select语句后+for update表示,当前事务没有结束前,对其他事务来说,对满足条件“age>20”的数据,所在行的字段都无法修改。
-
乐观锁
多线程并发都可以对数据进行修改,不需要盘对,但需要一个版本号。