推荐阅读
给软件行业带来了春天——揭秘Spring究竟是何方神圣(一)
给软件行业带来了春天——揭秘Spring究竟是何方神圣(二)
文章目录
JDBC简介
Java数据库连接,(Java Database Connectivity,简称JDBC)是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。
JDBC通过一系列接口定义了访问数据库的通用API,不同的数据库厂商根据各自数据库的特点提供的对JDBC的实现(实现类包)。
JDBC基本原理
JDBC执行流程:
- 连接数据源,如:数据库。
- 为数据库传递查询和更新指令。
- 处理数据库响应并返回的结果。
JDBC的架构
两层架构和三层架构
双层架构
作用:此架构中,Java Applet 或应用直接访问数据源。
条件:要求 Driver 能与访问的数据库交互。
机制:用户命令传给数据库或其他数据源,随之结果被返回。
部署:数据源可以在另一台机器上,用户通过网络连接,称为 C/S配置(可以是内联网或互联网)。
三层架构
该架构特殊之处在于,引入中间层服务。
流程:命令和结构都会经过该层。
吸引:可以增加企业数据的访问控制,以及多种类型的更新;另外,也可简化应用的部署,并在多数情况下有性能优势。
三层?
UI(表现层) :主要是与用户交互的界面,用户接受用户输入的数据和显示处理后用户需要的数据。
BLL(业务逻辑层):UI层和DAL层之间的桥梁。实现业务逻辑(验证,计算,业务规则等)。
DAL(数据访问层):与数据库打交道。主要实现对数据的增、删、改、查。将存储在数据库中的数据提交给业务层,同时将业务层处理的数据保存到数据库。(当然这些操作都是基于UI层的。用户的需求反映给界面(UI),UI反映给BLL,BLL反映给DAL,DAL进行数据的操作,操作后再一一返回,直到将用户所需数据反馈给用户).
Entity层
Entity(实体层):它不属于三层中的任何一层,但是它是必不可少的一层。
Entity在三层架构中的作用:
- 1、实现面向对象思想中的"封装";
- 2、贯穿于三层,在三层之间传递数据;(注:确切的说实体层贯穿于三层之间,来连接三层)
- 3、每一层(UI—>BLL—>DAL)之间的数据传递(单向)是靠变量或实体作为参数来传递的,这样就构造了三层之间的联系,完成了功能的实现。
- 对于初学者来说,可以这样理解:每张数据表对应一个实体,即每个数据表中的字段对应实体中的属性
- (注:当然,事实上不是这样。为什么?1>,可能我们需要的实体在数据表对应的实体中并不存在;2>,我们完全可以将所有数据表中的所有字段都放在一个实体里)
(注:这里为什么说可以暂时理解为每个数据表对应一个实体??
答:大家都知道,我们做系统的目的,是为用户提供服务,用户可不关心你的系统后台是怎么工作的,用户只关心软件是不是好用,界面是不是符合自己心意。用户在界面上轻松的增、删、改、查,那么数据库中也要有相应的增、删、改、查,而增删改查具体操作对象就是数据库中的数据,说白了就是表中的字段。所以,将每个数据表作为一个实体类,实体类封装的属性对应到表中的字段,这样的话,实体在贯穿于三层之间时,就可以实现增删改查数据了)
三层与实体层之间的依赖关系:
两层与三层的区别
两层:
(当任何一个地方发生变化时,都需要重新开发整个系统。"多层"放在一层,分工不明确耦合度高——难以适应需求变化,可维护性低、可扩展性低)
三层:
(发生在哪一层的变化,只需更改该层,不需要更改整个系统。层次清晰,分工明确,每层之间耦合度低——提高了效率,适应需求变化,可维护性高,可扩展性高)。
综上,三层架构的优势:
- 结构清晰、耦合度低
- 可维护性高,可扩展性高
- 利于开发任务同步进行, 容易适应需求变化
劣势:
- 降低了系统的性能。如果不采用分层式结构,很多业务可以直接造访数据库,以此获取相应的数据,如今却必须通过中间层来完成。
- 有时会导致级联的修改。这种修改尤其体现在自上而下的方向。如果在表示层中需要增加一个功能,为保证其设计符合分层式结构,可能需要在相应的业务逻辑层和数据访问层中都增加相应的代码
- 增加了代码量,增加了工作量
JDBC使用步骤
- 导入jdbc操作
- 注册jdbc驱动
● -参数:"驱动程序类名"(mysql,oracle等不同的驱动名)
● -Class.forname("驱动程序类名")
//加载MySql驱动
Class.forName("com.mysql.jdbc.Driver")
- 获得Connection 对象
● 3个参数:URL,username,password连接数据库
String url="jdbc:mysql://127.0.0.1:3306/test";
String username="root";
String password="123456";
Connection conn= DriverManager.getConnection(url,username,password);
- 创建Statement()对象
用于执行SQL语句
execute() 可以执行任何SQL语句,但是常用于执行ddl,dcl语句
executeUpdate() 执行dml语句,如 insert,update delete
executeQuery() 执行DQL语句,如select
//创建 Statement
Statement st= conn.createStatement();
//执行DML语句
String dml="delete from k where age=24 ";
String dml ="delete from k"+" where age=26";
- 处理SQL执行结果:
● execute(DDL) 如果没有异常则成功。
● executeUpdate(DML) 返回数字,表示更新的行数,抛出异常则失败。
● executeQuery(DQL) 返回resultset(结果集)对象,代表二维查询结果,使用for遍历处理,如果查询失败抛出异常。
int b=st.executeUpdate(dml);
- 关闭数据连接
conn.close();
JDBC核心API
Statement
三个方法,分别执行三种语句,DQL,DML,DDL语句。
Statement st = conn.createStatement();
String ddl="create table a "+"(ids varchar(6))";
boolean b=st.execute(ddl);
System.out.println(b);
/这里的返回结果:
TRUE:表示有结果集,
* False:表示没有结果集
* 创建失败抛异常
* 在ddl语句执行如何判断结果,可以根据是否有没有异常,没有则创建成功。
* 在ddl语句中,创建表,实际上没有结果集返回,因为没有二维表返回,在这里并不是创建成功就是TRUE。但是如果执行select查询语句时,就有结果集返回,即为TRUE。/
这里显示为FALSE,但是实际上是创建成功了。
不仅如此,进行表的删除等操作都是一样的。
注意点:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class test {
public static void main(String[] args) {
try{
Class.forName("com.mysql.jdbc.Driver");
String url="jdbc:mysql://127.0.0.1:3306/test";
String username="root";
String password="123456";
Connection conn= DriverManager.getConnection(url,username,password);
//创建 Statement
Statement st= conn.createStatement();
//执行DML语句
String dml="insert into k (name,id) values('cc',24) ";
int b=st.executeUpdate(dml);
System.out.println(b);
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
在idea返回结果
在数据库中查询的结果。
在拼接SQL语句时,一定要注意之间的空格,不然容易报错。
ResultSet
ResultSet代表dql查询结果,是2维结果,其内部维护了一个读取数据的游标,默认情况下,游标在第一行数据之前,当调用next()方法时候,游标会向下移动,并将返回结果集中是否包含数据,如果包含数据就返回TRUE,结果集还提供了很好getXXX方法用于获取结果集游标指向当前行数据。
String sql="select * from k";
ResultSet rs=st.executeQurry(sql);
while(rs.next()){
String str=rs.getString("age"),
System.out.println(str);
}
可滚动结果集
ResultSetMetaData
ResultSetMetaData:数据结果集的元数据(结果集的相关数据,不是结果集本身)
Connection conn=null;
try{
conn=DBUtils.getConnection();
String sql="select * from k";
Statement st=conn.createStatement();
ResultSet rs=conn.executeQuery(sql);
//结果集元数据
ResultSetMetaData rsm =rs.getMetaData();
int n=rsm.getColumnCount();
for (int i=1;i<=n;i++){
String name=metaData.getColumnName(i);
System.out.println(name);
}
}catch(Exception e){
e.printStackTrace();
}finally{
DUBtils.close(conn);
}
Properties
Properties 读取配置文件
properties 是Java中专门用于读取配置文件的API。
- 其底层就是文本的API
- Propertics 本身实现MAP接口,内部是散列表
- Properties限定key和value都是String类型。
Properties常用的API方法
- load(流) 读取一个配置文件
- String getProperty(key)读取一个属性值
使用步骤:
- 创建properties对象
- 利用load方法读取配置文件
- 利用getproperty查询属性文件内容。
利用配置文件可以将程序中的参数保存到配置文件中,修改程序参数只需要修改配置文件即可。
//1.创建properties 对象
Properties pr=new Properties();
System.out.println(pr);
System.out.println(pr.size());
//2.利用load方法读取文件
InputStream in = day02.class.getClassLoader().getResourceAsStream("db.properties");
//执行后,将文件内容读取到了散列表中
pr.load(in);
System.out.println(pr);
System.out.println(pr.size());
//查找 文件内容,就是读取文件内容
System.out.println(pr.getProperty("jdbc.password"));
# db.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/test
jdbc.user=root
jdbc.password=123456
PreparedStatement
preparedStatement 对象用于执行带参数的预编译执行计划。
//带参数的SQL语句
String sql="insert into k (name,age) values (?,?)";
// 将SQL发送到数据库,创建执行计划,返回值就是代表SQL语句在服务器的执行计划。
PreparedStatement ps=conn.prepareStatement(sql);
//替换执行计划中的参数,2个参数,顺序不可错,按照序号发送参数。
ps.setString(1,"bb");
ps.setInt(2,21);
//执行执行计划
int n=ps.executeUpdate();
System.out.println(n);
预防SQL
SQL注入的产生
public class login {
public static void main(String[] args){
//获取用户输入
Scanner in=new Scanner(System.in);
System.out.println("用户名:" );
String name=in.nextLine();
System.out.println("密码:");
String pwd=in.nextLine();
//检查登录情况
boolean pass=login(name,pwd);
if (pass){
System.out.println("欢迎你!"+name);
}else{
System.out.println("用户名或者密码错误!");
}
}
//检查用户是否能够登录
public static boolean login(String name,String pwd){
String sql="select * from h where name=\'"+name+"\'"+"and pwd=\'" +pwd+"\' ";
System.out.println(sql);
Connection conn=null;
try {
conn=DBUtils.getConnection();
Statement st= conn.createStatement();
ResultSet rs=st.executeQuery(sql);
while (rs.next()){
int a=rs.getInt("id");
return a>=1;
}
}catch (Exception e){
e.printStackTrace();
}finally {
DBUtils.close(conn);
}
return false;
}
}
SQL注入的原因:
and 的优先级高于or ,所以当前面的FALSE and FALSE,即为 FALSE,但是 or 后面为TRUE,所以FALSE or TRUE,最终结果为TRUE。
用户输入了含有SQL成分的语句的参数,参数在拼接SQL时候造成了SQL语句的语义改变,改变了SQL语句的执行计划!
防守手段:
拦截用户输入的SQL成分。
select * from h where name=? and pwd =?
这种情况,or 只是一个普通的字符串,不当成SQL成分
这种就可以利用preparedStatement。
public class right {
public static void main(String[] args){
Scanner in=new Scanner(System.in);
System.out.println("用户名:");
String name=in.nextLine();
System.out.println("密码:");
String pwd=in.nextLine();
boolean pass=right(name,pwd);
if (pass){
System.out.println("登录成功");
}else {
System.out.println("登陆失败");
}
}
public static boolean right(String name,String pwd){
Connection conn=null;
String sql="select * from h "+" where name=? and pwd=? ";
System.out.println(sql);
try {
conn=DBUtils.getConnection();
PreparedStatement pr= conn.prepareStatement(sql);
pr.setString(1,name);
pr.setString(2,pwd);
ResultSet rs=pr.executeQuery();
while (rs.next()){
int n=rs.getInt("id");
return n>=1;
}
}catch (Exception e){
e.printStackTrace();
}finally {
DBUtils.close(conn);
}
return false;
}
}
连接管理
在软件中数据库连接使用非常频繁,如果每次都创建连接,就会造成代码的大量冗余,常规做法是建立数据库连接工具类,封装数据库连接流程,统一数据库连接过程,使用时候可以简化代码。
实现步骤:
- 创建数据库连接参数文件,db.properties
- 创建DBUtils.java 封装数据库连接方法
-
利用properties读取配置文件夹中的数据库连接参数
-
创建方法 getConnection方法封装数据库连接过程。
3.使用getConnection()连接数据库
public class DBUtils {
static String driver;
static String url;
static String user;
static String password;
//静态代码块的目的是从
static {
//初始化静态属性
/*1.利用properties读取配置文件
* 2.从配置文件中查找相应参数值
* */
try{
Properties pr=new Properties();
InputSteam in=DBUtils.class.getClassLoader().getResourceAsStream("db.properties");
pr.load(in);
System.out.println(pr);
//初始化数据
driver=pr.getProperty("jdbc.driver");
url=pr.getProperty("jdbc.url");
password=pr.getProperty("jdbc.password");
user=pr.getProperty("jdbc.user");
in.close();
}catch(IOException e){
e.printStackTrace();
}
}
/*封装创建数据库连接的过程,简化数据库的连接*/
public static Connection getConnection(){
try {
Class.forName(driver);
Connection conn=DriverManager.getConnection(url,user,password);
return conn;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/*
* dbutils.java
* 关闭数据库连接方法,封装复杂的关闭过程
* */
public static void close(Connection conn){
if (conn!=null){
try {
conn.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
这样可以简化重复操作。
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/test
jdbc.user=root
jdbc.password=123456
//创建连接
Connection conn=DBUtils.getConnection();
//创建Statement()对象
Statement st=conn.creataStatement();
String st="select *from k";
//查询结果
ResultSet rs=st.executeQuery(sql);
while(rs.next()){
String name=rs.getString("name");
System.out.println("name: "+name);
}
conn.close();
DriverManager 管理数据库连接适合单线程情况,而在多线程并发情况下,为了能够重用数据库连接,同时控制并发连接总数,保护数据库避免连接重载,一定要使用数据库连接池。
连接池技术
数据库连接池的开源实现非常多,dbcp是常用的连接池之一。
导入dbcp。
导包
从模块中搜索对应的包,然后导入时,点击应用后,在确定。
import org.apache.commons.dbcp2.BasicDataSource;
public class day03 {
public static void main(String[] args)
throws Exception{
String driver="com.mysql.jdbc.Driver";
String url="jdbc:mysql://127.0.0.1:3506/test";
String password="123456";
String user="root";
BasicDataSource ds=new BasicDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(user);
ds.setPassword(password);
//设置连接池的管理参数
ds.setInitialSize(2);
ds.setMaxTotal(100);
//使用连接池中的数据库连接
Connection conn=ds.getConnection();
//执行SQL
String sql="select *from k";
Statement st=conn.createStatement();
ResultSet rs=st.executeQuery(sql);
while(rs.next()){
String str=rs.getString("age");
System.out.println(str);
}
//归还连接到数据库连接池
conn.close();
}
}
try{
conn=DBUtils.getConnection();
String sql="select * from k";
Statement st=conn.createStatement();
ResultSet rs=st.executeQuery(sql);
while(rs.next()){
String str=rs.getString("name");
System.out.println(str);
}
rs.close();
st.close();
}
连接并发测试
public class day05 {
public static void main(String[] args){
Thread t1=new TestTherad(4000);
Thread t2=new TestTherad(5000);
Thread t3=new TestTherad(1000);
t1.start();
t2.start();
t3.start();
}
}
class TestTherad extends Thread{
int wait;
public TestTherad(int wait){
this.wait=wait;
}
public void run(){
Connection conn=null;
try {
conn=DBUtils.getConnection();
System.out.println("获取连接:"+conn);
Thread.sleep(wait);
String sql="select * from k";
Statement st=conn.createStatement();
ResultSet rs=st.executeQuery(sql);
while(rs.next()){
System.out.println(rs.getString("name"));
}
System.out.println(wait+"结束");
}catch (Exception e){
e.printStackTrace();
}finally {
DBUtils.close(conn);
}
}
}
JDBC事务
事务(Transaction) :数据库中保证交易可靠的机制。
计算机中使用到原子,基本是指某项不能在分或打开的。即不可再分的。例如:交易操作,即借钱中,一个账户的扣除和另一个账户的增加必须同时进行,这中间操作不可再分了。
隔离性演示:
如数据库未执行完,并未进行commit,那么对于同一影响的数据就不能够被操作,会在执行过程中类似于卡住,这是因为数据进行了加锁。(oracle数据库)
事务API
conn.setAutoCommit(false);//开启事务
conn.commit();//提交事务
conn.rollback();//回滚事务
交易测试案例
public class test {
public static void main(String[] args) {
pay("bob","cindy",100);
pay("alice","bob",-100);
}
public static void pay(String from ,String to,double money ){
String sql1="update k set balance=balance+ ? where name=? ";
String sql2="select balance from k where name= ? ";
Connection conn=null;
try{
conn=DBUtils.getConnection();
conn.setAutoCommit(false);
PreparedStatement ps=conn.prepareStatement(sql1);
ps.setDouble(1,-money);
ps.setString(2,from);
int n=ps.executeUpdate();
if (n!=1){
throw new Exception("error");
}
//加余额
ps.setDouble(1,money);
ps.setString(2,to);
n=ps.executeUpdate();
if (n!=1){
throw new Exception("ok");
}
ps.close();
//检查
ps=conn.prepareStatement(sql2);
ps.setString(1,from);
ResultSet resultSet=ps.executeQuery();
while (resultSet.next()){
double balance=resultSet.getDouble(1);
if (balance<0){
throw new Exception("nonono");
}
}
conn.commit();
}catch (Exception e){
e.printStackTrace();
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}finally {
DBUtils.close(conn);
}
}
}
JDBC批量处理
一个批处理是被发送到数据库以作为单个单元执行的一组更新语句。
批处理API
executeBatch()
int[] executeBatch() throws SQLException;
数组值可能是以下之一:
- 大于或等于零的数字,表示命令已成功处理,并且是更新计数,给出了数据库中受命令影响的行数执行
- SUCCESS_NO_INFO **( -2)**的值,表示命令为处理成功,但受影响的行数为未知
- 如果批量更新中的命令之一无法正确执行,此方法引发BatchUpdateException,JDBC driver可能会也可能不会继续处理剩余的命令。但是driver的行为是与特定的DBMS绑定的,要么总是继续处理命令,要么从不继续处理命令。如果驱动程序继续处理,方法将返回 EXECUTE_FAILED(-3)。
批量执行SQL语句
public class ddl {
public static void main(String[] args){
String sql1="create table log_1 (id int(8),msg varchar(100))";
String sql2="create table log_2 (id int(8),msg varchar(100))";
Connection conn=null;
try {
conn= DBUtils.getConnection();
Statement st=conn.createStatement();
st.addBatch(sql1);
st.addBatch(sql2);
//执行一批SQL语句
int [] ary=st.executeBatch();
System.out.println(Arrays.toString(ary));
System.out.println("ok");
}catch (Exception e){
e.printStackTrace();
}finally {
DBUtils.close(conn);
}
}
}
批量参数处理
import test.DBUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.Arrays;
public class test {
public static void main(String[] args){
String sql="insert into k (name,age,balance) values(?,?,?)";
Connection conn=null;
try {
conn= DBUtils.getConnection();
PreparedStatement ps=conn.prepareStatement(sql);
for (int i=0;i<5;i++){
//替换参数
ps.setString(1,"doc");
ps.setInt(2,25+i);
ps.setInt(3,800);
//将参数添加到ps缓存区
ps.addBatch();
}
//批量执行
int [] ary=ps.executeBatch();
System.out.println(Arrays.toString(ary));
}catch (Exception e){
e.printStackTrace();
}finally {
DBUtils.close(conn);
}
}
}
防止OutOfMemory错误
如果Preparedstatement 对象中的SQL列表包含过多的待处理SQL语句可能会产生OutOfMemory错误。
可以采取分批次执行,可以避免内存溢出问题
if ((i+1)%4==0){
ps.executeBatch();
ps.clearBatch();
}
}
返回自动主键
jdbc API 提供了返回插入数据期间自动生成的id的值,
public class test {
public static void main(String[] args){
Connection connection=null;
try {
connection= DBUtils.getConnection();
connection.setAutoCommit(false);
String sql="insert into keywords (word) values(?)";
//自动生成序号的列名
String[] cols={"id"};
PreparedStatement ps=connection.prepareStatement(sql,cols);
ps.setString(1,"hello");
int n=ps.executeUpdate();
if (n!=1){
throw new Exception("error");
}
//获取自动生成的id
ResultSet rs=ps.getGeneratedKeys();
int id=-1;
while (rs.next()){
id=rs.getInt(1);
}
rs.close();
ps.close();
String sql2="insert into post (content,kid) values(?,?)";
ps=connection.prepareStatement(sql2);
ps.setString(1,"world");
ps.setInt(2,id);
n=ps.executeUpdate();
if (n!=1){
throw new Exception("good");
}
connection.commit();
System.out.println("ok");
}catch(Exception e){
e.printStackTrace();
DBUtils.rollback(connection);
}finally {
DBUtils.close(connection);
}
}
}
实现分页查询
数据库连接是编程中不可或缺的一环,通过本文你已经了解了如何轻松驾驭JDBC。然而,这仅仅是一个开始。深入学习数据库管理、优化查询、事务处理等方面的知识,将为你打开更广阔的世界。
感谢阅读本文,如有任何问题,欢迎在评论区积极讨论。