JDBC
1 什么是JDBC?
JDBC(Java DataBase Connectivity),即Java数据库连接!也就是说,Java程序员可以使用JDBC API来操作数据库。
最早JDBC是Java EE中的规范,但是现在已经添加到Java SE中了。也就是说,JDBC API在JDK中就已经存在了。与JDBC相关的包有:java.sql和javax.sql两个包!
2 JDBC原理
早期SUN公司的天才们想编写一套可以连接天下所有数据库的API,但是当他们刚刚开始时就发现这是不可完成的任务,因为各个厂商的数据库服务器差异太大了。后来SUN开始与数据库厂商们讨论,最终得出的结论是,由SUN提供一套访问数据库的规范,并提供连接数据库的协议标准,然后各个数据库厂商会遵循SUN的规范提供一套访问自己公司的数据库服务器的API出现。SUN提供的范围命名为JDBC,而各个厂商提供的,遵循了JDBC规范的,可以访问自己数据库的API被称之为驱动!
现在大家已经知道了,想使用JDBC必须要有驱动才可以,驱动就是JDBC中接口的实现,并且是针对自己厂商的数据库服务器的实现。我们使用的是MySQL数据库服务器,那么就需要有MySQL驱动。
3.JDBC编码步骤
连接数据库需要MySQL的JDBC驱动,即:mysql-connector-java-5.1.13-bin.jar。
对数据库的操作可以大致分为两种:更新操作、查询操作。更新操作就是增、删、改,它没有结果,而查询操作是有结果的。
- A. 注册驱动
- B. 获取与数据库的连接
- C. 得到代表SQL语句的对象
- D. 发送SQL语句:DML、DQL
- E. 获取结果集
- F. 从结果集中获取数据
- G. 关闭
1、注册驱动
Driver d = new Driver(); DriverManager.registerDriver(d); |
其中Driver是com.mysql.jdbc.Driver类型,这种方法使用了硬编码,也就是说将来如果想更换数据库是不可以的,因为代码中使用了MySQL提供的类。
其实JDBC为了让我们不出现硬编码,要求各个厂商提供的驱动都要可以把自己来注册到DriverManager中,答案在Driver()的源码中。
static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can‘t register driver!"); } } |
上面代码是com.mysql.jdbc.Driver类中的静态代码块,它会new一个自己类型的对象,然后再把自己注册到DriverManager中。我们都知道static块会在类被加载时就会执行,也就是说,在JVM加载Driver类时,已经把一个Driver类的对象注册到DriverManager中了,所以我们就不需要再去注册了。我们只需要保证Driver类被加载就OK了!修改上面加载驱动的代码如下:
Class.forName("com.mysql.jdbc.Driver"); |
上面代码是用来加载com.mysql.jdbc.Driver类的代码,这样就可以把Driver注册到DriverManager中了!
在我们的代码中,不要出现驱动Jar包中的类!这方便我们将来切换数据库!!!
2、获取与数据库的连接
//方式一: Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/day10", "root", "sorry"); //方式二: Properties props = new Properties(); props.put("user", "root");//key看数据库的规定 props.put("password", "sorry"); props.put("useUnicode", "true"); props.put("characterEncoding", "utf8"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/day10", props); //方式三: Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/day10?user=root&password=sorry"); |
还可以使用DriverManager的registerDriver()方法,但这个方法通常我们是不会使用的,因为使用它会导致对特定驱动的依赖。
注意,数据库的URL需要查看相关文档,不同数据库不同版本,URL都不一定一样.
URL: 协议(jdbc):子协议(mysql):主机:端口/数据库
我们知道在使用DriverManager的getConnection()方法时需要给出三个参数,其中一个是url,下面我们来聊一聊这个url是什么东西。JDBC使用的URL用于描述数据的来源,它的语法如下:
可以把它分为三个部分,每个部分中间都是冒号:
l 其中第一部分是jdbc,这是固定的;
l 第二部分为子协议名称,一般都是特定厂商的数据库名称,例如MySQL就是jdbc:mysql:…,Oracle就是jdbc:oracle:…;
l 第三部分由数据库厂商来确定,这一部分通常需要说明数据库服务器主机的IP、端口,以及数据库名称。
MySQL的url:jdbc:mysql://localhost:3306/mydb1
其实还可以给url添加参数,最为常见的参数就是:
l useUnicode=true
l characterEncoding=UTF8
这两个参数是指定获取的Connection使用的编码!当然如果没有指定,那么会使用当前MySQL数据库的字符编码集。因为我们在安装MySQL时已经指定的UTF8,所以就算不指定这两个参数,获取到的连接也同样是UTF8的。
其实在不指定主机和端口时,默认也是连接localhost主机的3306端口,所以可以把url写成如下的样子:
jdbc:mysql:///mydb1
当然,最后的mydb1是要连接的数据库,这部分是不能少的。
建议把url写完整了:
jdbc:mysql://localhost:3306/mydb1?useUnicode=true&characterEncoding=UTF8
3、得到代表SQL语句的对象
Statement stmt = conn.createStatement();
4、发送SQL语句:DML、DQL
ResultSet rs = stmt.executeQuery("selcet chinese,english,math from student");
//Statement常用方法
//ResultSet executeQuery(String sql):sql一般是DQL语句, 用来发送查询语句.
//int executeUpdate(String sql):sql一般是没有返回结果集的语句,比如DML、DDL。返回值是影响到的行数, 用来发送增、删、改语句
//boolean execute(String sql):sql可以任何的语句。返回值:如果执行的sql语句有结果集,返回true,没有结果集,返回false
5、如果是DQL语句,有结果,得到返回的结果
6、遍历结果集
while(rs.next()){ System.out.println("---------------------------"); System.out.print(rs.getObject("chinese")+"\t"); System.out.print(rs.getObject("english")+"\t"); System.out.println(rs.getObject("math")); } |
7、释放占用的资源(官方文档,最好写成工具类.)
前面我们在获取Connection时使用了硬编码,把driverClassName、url、username、password都直接写到了Java代码中,如果将来需要更换数据库,或者更换用户来登录数据库,这都不方便,所以我们应该把连接数据库的数据写到配置文件中,例如:
//JDBC工具类 public class JdbcUtil { private static String driverClass; private static String url; private static String user; private static String password; static{ try { //从配置文件中读取信息 InputStream in = JdbcUtil.class.getClassLoader().getResourceAsStream("dbcfg.properties"); Properties pro = new Properties(); pro.load(in); driverClass = pro.getProperty("driverClass"); url = pro.getProperty("url"); user = pro.getProperty("user"); password = pro.getProperty("password"); Class.forName(driverClass); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(url,user,password); } public static void release(ResultSet rs,Statement stat,Connection conn){ if(rs!=null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } rs = null; } if(stat!=null){ try { stat.close(); } catch (SQLException e) { e.printStackTrace(); } stat = null; } if(conn!=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } conn = null; } } } |
3.SQL攻击
在需要用户输入的地方,用户输入的是SQL语句的片段,最终用户输入的SQL片段与我们DAO中写的SQL语句合成一个完整的SQL语句!例如用户在登录时输入的用户名和密码都是为SQL语句的片段!
演示SQL攻击
首先我们需要创建一张用户表,用来存储用户的信息。
CREATE TABLE tab_user( uid CHAR(32) PRIMARY KEY, username VARCHAR(30) UNIQUE KEY NOT NULL, PASSWORD VARCHAR(30) );
INSERT INTO tab_user VALUES(‘U_1001‘, ‘zs‘, ‘zs‘); SELECT * FROM tab_user; |
现在用户表中只有一行记录,就是zs。
下面我们写一个login()方法!
public void login(String username, String password) { Connection con = null; Statement stmt = null; ResultSet rs = null; try { con = JdbcUtils.getConnection(); stmt = con.createStatement(); String sql = "SELECT * FROM tab_user WHERE " + "username=‘" + username + "‘ and password=‘" + password + "‘"; rs = stmt.executeQuery(sql); if(rs.next()) { System.out.println("欢迎" + rs.getString("username")); } else { System.out.println("用户名或密码错误!"); } } catch (Exception e) { throw new RuntimeException(e); } finally { JdbcUtils.close(con, stmt, rs); } } |
下面是调用这个方法的代码:
login("a‘ or ‘a‘=‘a", "a‘ or ‘a‘=‘a"); |
这行当前会使我们登录成功!因为是输入的用户名和密码是SQL语句片段,最终与我们的login()方法中的SQL语句组合在一起!我们来看看组合在一起的SQL语句:
SELECT * FROM tab_user WHERE username=‘a‘ or ‘a‘=‘a‘ and password=‘a‘ or ‘a‘=‘a‘ |
2 防止SQL攻击
过滤用户输入的数据中是否包含非法字符;
分步交验!先使用用户名来查询用户,如果查找到了,再比较密码;
使用PreparedStatement。
PreparedStatement是Statement的子接口,你可以使用PreparedStatement来替换Statement。
PreparedStatement的好处:
防止SQL攻击;
提高代码的可读性,以可维护性;
提高效率。(支持预编译SQL –占位符’? ’)
2 PreparedStatement的使用
String sql = “select * from tab_student where s_number=?”; PreparedStatement pstmt = con.prepareStatement(sql); pstmt.setString(1, “S_1001”); ResultSet rs = pstmt.executeQuery(); rs.close(); pstmt.clearParameters();//再次使用时需要把原来的设置清空。 pstmt.setString(1, “S_1002”); rs = pstmt.executeQuery(); |
在使用Connection创建PreparedStatement对象时需要给出一个SQL模板,所谓SQL模板就是有“?”的SQL语句,其中“?”就是参数。
在得到PreparedStatement对象后,调用它的setXXX()方法为“?”赋值,这样就可以得到把模板变成一条完整的SQL语句,然后再调用PreparedStatement对象的executeQuery()方法获取ResultSet对象。
注意PreparedStatement对象独有的executeQuery()方法是没有参数的,而Statement的executeQuery()是需要参数(SQL语句)的。因为在创建PreparedStatement对象时已经让它与一条SQL模板绑定在一起了,所以在调用它的executeQuery()和executeUpdate()方法时就不再需要参数了。
PreparedStatement最大的好处就是在于重复使用同一模板,给予其不同的参数来重复的使用它。这才是真正提高效率的原因。
所以,建议大家在今后的开发中,无论什么情况,都去需要PreparedStatement,而不是使用Statement。
4.DAO解耦
把控制权转移到外面,想要创建对象,必须先传参数依赖注入
public void setDao(User dao){
this.dao = dao;
{
另一种方式通过构造函数转过来
单开就是 单例设计模式
//到底使用哪一个,是由自己指定的。是new出来的,没法解耦 //如果由外部传入使用的实现类,这个过程称之为控制反转,就可以解耦了.IoC DI:依赖注入:Spring的核心 //工厂创建模式,就是把创建的细节隐藏起来. private UserDao dao = DaoFactory.getInstance().getUserDaoImpl();// = new UserDaoMySQLEnhanceImpl(); // public UserServiceImpl(UserDao dao){//依赖注入方法一,通过构造方式 // this.dao = dao; // } // public void setDao(UserDao dao){//依赖注入方式二,通过set方法 // this.dao = dao; // } |
//创建DAO实例的工厂:饿汉子式单例 public class DaoFactory { //先创建工厂类,需要私有化,为了能一家在就实例化,需要静态修饰 private static DaoFactory instance = new DaoFactory(); //无参构造函数私有化 private DaoFactory(){} //定义静态方法,调用类 public static DaoFactory getInstance(){ return instance; } //私有Properties类, private static Properties props = new Properties(); static{ //静态代码块,初始化就加载,这样就参数就可以使用字符串,就可以写到配置文件中. InputStream in = DaoFactory.class.getClassLoader().getResourceAsStream("dao.properties"); try { props.load(in); } catch (IOException e) { throw new RuntimeException(e); } } //创建一个UserDaoImpl实例类,需要的参数读取配置文件 //这样就进行了解耦,所有需要的参数可以通过修改配置文件来完成 public UserDao getUserDaoImpl(){ try { String daoImplName = props.getProperty("userDao"); return (UserDao)Class.forName(daoImplName).newInstance(); } catch (Exception e) { throw new RuntimeException(e); } } } |
5 小知识点
读取二进制文件
我们一直在向数据库保存varhcar、int、date等类型的数据,现在我们要尝试把一张图片或一个mp3保存到数据库中去。
首先我们需要创建一张表,表中要有一个mediumblob(16M)类型的字段。
CREATE TABLE tab_bin( id INT PRIMARY KEY AUTO_INCREMENT, filename VARCHAR(100), data MEDIUMBLOB ); |
向数据库插入二进制数据需要使用PreparedStatement为原setBinaryStream(int, InputSteam)方法来完成。
con = JdbcUtils.getConnection(); String sql = "insert into tab_bin(filename,data) values(?, ?)"; pstmt = con.prepareStatement(sql); pstmt.setString(1, "a.jpg"); InputStream in = new FileInputStream("f:\\a.jpg");//得到一个输入流对象 pstmt.setBinaryStream(2, in);// 为第二个参数赋值为流对象 pstmt.executeUpdate(); |
读取二进制数据,需要在查询后使用ResultSet类的getBinaryStream()方法来获取输入流对象。也就是说,PreparedStatement有setXXX(),那么ResultSet就有getXXX()。
con = JdbcUtils.getConnection(); String sql = "select filename,data from tab_bin where id=?"; pstmt = con.prepareStatement(sql); pstmt.setInt(1, 1); rs = pstmt.executeQuery(); rs.next(); String filename = rs.getString("filename"); OutputStream out = new FileOutputStream("F:\\" + filename); //使用文件名来创建输出流对象。 InputStream in = rs.getBinaryStream("data");//读取输入流对象 IOUtils.copy(in, out);// 把in中的数据写入到out中。 out.close(); |
还有一种方法,就是把要存储的数据包装成Blob类型,然后调用PreparedStatement的setBlob()方法来设置数据
con = JdbcUtils.getConnection(); String sql = "insert into tab_bin(filename,data) values(?, ?)"; pstmt = con.prepareStatement(sql); pstmt.setString(1, "a.jpg"); File file = new File("f:\\a.jpg"); byte[] datas = FileUtils.getBytes(file);//获取文件中的数据 Blob blob = new SerialBlob(datas);//创建Blob对象 pstmt.setBlob(2, blob);//设置Blob类型的参数 pstmt.executeUpdate(); |
con = JdbcUtils.getConnection(); String sql = "select filename,data from tab_bin where id=?"; pstmt = con.prepareStatement(sql); pstmt.setInt(1, 1); rs = pstmt.executeQuery(); rs.next(); String filename = rs.getString("filename"); File file = new File("F:\\" + filename) ; Blob blob = rs.getBlob("data"); byte[] datas = blob.getBytes(0, (int)file.length()); FileUtils.writeByteArrayToFile(file, datas); |
批处理SQL
语句不同,有很多行,可以使用addBatch方法批处理,别忘了executeBatch一下.
其实Batch里面就是封装了一个缓存
//向t1表中插入两条记录,删除第一条记录 //利用statement可以执行SQL语句不同的批处理 public void testBatch1(){ Connection conn = null; Statement stmt = null; try{ conn = JdbcUtil.getConnection(); stmt = conn.createStatement(); //把要输入的SQL语句放入字符串变量中去; String sql1 = "insert into t1 (id,name) values(1,‘aaa1‘)"; String sql2 = "insert into t1 (id,name) values(2,‘bbb1‘)"; String sql3 = "delete from t1 where id=1"; //注入SQL语句,是注入,并没有执行; stmt.addBatch(sql1); stmt.addBatch(sql2); stmt.addBatch(sql3); //对注入的SQL语句执行 //数组的元素表示每条语句影响到的行数。 int[] i = stmt.executeBatch(); }catch(Exception e){ throw new RuntimeException(e); }finally{ JdbcUtil.release(null, stmt, conn); } } |
利用PreparedStatement执行批处理,只能用在SQL语句相同的情况下。参数有可能不同
//向t1表中批量插入10条记录,语句相同,参数不同 @Test public void testBatch2(){ Connection conn = null; PreparedStatement stmt = null; try{ conn = JdbcUtil.getConnection(); //一开始就需要把SQL语句加载到PreparedStatement中,用占位符 String sql="insert into t1 (id,name) values(?,?)"; stmt = conn.prepareStatement(sql); //输入SQL语句中的参数 for(int x=0;x<10;x++){ stmt.setInt(1, x+1); stmt.setString(2, "aaa"+(x+1)); //对参数进行注入 stmt.addBatch(); } //对注入的参数执行 stmt.executeBatch(); }catch(Exception e){ throw new RuntimeException(e); }finally{ JdbcUtil.release(null, stmt, conn); } } |
OCI 效率高 但是要装客户文件
Thin 效率低 但是不需要装
一般开发用这个Thin
插入1000001条记录,大数据插入
//向t1表中批量插入1000001条记录 @Test public void testBatch3(){ Connection conn = null; PreparedStatement stmt = null; try{ conn = JdbcUtil.getConnection(); //一开始就需要把SQL语句加载到PreparedStatement中,用占位符 String sql="insert into t1 (id,name) values(?,?)"; stmt = conn.prepareStatement(sql); //输入SQL语句中的参数 for(int x=0;x<1000001;x++){ stmt.setInt(1, x+1); stmt.setString(2, "aaa"+(x+1)); stmt.addBatch(); //定义一个判断,每插入1000条就执行一次,还需要清空注入的语句. if(x%1000==0){ stmt.executeBatch();//执行 stmt.clearBatch();//清空 } } //扫尾执行,因为可能会有尾数 stmt.executeBatch(); }catch(Exception e){ throw new RuntimeException(e); }finally{ JdbcUtil.release(null, stmt, conn); } } |
返回自增长主键
当表的主键是自增长的,那么就是由数据库来维护主键的值。当我们使用Java向数据库插入一行记录后,主键的值我们是不知道的。当然你可以去再查询一次!但是这不是最好的办法。
想获取主键自增长的值,需要在创建PreparedStatement时开始:
String sql = "insert into tt2(name) values(?)"; pstmt = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); //表示在执行SQL语句后会去获取主键自增长的值。 pstmt.setString(1, "hello"); pstmt.executeUpdate(); |
在执行完INSERT语句之后,可以通过PrepareStatement的getGeneratedKeys()方法获取一个结果集对象,然后再获取结果集中唯一一行,唯一一列的数据,这就是主键自增长的值。
ResultSet rs = pstmt.getGeneratedKeys();//获取自增长主键结果集 rs.next(); int id = rs.getInt(1); System.out.println(id); |
调用存储过程
1 创建存储过程
首先创建3个存在过程:
l 无参的;
l 入参的;
l 出参的。
DELIMITER // CREATE PROCEDURE pro1() BEGIN SELECT * FROM stu; END// DELIMITER ; |
DELIMITER // CREATE PROCEDURE pro2(IN _sid VARCHAR(10)) BEGIN SELECT * FROM stu WHERE sid=_sid; END// DELIMITER ; |
DELIMITER // CREATE PROCEDURE pro3(IN _sid VARCHAR(10), OUT _sname VARCHAR(20)) BEGIN SELECT sname INTO _sname FROM stu WHERE sid=_sid; END// DELIMITER ; |
2 调用pro1(无参存储过程)
要调用存储过程需要使用CallableStatement,获取CallableStatement需要调用Connection的prepareCall(String sql)方法。
con = JdbcUtils.getConnection(); String sql = "{call pro1()}";//调用存储过程的SQL语句需要一对大括号括起来 cstmt = con.prepareCall(sql); rs = cstmt.executeQuery();//如果存储过程会返回结果集,那么就调用executeQuery()方法,否则调用executeUpdate()方法。 while(rs.next()) { int colCnt = rs.getMetaData().getColumnCount(); for(int i = 1; i <= colCnt; i++) { Object o = rs.getObject(i); System.out.print(o + " "); } System.out.println(); } |
3 调用有入参存储过程
con = JdbcUtils.getConnection(); String sql = "{call pro2(?)}";//给出一个参数 cstmt = con.prepareCall(sql); cstmt.setString(1, "S_100");//为参数赋值 rs = cstmt.executeQuery(); rs.next(); System.out.println(rs.getString(1) + ", " + rs.getString(2) + ", " + rs.getInt(3) + ", " + rs.getString(4)); |
4 调用有出参的存储过程
con = JdbcUtils.getConnection(); String sql = "{call pro3(?,?)}";//第一个参数是入参,第二个是出参。 cstmt = con.prepareCall(sql); cstmt.setString(1, "S_100"); cstmt.registerOutParameter(2, Types.VARCHAR);// 注册一个出参!说明它的SQL类型 cstmt.execute();//这里调用executeUpdate()、executeQuery()或是execute()都行! String name = cstmt.getString(2);// 获取出参的值 System.out.println(name); |
元数据
1 DatabaseMetaData
获取DatabaseMetaData对象:
Connection con = ... DatabaseMetaData dbmd = con.getMetaData(); |
基本功能
String name = dbmd.getDatabaseProductName();//获取数据库名称:MySQL int v1 = dbmd.getDatabaseMajorVersion();//获取数据库主版本号:5 int v2 = dbmd.getDatabaseMinorVersion();//获取数据库次版本号:1 System.out.println(name + v1 + "." + v2); |
String driverName = dbmd.getDriverName();//获取驱动名 String driverVersion = dbmd.getDriverVersion();//获取驱动版本 System.out.println(driverName + ", " + driverVersion); |
String url = dbmd.getURL();//获取URL String username = dbmd.getUserName();//获取用户名 System.out.println(url); System.out.println(username); |
获取所有数据库名称
ResultSet rs = dbmd.getCatalogs(); while(rs.next()) { System.out.println(rs.getString(1)); } |
获取指定数据库中所有表名称
ResultSet rs = dbmd.getTables("mydb1", null, null, new String[]{"TABLE"}); while(rs.next()) { System.out.println(rs.getString("TABLE_NAME")); } |
2 ParameterMetaData
得到ParameterMataData对象
Connection con = ... String sql = "insert into tab_student value(?,?,?,?)"; PreparedStatement pstmt = con.prepareStatement(sql); ParameterMetaData pmd = pstmt.getParameterMetaData(); |
ParameterMetaData是针对“?”的元数据!但是很多驱动对它的支持不是很好。
int cnt = pmd.getParameterCount();//参数的个数 for(int i = 1; i <= cnt; i++) { System.out.println(pmd.getParameterTypeName(i));//当前参数类型名 System.out.println(pmd.getParameterType(i));//当前参数类型 System.out.println(pmd.getParameterClassName(i));//当前参数Java类型名 System.out.println(pmd.isNullable(i));//当前参数是否可以为NULL } |
3 结果集列数据:ResultSetMetaDate
获取ResultSetMetaData
ResultSet rs = ...; ResultSetMetaData rsmd = rs.getMetaData(); |
方法介绍:
ResultSetMetaData rsmd = rs.getMetaData(); int cnt = rsmd.getColumnCount();//获取结果集列数 for(int i = 1; i <= cnt; i++) { System.out.print(rsmd.getColumnName(i));//获取当前列名称 if(i < cnt) { System.out.print(", "); } } System.out.println(); for(int i = 1; i <= cnt; i++) { System.out.print(rsmd.getColumnClassName(i));//获取当前列Java类型名 if(i < cnt) { System.out.print(", "); } } System.out.println(); while(rs.next()) { for(int i = 1; i <= cnt; i++) { System.out.print(rs.getObject(i));//看清楚,这个是rs不是rsmd if(i < cnt) { System.out.print(", "); } } System.out.println(); } |