文章目录
驱动
驱动,也是一种程序,用于驱动相关的软件
驱动的全称是设备驱动程序,由硬件厂商根据操作系统(win,linux,os)编写,包含硬件设备的配置信息
有了驱动,硬件设备就可以和相关软件进行通信
驱动不是安装在相关软件的目录里,而是添加到操作系统,作为内存运行程序存在
软件与硬件之间可以有驱动
软件与软件之间也可以有驱动,比如说服务器连接数据库
JDBC驱动的版本
我接触了两个版本的驱动jar包,一个是针对mysql5.0,一个是针对mysql8.0
不同版本的配置文件是不同的,url和driverclass不同
5.0驱动配置文件
8.0驱动配置文件
8.0需要指明使用utf-8编码集,java.sql.Driver接口的实现类的位置也变了,不再是com.mysql.jdbc.Driver,而是com.mysql.cj.jdbc.Driver
folder和directory
虽然前者一般译作文件夹,后者译作目录,但很多情况下并没有严格区分
但是,我们知道,计算机系统中的术语往往是严格区分的,只是说超出作用范围才不再区分
在windows系统下:
(1)Folder 的范围是很大的,它包括了系统中所有可以双击打开并查看其内容的「文件夹」项目,包括「计算机」、「回收站」、「控制面板」等这些虚拟文件夹,以及「计算机」中的所有磁盘驱动器,以及其中的任何文件夹 (Directory)。
(2)Directory 的概念则小的很多,它必须是存在于物理磁盘上的一个「文件夹」项目,而「计算机」中的所有磁盘驱动器,如「本地磁盘 (C:)」则不认为是 Directory
概览
前面已经说过,浏览器是特殊的客户端
因此,javaweb主要分为两种架构:
(1)B/S: browser server
(2)C/S: client server
移动端app采用混合的模式
HTML:页面的结构
CSS: 页面的美化
JS:页面的行为
JQuery:封装了JS
VUE:封装了JS(简单)
React:封装了JS
TOMCAT:服务器
xml:可以自定义标签,用来写配置文件
JSON:服务器向浏览器传数据以JSON的格式,轻量级,更易解析
servlet:用于和浏览器交互,tomcat中最重要的组件获取请求,处理请求,响应请求
filter:过滤器,比如说购物车点击购买,会先看是否登录等等
listener:监听器,监听用户的操作
HTTP:浏览器和服务器交互需要遵循的协议
JSP:java服务器页面,响应请求显示的页面,动态页面
EL,JSTL:用于提高JSP的开发效率
会话控制:服务器无法区分请求的来源,通过会话控制区分请求来自于哪个服务器
Ajax:实现异步请求。比如说网站注册,输入完用户名,后面就会判断是否通过,此时我们并没有主动点击注册
一. JDBC概述
前言
从一开始就将,JDBC是面向接口编程,第三方API是不暴露的,要隐藏起来
但是在实际编程中,因为一直没有看到接口的实现类总觉得别扭。
唯独只有Driver接口,最开始我们看到生成驱动实例时见到了他的实现类Driver driver = new com.mysql.jdbc.Driver();
这个时候我去查了厂家提供的驱动jar包,里边果然有PreparedStatement接口的实现类,ResultSet接口的实现类,ResultSetMetaData接口的实现类,Blob接口的实现类
持久化存储
java的数据存取技术
注意,这里特指的是java的数据库存取技术,java如何更好地实现和数据库的交互
(1)JDBC: java数据库连接,直接访问数据库
(2)JDO (Java Data Object )技术
(3)第三方O/R工具,如Hibernate, Mybatis 等
JDBC是java访问数据库的基石,JDO、Hibernate、MyBatis等只是更好的封装了JDBC
JDBC简介
定义了用来访问数据库的标准Java类(java.sql,javax.sql)使用这些类库可以以一种标准的方法、方便地访问数据库资源
JDBC为访问不同的数据库提供了一种统一的途径,为开发者屏蔽了一些细节问题.
所以说java和具体的数据库是解耦合的,JDBC提供了一些访问数据库必要功能范式,与具体的数据库无关
这样一来,JDBC就不需要关注不同数据库的具体实现细节,这些细节根本记不住,非常不方便。每种数据库的操作都有差别。
JDBC体系结构
JDBC接口(API)包括两个层次:
(1)面向应用的API:Java API,抽象接口,供应用程序开发人员使用(连接数据库,执行SQL语句,获得结
果)。
(2)面向数据库的API:Java Driver API,供开发商开发数据库驱动程序用
由厂商编写具体的驱动(重写那些抽象方法等),程序员只需要面向JDBC就行了
JDBC程序编写
创建connection对象:连接数据库(有点像socket编程)
创建statement对象:负责增删改查的操作
使用resultset对象:查询返回的结果集放在这里
二. 获取数据库连接
要素一:Driver接口实现类
和这个图描述的一样,在编写具体程序时,不需要访问Driver 接口的实现类,驱动程序管理器类(java.sql.DriverManager)去调用这些Driver实现
Oracle的驱动:oracle.jdbc.driver.OracleDriver
mySql的驱动: com.mysql.jdbc.Driver
Driver 接口的实现类就是上面的驱动
获取connection对象的范式
lib和bin
bin一般放的是CS项目中生成的class文件及目录;
lib一般放的是CS/BS项目中外部引入的jar包文件。
lib可以理解为library (库)。 是文件夹
jar包文件是全称是java archive(java档案文件),一般是外部引入的第三方资源
导入驱动程序
将驱动程序放到新建的lib文件夹里
build path之后,就可以看到在referenced libraries里看到这个驱动程序了
点开引用库里边的jar包文件,里边就有Driver接口的实现类
com.mysql.jdbc包下有一个Driver类,这就是Driver接口的实现类
要素二:URL
url,统一资源定位符,它表示Internet上某一资源的地址
之前在socket编程那里,url就是网址,比如说http://www.baidu.com
在这里,url表示服务器上的资源的地址
JDBC URL的标准由三部分组成,各部分间用冒号分隔。
其形式为–jdbc:子协议:子名称
(1)协议:JDBC URL中的协议总是jdbc
(2)子协议:子协议用于标识一个数据库驱动程序
(3)子名称:一种标识数据库的方法。子名称可以依不同的子协议而变化,用子名称的目的是为了定位数据库
提供足够的信息。包含主机名(对应服务端的ip地址),端口号,数据库名
要素三:用户名和密码
将用户名和密码封装在Properties对象中
程序
Properties类继承了Hashtable,结构相对简单
(1)加载驱动
(2)获取连接
所谓的“加载驱动”或者“获取连接”这种说法非常抽象,离程序很远,对具体的代码进行了一层抽象。
加载驱动就是实例化一个Driver接口实现类的对象
获取连接就是实例化一个Connection类的对象
都是实例化了一个对象,万物皆对象
// 获取Driver实现类对象
Driver driver = new com.mysql.jdbc.Driver();
String url = "jdbc:mysql://localhost:3306/test";
// 将用户名和密码封装在Properties中
Properties info = new Properties();
info.setProperty("user", "root");
info.setProperty("password", "xxxx");
Connection conn = driver.connect(url, info);
System.out.println(conn);
获取连接的方式二
对方式一的迭代
为什么迭代
我们希望代码有更好的可移植性,一次编写,多次使用。但是上述代码中显式出现了第三方数据库的API,这意味着换一个场景就需要换掉这个API
// 1.获取Driver实现类对象:使用反射
Class clazz = Class.forName("com.mysql.jdbc.Driver");
Driver driver = (Driver) clazz.newInstance();
// 2.提供要连接的数据库
String url = "jdbc:mysql://localhost:3306/test";
//String url = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8";
// 3.提供连接需要的用户名和密码
Properties info = new Properties();
info.setProperty("user", "root");
info.setProperty("password", "xxxx");
// 4.获取连接
Connection conn = driver.connect(url, info);
System.out.println(conn);
我们使用反射机制,将API中类的空参构造器换成了一个子串
为什么反射机制隐藏了API??
这种方式的结果就叫做“隐藏了API”???体会不深!!!
反射毕竟还是提供了Driver接口实现类的地址,我还是能找到这个实现类啊??
我查到说,反射机制可以用来调用API中被隐藏的类,但是需要提前知道类名以及参数列表
获取连接的方式三DriverManager类(最常用)
继续迭代,使用DriverManager替换Driver接口
前面提到,在编写具体程序时,不需要访问Driver 接口的实现类,驱动程序管理器类(java.sql.DriverManager)去调用这些Driver实现
需要用DriverManager类调用两个静态方法
(1)加载驱动
(2)注册驱动
(3)获取连接
// 1.获取Driver实现类的对象
Class clazz = Class.forName("com.mysql.jdbc.Driver");
Driver driver = (Driver) clazz.newInstance();
// 2.提供另外三个连接的基本信息:
String url = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8";
String user = "root";
String password = "xxxx";
// 注册驱动
DriverManager.registerDriver(driver);
// 获取连接
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
获取连接的方式四
方式三的优化:可以只是加载驱动,不用显示的注册驱动过了
// 1.提供三个连接的基本信息:
String url = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8";
String user = "root";
String password = "xxxx";
// 2.加载Driver
Class.forName("com.mysql.jdbc.Driver");
// 3.获取连接
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
Class.forName(“com.mysql.jdbc.Driver”);这一步就隐式地实现了driver的注册。因为Driver接口的实现类com.mysql.jdbc.Driver类里面有一个用于注册Driver的静态代码块,当Class.forName()加载Driver接口实现类的时候,静态代码块已经执行了
甚至说Class.forName(“com.mysql.jdbc.Driver”);都可以不要,因为驱动程序里面的配置文件已经把这个Driver实现类加载好了。但这一步并不是对所有数据库通用的操作
获取连接的方式五
将数据库连接需要的4个基本信息写到配置文件中,url,user,password
在项目的src文件夹下,新建一个文件作为配置文件
前面讲反射机制的时候,讲过使用classloader对象来加载配置文件,调用getResourceAsStream()方法
//1.读取配置文件中的4个基本信息
InputStream is = ConnectionTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
//2.加载驱动
Class.forName(driverClass);
//3.获取连接
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
ConnectionTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
这一步看起来很复杂,他的重点是获取类的加载器(不管是哪一个类),然后调用getResourceAsStream方法
ConnectionTest.class:获取Class类的对象
ConnectionTest.class.getClassLoader():获取类加载器
getResourceAsStream(): 读取文件,存到输入流对象
Properties类和双列数据深度绑定,专门用于读取配置文件
好处
①实现了代码和数据的分离,如果需要修改配置信息,直接在配置文件中修改,不需要深入代码
②如果修改了配置信息,省去重新编译的过程。
三. 使用PreparedStatement实现CRUD操作
CRUD说的就是增查改删。 C:就是创建(Create), R:就是查找(Retrieve), U:就是更改(Update), D:就是删除(Delete)
在 java.sql 包中有 3 个接口分别定义了对数据库的调用的不同方式:
(1)Statement接口:用于执行静态 SQL 语句并返回它所生成结果的对象。
(2)PreparedStatement接口:SQL 语句被预编译并存储在此对象中,可以使用此对象多次高效地执行该语句。
(3)CallableStatement接口:用于执行 SQL 存储过程
感觉上statement取得是“说明,报告”这个意思,说明他接下来要进行怎样的操作,增删改查中的哪一种
使用Statement操作数据表的弊端
举了这么一个例子,用户登录
(1)首先,用户名以及对应的密码存在数据库的表中
(2)客户端连接数据库
(3)客户端输入一组用户名,密码,去查这组数据是否在数据库的相应的表中
Statement接口导致SQL注入
一句话就是:使用Statement接口,会导致 使用数据表中不存在的用户名和密码也能登陆成功
SELECT USER,PASSWORD
FROM user_table
WHERE USER = '"+ user +"' AND PASSWORD = '"+ password +"'
SELECT USER,PASSWORD
FROM user_table
WHERE USER = '1' OR ' AND password = '=1 OR '1' = '1'
"SELECT user,password FROM user_table WHERE user = '"+ user +"' AND password = '"+ password +"'"
"SELECT user,password FROM user_table WHERE user = '1' or ' AND password = '=1 or '1' = '1'"
在具体的代码中,查询语句是以字符串的形式存在的.我们本来的过滤条件是且的关系,但是通过注入,将过滤条件改成了或的关系
SQL注入的查询语句是一个完整的字符串,不像正常的那种,存在拼串操作。SQL注入的查询语句直接定死了。我觉得应该和字符串解析有关,从字符串到具体的sql查询命令的转换存在漏洞
SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的 SQL 语句段或命令
解决方案
使用PreparedStatement接口替换Statement接口,也就是加上了对用户输入数据的检查
PreparedStatement接口的好处
(1)声明的sql语句不需要拼串了
(2)解决了SQL注入问题。带有占位符的sql语句在填充占位符之前会进行预编译,意味着除了占位符的内容之外的所有关系都固定下来了
(3)占位符可以填充的内容非常丰富,包括IO流。这样一来可以处理Blob数据
(4)更高效地批量操作
PreparedStatement接口的使用
PreparedStatement接口是Statement接口的子接口
增(插入)
从代码层面看,curd四个操作都封装在方法当中,需要先连数据库服务器,再由PreparedStatement接口对象去负责增删改查:
(1)通过调用 Connection 对象的prepareStatement(String sql) 方法获取 PreparedStatement 对象
(2)PreparedStatement 对象所代表的 SQL 语句中的参数用问号(?)来表示
(3)调用 PreparedStatement 对象的setXxx() 方法来设置这些参数
(4) setXxx() 方法有两个参数,第一个参数是要设置的 SQL 语句中的参数的索引(从 1开始),第二个是设置的 SQL 语句中的参数的值
还是有很值得说的几点:
(1)配置文件不要加空格
我无意间在后面加了些空格,导致无法找到这个类,排查了很久
(2)最开始我一直以为是在配置文件中就要写要操作的数据表。但实际上,配置文件是说明我们要连接的是mysql服务器的哪一个数据库
(3)我们要操作的数据表是在sql语句中声明的
(4)sql语句在这里以字符串的形式存在
(5)调用prepareStatement(sql)创建一个CallableStatement对象,但是CallableStatement也是一个类,它是PreparedStatement接口的子接口。最终返回的是一个PreparedStatement实例。后面还用PreparedStatement引用去调用了方法,这里我没整明白,到底是哪个类的实例,接口是不能实例化的,所以一定有个实现类
(6)增加的具体数据在声明sql语句时用的是占位符-问号
(7)curd之前都要先连接服务器的数据库
public void testInsert() {
Connection conn = null;
PreparedStatement ps = null;
try {
// 省略掉了前面读取配置,加载驱动,注册驱动
// 到这里成功获取连接
conn = DriverManager.getConnection(url, user, password);
//4.预编译sql语句,返回PreparedStatement的实例
String sql = "insert into customers(name,email,birth)values(?,?,?)";//?:占位符,不是通配符,很重要
ps = conn.prepareStatement(sql);
//5.填充占位符,索引从1开始,符合实际生产的需要
ps.setString(1, "哪吒");
ps.setString(2, "nezha@gmail.com");
// 获取格式化的日期
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
java.util.Date date = sdf.parse("1000-01-01");
ps.setDate(3, new Date(date.getTime()));
//6.执行操作
ps.execute();
} catch (Exception e) {
e.printStackTrace();
}finally{
//7.资源的关闭
try {
if(ps != null)
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
try {
if(conn != null)
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
改
将获取连接和关闭资源两个操作封装到工具类了
public void testUpdate(){
Connection conn = null;
PreparedStatement ps = null;
try {
conn = JDBCUtils.getConnection();
String sql = "UPDATE customers SET `name` = ? WHERE id = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, "莫扎特");
ps.setObject(2, 18);
ps.execute();
System.out.println("修改数据成功");
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
JDBCUtils.closeResources(conn, ps);
}
}
通用的增删改
sql语句会不一样,再往深看,占位符的数量不一样
像这种不确定的部分,就抽象为参数
可变形参
在编写方法的过程中,可能会遇见一个方法有不确定参数个数的情况,一般有两种做法:
(1)重载(扩展非常麻烦,得加非常多的重载方法)
(2)使用数组作为参数
public void method(int...args);
指明可变形参的类型,而且可变形参只能有一个
变量args底层是一个数组
程序
还是有一些值的说的点:
(1)可变形参的应用
(2)遇到一个问题,程序执行成功,但是数据表没改
排查了很久,结果发现是PreparedStatement实例没有执行
(3)完整的过程要记牢:读取配置,加载驱动,注册驱动,获取连接,声明sql语句,调用连接实例的prepareStatement方法,设置sql语句参数,PreparedStatement实例执行,关闭资源
(4)单元测试还遇到一个初始化错误的问题。原因在于对执行单元测试的方法有要求:权限必须是public,返回值需为void,不能用static修饰,不能带有参数。我将一个带参数的方法加了单元测试,报初始化错误
public void testCommonUpdate(){
String sql = "DELETE FROM customers WHERE id = ?";
update(sql,3);
System.out.println("operation over");
}
public void update(String sql, Object ...args){
//get connection
Connection conn = null;
//PreparedStatement object take in charge of curd operation
PreparedStatement ps = null;
try {
conn = JDBCUtils.getConnection();
ps = conn.prepareStatement(sql);
//填充占位符
for(int i = 0;i < args.length;i++){
ps.setObject(i+1, args[i]);
}
ps.execute();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
//close all of resources
JDBCUtils.closeResources(conn, ps);
}
}
查询
java bean
大家对豆子的印象是什么,大概就是只能看到豆子硬硬的外皮,而看不到内部的东西。那么在java中,bean可以看成是一个黑盒子,即只需要知道其功能而不必知道其内部构造和设计
笼统来说就是一个类,一个可复用的类
java bean和java utils还不一样,前者放实体类,后者是一些可调用的方法
Java语言中的许多库类名称,多与咖啡有关,如JavaBeans(咖啡豆)、NetBeans(网络豆)以及ObjectBeans (对象豆)等等
程序
和增删改的区别在于要处理返回的结果集
在java万物皆对象的背景下,结果集也要由一个引用去指向
java.sql中有一个ResultSet类来接收返回的结果集
(1)之前我们说过数据表是一个类,每行记录是这个类的一个实例。其实这就很像容器。处理结果集的方式很像当初使用迭代器的方式,但也有区别
(2)迭代器有hasNext方法返回bool值,next方法有两个作用,一是充当指针,负责指针下移,二是返回所指向的实例的相关数据
(3)ResultSet类有next方法,负责两件事,一是返回bool值,二是充当指针,负责指针下移
(4)ORM,对象关系映射,也就是前面说的数据表是一个类,每行记录是这个类的一个实例。但这并不是mysql提出的,而是在使用java操作mysql数据库的过程中,由java提出的映射
(5)依照ORM思想,造一个对应于数据表的实体类放在java bean当中,数据表的字段作为实体类的属性,都是私有属性。并重写toString方法
(6)我们使用JDBC和数据库交互,第三方API都是隐藏的,面向的是java.sql库下的接口
(7)java与sql数据类型转换表
(8)从结果集中取数据的时候,调用了ResultSet实例的getXXX方法,其参数就是sql语句中字段的索引,从1开始。依据这个索引来区分字段
public void testQuery1(){
Connectionconn = null;
PreparedStatement ps = null;
//执行并返回结果集
ResultSet resultSet = null;
try {
conn = JDBCUtils.getConnection();
String sql = "select id,name,email,birth from customers where id = ?";
ps = conn.prepareStatement(sql);
ps.setObject(1, 1);
resultSet = ps.executeQuery();
//处理结果集
if(resultSet.next()){
//next():判断结果集的下一条是否有数据,如果有数据返回true,并指针下移;如果返回false,指针不会下移。
//获取当前这条数据的各个字段值
int id = resultSet.getInt(1);
String name = resultSet.getString(2);
String email = resultSet.getString(3);
Date birth = resultSet.getDate(4);
//方式三:将数据封装为一个对象(推荐)
Customer customer = new Customer(id,name,email,birth);
System.out.println(customer);
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
//关闭资源
JDBCUtils.closeResources(conn, ps, resultSet);
}
通用的查询
通用的查询与具体的某次查询的差别在于,通用查询的方法不知道要查询的字段的数量和占位符的数量
对于具体的某次查询,已经定好了要查的字段,所以直接从结果集中依次取出数据,然后用带参的构造器实例化映射类就可以了
int order_id = rs.getInt(1);
String order_name = rs.getString(2);
Date order_date = rs.getDate(3);
Order ord = new Order( order_id,order_name,order_date);
对于通用的查询,我们在获取结果集之前是不知道要查询的字段的,要通过结果集的元数据才能知道查询的字段的数量,字段名,别名
值得注意的有几点:
(1)声明sql语句时使用了占位符,每个占位符都需要手动设置以完成sql语句的编写。这些设置在for循环中完成
(2)要注意占位符的数量和要查询字段的数量不是一个东西,我最开始就搞错了
(3)结果集的相关信息(不是数据,数据放在ResultSet实例中)放在ResultSetMetaData类(元数据)的实例中,可以获取要查询的字段名和总的字段数量。
(4)具体的值是由ResultSet类实例来提供的,不要和元数据搞混了
(5)最重要的一点,将查询到的数据赋值。赋值给数据表映射到java中的类实例的某个属性。这一点要单独说
将查询到的数据赋值给数据表映射到java中的类实例的某个属性-反射
假设有一个类,类有一个整型的属性叫field_1,一个浮点型的属性叫field_2。请问我要如何以字符串的形式获得属性名???或者说假设有一个字符串叫field_1,请问如何只通过这个字符串给field_1赋值???
public void cmpName(String temp, Object columnValue){
switch(temp){
case "id":
this.id = (int)columnValue;
break;
case "name":
this.name = (String)columnValue;
break;
case "email":
this.email = (String)columnValue;
break;
case "birth":
this.birth = (Date)columnValue;
break;
}
}
显然,只通过属性名字符串给属性赋值是可行的。但是,一方面有多少个属性,就得有多少个case语句;另一方面,每一次给属性赋值,都需要跑一遍switch case,这显然是低效的。而且,增加了java bean中实体类的代码量
反射机制可以很好的解决这个问题,当类加载到内存了,类的属性方法构造器等信息也就拿到了
在本题背景下,我们获取到了字符串形式的类属性名,就可以通过这个属性名实例化属性类,从而设置该属性的值 Field field = Customer.class.getDeclaredField(columnName);
使用反射机制,就不需要在实体类中去定义一个用于匹配属性名的方法了,不管是增加还是减少属性的数量,也不需要去维护这个方法,这些变化都随着类加载到内存体现出来了
这也是我第一次体会到反射机制的作用
数据表的字段名和映射类的属性名不同
以数据表映射类的属性名为准
在sql语句中给字段名取别名
元数据将列名和字段的别名做了区分
不再是调用元数据实例的getColumnName方法,
而是调用getColumnLabel
程序
下面的通用代码还是有地方固定了,就是数据表映射到java中的实体类Customer固定了,换一张数据表就行不通了。因为实体类的属性固定了
也就是说下面的通用代码是只适用于customers这张表的通用查询
public void testOrderForQuery(){
String sql ="select order_id orderId, order_name orderName, order_date orderDate from `order` where order_id = ?";
Order ord = orderForQuery(sql, 1);
System.out.println(ord);
System.out.println("操作成功");
}
public Order orderForQuery(String sql, Object ...args){
//获取连接
Connection conn = null;
//获取预编译的sql语句
PreparedStatement ps = null;
//接收返回的结果集
ResultSet rs = null;
try {
//获取连接
conn = JDBCUtils.getConnection();
//获取预编译的sql语句
ps = conn.prepareStatement(sql);
//填充占位符
for(int i = 0;i < args.length;i++){
ps.setObject(i+1, args[i]);
}
//接收返回的结果集
rs = ps.executeQuery();
//获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
//从元数据中得到列数
int columnCount = rsmd.getColumnCount();
//从结果集中取数据
if(rs.next()){
//接收数据的order类对象
Order ord = new Order();
//循环取出一行中每个字段的数据
for(int i = 1;i <= columnCount;i++){
//获取每个字段的数据
Object columnValue = rs.getObject(i);
//通过元数据取得列名
//String columnName = rsmd.getColumnName(i);
String columnLabel = rsmd.getColumnLabel(i);
//通过反射为order对象的属性赋值
Field field = Order.class.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(ord, columnValue);
}
return ord;
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
//关闭资源
JDBCUtils.closeResources(conn, ps, rs);
}
return null;
}
通用查询的骨架
针对不同表的通用查询-一条记录
分析前面针对单张表的通用查询的程序,可以知道程序重点要改进的地方就是要告诉通用查询方法,需要用哪一个映射类来接收结果集的数据
从下面的程序中也可以看出,使用反射机制告诉通用查询方法,用的是哪一个映射类
public void testGetInstance(){
String sql = "select id,name,email from customers where id = ?";
Customer customer = getInstance(Customer.class, sql, 12);
System.out.println(customer);
System.out.println("操作成功");
}
public <T> T getInstance(Class<T> clazz, String sql, Object ...args){
//获取连接
//获取预编译的sql语句
//接收返回的结果集
try {
//获取连接
//获取预编译的sql语句
//填充占位符
for(int i = 0;i < args.length;i++){
ps.setObject(i+1, args[i]);
}
//接收返回的结果集
//获取结果集的元数据
//从元数据中得到列数
//从结果集中取数据
if(rs.next()){
//接收数据的映射类对象
T t = clazz.newInstance();
//循环取出一行中每个字段的数据
for(int i = 1;i <= columnCount;i++){
//获取每个字段的数据
//通过元数据取得列名
//通过反射为order对象的属性赋值
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t, columnValue);
}
return t;
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
//关闭资源
JDBCUtils.closeResources(conn, ps, rs);
}
return null;
}
针对不同表的通用查询-多条记录
基于ORM思想(对象关系映射),多条记录就是映射类的多个对象。一般我们使用容器来存储多个同类型的实例
就多了关于容器的部分,造容器,向容器添加对象,遍历容器
public void testGetForList(){
String sql = "select id,name,email from customers where id < ?";
List<Customer> list = getForList(Customer.class, sql, 12);
//遍历返回的容器
list.forEach(System.out::println);
System.out.println("操作完成");
}
public <T> List<T> getForList(Class<T> clazz, String sql, Object ...args){
//获取连接
//获取预编译的sql语句
//接收返回的结果集
try {
//获取连接
//获取预编译的sql语句
//填充占位符
//接收返回的结果集
//获取结果集的元数据
//从元数据中得到列数
//创建容器List
ArrayList<T> list = new ArrayList<T>();
//从结果集中取数据
while(rs.next()){
//接收数据的映射类对象
//循环取出一行中每个字段的数据
for(int i = 1;i <= columnCount;i++){
//获取每个字段的数据
//通过元数据取得列名
//通过反射为order对象的属性赋值
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t, columnValue);
}
//向集合中添加映射类的对象
list.add(t);
}
return list;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
//关闭资源
JDBCUtils.closeResources(conn, ps, rs);
}
return null;
}
四. 操作BLOB类型字段
之前提到过,声明sql语句时使用了占位符,获得预编译的sql语句之后,再填充占位符
可以使用IO流填充占位符,也就意味着数据库提供了一种数据类型可以存储图片,视频
还有一点,比如说MediumBlob类型,有一个默认的packet大小是1M(我也不知道这个packet是啥),反正就是默认情况下,超过1M的图片是不能存的。需要在数据库的配置文件中去手动设置max_allowed_packet=16M
插入Blob类型数据
简单来说,有些字段的类型是Blob类型,这就跟有些字段是int类型一样,造数据表的时候确定好的。Blob类型的数据可以用java中的IO流实例来赋值。具体到程序,就是在填充占位符时,可以使用IO流实例来填充
填充占位符时,PreparedStatement实例调用了区别于之前的方法,调用的是setBlob()方法
//向数据表customers的Blob类型的字段插入数据
@Test
public void testInsert(){
Connection conn = null;
PreparedStatement ps = null;
try {
conn = JDBCUtils.getConnection();
String sql = "insert into customers(name,email,birth,photo) values(?,?,?,?)";
ps = conn.prepareStatement(sql);
ps.setObject(1, "张宇豪");
ps.setObject(2, "zhang@qq.com");
ps.setObject(3, "1992-09-08");
FileInputStream is = new FileInputStream(new File("playgirl.jpg"));
ps.setBlob(4, is);
ps.execute();
System.out.println("操作完成");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
JDBCUtils.closeResources(conn, ps);
}
}
对于读取图片,我们使用节点字节输入流FileInputStream
读取Blob类型数据
值得说的几点:
(1)从数据库读取Blob类型的数据,不是像一般类型的数据,最后还能用toString方法输出,Blob类型的数据最后是要持久化的
(2)ResultSet实例调用getBlob方法来获取Blob类型的实例
(3)Blob类型的实例又调用getBinaryStream方法得到一个基类InputStream的实例
//查询Blob类型字段的数据
@Test
public void testQuery(){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
InputStream is = null;
FileOutputStream fos = null;
try {
conn = JDBCUtils.getConnection();
String sql = "select id,name,email,birth,photo from customers where id = ?";
ps = conn.prepareStatement(sql);
ps.setObject(1, 20);
rs = ps.executeQuery();
if(rs.next()){
int id = rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
Date birth = rs.getDate("birth");
Blob photo = rs.getBlob("photo");
//所有的数据都在这个实例里面
is = photo.getBinaryStream();
fos = new FileOutputStream("zhangyuhao.jpg");
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
}
Customer cust = new Customer(id, name, email, birth);
System.out.println(cust);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
try {
if(fos != null)
fos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
if(is != null)
is.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
JDBCUtils.closeResources(conn, ps, rs);
}
}
五. 批量插入
像删,改天然就具有批量的性质,所有批量操作更多说的是批量插入数据
使用PreparedStatement实现批量操作的核心就是不断修改占位符
/*
* 使用PreparedStatement实现批量数据的操作
*
* update、delete本身就具有批量操作的效果。
* 此时的批量操作,主要指的是批量插入。使用PreparedStatement如何实现更高效的批量插入?
*
* 题目:向goods表中插入20000条数据
* CREATE TABLE goods(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(25)
);
* 方式一:使用Statement
* Connection conn = JDBCUtils.getConnection();
* Statement st = conn.createStatement();
* for(int i = 1;i <= 20000;i++){
* String sql = "insert into goods(name)values('name_" + i + "')";
* st.execute(sql);
* }
*
*/
//批量插入的方式二:使用PreparedStatement实现
@Test
public void testInsert1(){
Connection conn = null;
PreparedStatement ps = null;
try {
long start = System.currentTimeMillis();
conn = JDBCUtils.getConnection();
String sql = "insert into goods(`name`) values(?)";
ps = conn.prepareStatement(sql);
for(int i = 1;i <= 20000;i++){
ps.setObject(1, "name_" + i);
ps.execute();
}
long end = System.currentTimeMillis();
System.out.println("执行完成,花费的总时间为:"+(end-start));
//花费的总时间为:24.331秒
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
JDBCUtils.closeResources(conn, ps);
}
}
执行20000条记录的插入操作,花费的总时间为:24.331秒,还可以优化
方法三
速度提升力两个数量级
每500条sql语句执行一次
我还犯了一个错误,让PreparedStatement实例去调用的是execute()方法,这样只执行了第500条,1000条,1500条等共40条sql语句。应该调用executeBatch()方法执行addBatch的所有sql语句
优化的原理:
(1)程序和mysql服务器的通信是socket通信,每进行一次通信都会有开销
(2)一条sql语句执行一次,相当于总共进行了20000次通信,这个开销是巨大的
(3)一次性像服务器传入多条sql语句,虽然数据量增大也会带来一定的开销,但相比与启动一次通信,这个开销是相当小的
(4)就好比之前的IO流,如果一个字节一个字节的读数据,其效率是远低于一次读一串数据
(5)还包括硬盘对于大文件的读写是远远快过大量小文件的读写
/*
* 批量插入的方式三:
* 1.addBatch()、executeBatch()、clearBatch()
* 2.mysql服务器默认是关闭批处理的,我们需要通过一个参数,让mysql开启批处理的支持。
* ?rewriteBatchedStatements=true 写在配置文件的url后面
* 3.使用更新的mysql 驱动:mysql-connector-java-5.1.37-bin.jar
*/
@Test
public void testInsert2(){
Connection conn = null;
PreparedStatement ps = null;
try {
long start = System.currentTimeMillis();
conn = JDBCUtils.getConnection();
String sql = "insert into goods(`name`) values(?)";
ps = conn.prepareStatement(sql);
for(int i = 1;i <= 20000;i++){
ps.setObject(1, "name_" + i);
ps.addBatch();
if(i % 500 == 0){
ps.executeBatch();
ps.clearBatch();
}
}
long end = System.currentTimeMillis();
System.out.println("执行完成,花费的总时间为:"+(end-start));
//花费的总时间为:0.317秒
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
JDBCUtils.closeResources(conn, ps);
}
}
如果是插入百万条数据,耗时3.8秒,还可以优化
方法四-终极方案
truncate和delete from的区别:后者可以通过set autocommit = False来主动停止提交
对于方案三,默认情况下,每五百条sql语句执行一次,就意味着将数据写到数据库中,并提交。提交也会带来开销
因此,在连接处设置不许自动提交数据,之后再主动提交一次
最后耗时2.9秒,提升两倍
//批量插入的方式四:设置连接不允许自动提交数据
@Test
public void testInsert3(){
Connection conn = null;
PreparedStatement ps = null;
try {
long start = System.currentTimeMillis();
conn = JDBCUtils.getConnection();
conn.setAutoCommit(false);
String sql = "insert into goods(`name`) values(?)";
ps = conn.prepareStatement(sql);
for(int i = 1;i <= 1000000;i++){
ps.setObject(1, "name_" + i);
ps.addBatch();
if(i % 500 == 0){
ps.executeBatch();
ps.clearBatch();
}
}
//主动提交数据
conn.commit();
long end = System.currentTimeMillis();
System.out.println("执行完成,花费的总时间为:"+(end-start));
//花费的总时间为:0.317秒
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
JDBCUtils.closeResources(conn, ps);
}
}