JDBC(一)获取连接,PreparedStatement,Blob类型,批量插入

驱动

驱动,也是一种程序,用于驱动相关的软件

驱动的全称是设备驱动程序,由硬件厂商根据操作系统(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);
	}
	
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值