目录
一、操作和访问数据库
使用statement实现对数据表的查询操作
statement中的SQL注入问题
二、PreparedStatement的使用
PreparedStatement的插入数据操作
封装连接和关闭数据库资源
PreparedStatement的修改数据操作
PreparedStatement通用的增删改操作
PreparedStatement的查询操作
针对于具体的某一张表的查询操作
Java与SQL对应数据类型转换表
针对于同一张表的通用的查询操作
针对于customers表的通用查询
针对于order表的通用的查询操作
针对于不同的表的通用的单行查询操作
针对于不同的表的通用的多行查询操作
数据库连接被用于向数据库服务器发送命令和 SQL 语句,并接受数据库服务器返回的结果。其实一个数据库连接就是一个Socket连接。
在 java.sql 包中有 3 个接口分别定义了对数据库的调用的不同方式:
Statement:用于执行静态 SQL 语句并返回它所生成结果的对象。
PrepatedStatement:SQL 语句被预编译并存储在此对象中,可以使用此对象多次高效地执行该语句。
CallableStatement:用于执行 SQL 存储过程
当前我们的user表中有如下的信息
查看一下
注意如果是使用idea编译的话,可能会遇到在@test中scanner无法读取键盘输入的问题,可以参考这篇博文的解决办法,当前测试idea2022这样是可以解决的。
JAVA【idea中的@test使用scanner无法从键盘输入的问题】_桜キャンドル淵的博客-CSDN博客
这里我们是先连接到了我们上述的数据表,然后将数据表中的user和password的内容独取出来,然后编成sql语句,交给我们的mysql,并且如果查询失败就返回用户名不存在或者密码错误,如果查询成功就返回登陆成功。
// 使用Statement的弊端:需要拼写sql语句,并且存在SQL注入的问题
//如何避免出现sql注入:只要用 PreparedStatement(从Statement扩展而来) 取代 Statement
@Test
public void testLogin() {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入用户名:");
//读取当前的文件
String user = scanner.nextLine();
System.out.print("请输入密码:");
//如果是使用next的方法的话,咱输入的一行中要是有空格就直接结束了
//而nextline以换行来代表获取的数据的结束
String password = scanner.nextLine();
//需要将我们user的具体数据传入。
String sql = "SELECT user,password FROM user_table WHERE user = '"+ user +"' AND password = '"+ password +"'";
User returnUser = get(sql, User.class);
if(returnUser != null){
System.out.println("登录成功");
}else{
System.out.println("用户名不存在或密码错误");
}
}
// 使用Statement实现对数据表的查询操作
public T get(String sql, Class clazz) {
T t = null;
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
// 1.加载配置文件
InputStream is = StatementTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
// 2.读取配置信息
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
// 3.加载驱动
Class.forName(driverClass);
// 4.获取连接
conn = DriverManager.getConnection(url, user, password);
st = conn.createStatement();
rs = st.executeQuery(sql);
// 获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
// 获取结果集的列数
int columnCount = rsmd.getColumnCount();
if (rs.next()) {
t = clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
// //1. 获取列的名称
// String columnName = rsmd.getColumnName(i+1);
// 1. 获取列的别名
String columnName = rsmd.getColumnLabel(i + 1);
// 2. 根据列名获取对应数据表中的数据
Object columnVal = rs.getObject(columnName);
// 3. 将数据表中得到的数据,封装进对象
Field field = clazz.getDeclaredField(columnName);
field.setAccessible(true);
field.set(t, columnVal);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return null;
}
如果我们在执行上述代码的过程中输入下面的用户名和密码,(事实上这个用户名和密码根本不存在),但是却返回查询成功的消息。
这就是SQL注入问题
原来我们想表达的是 xx and xx是一个并且的关系,也就是我们的use和password同时正确的时候才返回结果
SELECT user,password FROM user_table WHERE user = '"+ user +"' AND password = '"+ password +"'
然后我们下面的代码将1' or当成了usr,将=1 or '1' = '1当成了password
然后这段数据和我们上面的SQL语句拼接在一起之后意思就变了。
SELECT user,password FROM user_table WHERE user = '1' or ' AND password = '=1 or '1' = '1'
变成了xx or xx的结构
然后这个'1'='1'还一直是对的,所以一直会查出全部的结果。
所以这就是statement的弊端。
如果我们使用preparestatement就不会有这种问题(这里我们下面的写法需要将后面部分看完,此处仅做示例)
@Test
public void testLogin() {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入用户名:");
String user = scanner.nextLine();
System.out.print("请输入密码:");
String password = scanner.nextLine();
String sql = "SELECT user,password FROM user_table WHERE user = ? and password = ?";
User returnUser = getInstance(User.class,sql,user,password);
if(returnUser != null){
System.out.println("登录成功");
}else{
System.out.println("用户名不存在或密码错误");
}
}
public T getInstance(Class clazz,String sql, Object... args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConnection();
ps = conn.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
// 获取结果集的元数据 :ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
// 通过ResultSetMetaData获取结果集中的列数
int columnCount = rsmd.getColumnCount();
if (rs.next()) {
T t = clazz.newInstance();
// 处理结果集一行数据中的每一个列
for (int i = 0; i < columnCount; i++) {
// 获取列值
Object columValue = rs.getObject(i + 1);
// 获取每个列的列名
// String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
// 给t对象指定的columnName属性,赋值为columValue:通过反射
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t, columValue);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(conn, ps, rs);
}
return null;
}
那为什么我们的preparestatement就可以避免SQL注入问题,但是我们的statement就无法避免呢?
因为我们statement是将我们的传入的数据拼接到原有的SQL语句中,这种方式会导致整个SQL语句存在歧义。
但是如果是preparestatement,其原理是先预编译我们的部分SQL语句,也就是在还没有将我们的占位符填充进去的时候,我们的SQL已经将语句编译过了。说如果是and的话,其查询关系中的并列关系就已经确定下来了。然后我们之前的例子1' or 就认为是user,=1 or '1就认为是password无法再改变预编译过的关系。
通过调用 Connection 对象的 createStatement() 方法创建该对象。该对象用于执行静态的 SQL 语句,并且返回执行结果。
Statement 接口中定义了下列方法用于执行 SQL 语句:
int excuteUpdate(String sql):执行更新操作INSERT、UPDATE、DELETE ResultSet executeQuery(String sql):执行查询操作SELECT
但是使用Statement操作数据表存在弊端:
问题一:存在拼串操作,繁琐
问题二:存在SQL注入问题
SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的 SQL 语句段或命令(如:SELECT user, password FROM user_table WHERE user='a' OR 1 = ' AND password = ' OR '1' = '1') ,从而利用系统的 SQL 引擎完成恶意行为的做法。
对于 Java 而言,要防范 SQL 注入,只要用 PreparedStatement(从Statement扩展而来) 取代 Statement 就可以了。
除了解决了statement拼串,SQL的问题之外,preparedstatement还有哪些好处呢?
1.preparedstatement操作Blob的数据,而statement做不到(因为我们的statement传入的是占位符,所以我们就可以使用流的方式填充文件,图片之类的)
2.preparedstatement可以实现更高效的批量插入操作。
// 向customers表中添加一条记录
@Test
public void testInsert() {
Connection conn = null;
PreparedStatement ps = null;
try {
// 1.读取配置文件中的4个基本信息
// 使用ClassLoader.getSystemClassLoader来创建一个系统加载器
//jdbc.properties就是我们之前讲过的配置文件
InputStream is = ClassLoader.getSystemClassLoader().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.获取连接
conn = DriverManager.getConnection(url, user, password);
//4.预编译sql语句,返回PreparedStatement的实例
String sql = "insert into customers(name,email,birth)values(?,?,?)";//?:占位符
ps = conn.prepareStatement(sql);
//5.填充占位符
//第一个参数为parameterIndex,第二个参数为具体要输入的值
//因为是和MySQL交互,所以索引也是从1开始的。
ps.setString(1, "催逝员");
ps.setString(2, "[email protected]");
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();
}
}
}
执行完上述代码之后,我们就能在我们的数据表中找到我们的催逝员了
对于数据库的增删查看如果我们每次都要去写数据库的连接和关闭连接的操作就会非常麻烦,我们可以将这些功能封装起来。
在util下创建我们的要封装的功能。
//连接数据库操作
public static Connection getConnection() throws Exception {
// 1.读取配置文件中的4个基本信息
InputStream is = ClassLoader.getSystemClassLoader().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);
return conn;
}
//资源的关闭操作
public static void closeResource(Connection conn,Statement ps){
try {
if(ps != null)
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
try {
if(conn != null)
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
我们上面封装过数据库连接和关闭之后,我们就可以调用上面的代码,让我们的程序更加简洁。
//修改customers表的一条记录
@Test
public void testUpdate(){
Connection conn = null;
PreparedStatement ps = null;
try {
//1.获取数据库的连接
conn = JDBCUtils.getConnection();
//2.预编译sql语句,返回PreparedStatement的实例
String sql = "update customers set name = ? where id = ?";
ps = conn.prepareStatement(sql);
//3.填充占位符
ps.setObject(1,"莫扎特");
ps.setObject(2, 18);
//4.执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
}finally{
//5.资源的关闭
JDBCUtils.closeResource(conn, ps);
}
}
对于增删改操作,我们发现仅仅是参数个数不同,其基本形式都是传入一条SQL语句,然后填补SQL语句中的占位符,所以我们可以从中优化我们之前的代码,使之能够实现增删改操作。
@Test
public void testCommonUpdate(){
//测试删除操作
// String sql = "delete from customers where id = ?";
// update(sql,3);
//测试修改操作
String sql = "update `order` set order_name = ? where order_id = ?";
update(sql,"王大队长","2");
}
//通用的增删改操作
public void update(String sql,Object ...args){//sql中占位符的个数与可变形参的长度相同!
Connection conn = null;
PreparedStatement ps = null;
try {
//1.获取数据库的连接
conn = JDBCUtils.getConnection();
//2.预编译sql语句,返回PreparedStatement的实例
ps = conn.prepareStatement(sql);
//3.填充占位符
for(int i = 0;i < args.length;i++){
//注意我们的编号是从1开始的。
ps.setObject(i + 1, args[i]);//小心参数声明错误!!
}
//4.执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
}finally{
//5.资源的关闭
JDBCUtils.closeResource(conn, ps);
}
}
上述方法通用于同一个数据库中的不同的表。下面的图是测试修改操作时的结果,可以看到我们第二行的数据已经被修改为了王大队长了。
增删改是没有返回情况的,但是查询是有返回一个结果集的,我们应该如何去获取这个结果集然后处理数据呢?
这是我们的customers数据表
下面的方法是仅仅针对于查询固定格式的SQL语句的查询操作。 也就是我们指定了查询id,name,email,birth的值,不能够改动查询的项目。
@Test
public void testQuery1() {
Connection conn = 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);
//第一个参数是parameterIndex的编号(注意编号是从1开始的)
//第二个参数是我们想要查询的id的值
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);
//方式一:
// System.out.println("id = " + id + ",name = " + name + ",email = " + email + ",birth = " + birth);
//方式二:
// Object[] data = new Object[]{id,name,email,birth};
//方式三:将数据封装为一个对象(推荐)
Customer customer = new Customer(id, name, email, birth);
System.out.println(customer);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
//关闭资源
//结果集也需要关闭。
JDBCUtils.closeResource(conn, ps, resultSet);
}
}
在上面的代码中,我们推荐将返回的结果封装成一个对象
这里由于ORM编程思想,(object relational mapping)
一个数据表对应一个Java类
表中的一条记录对应Java类的一个对象
表中的一个字段对应Java类的一个属性
Java类型 | SQL类型 |
---|---|
boolean | BIT |
byte | TINYINT |
short | SMALLINT |
int | INTEGER |
long | BIGINT |
String | CHAR,VARCHAR,LONGVARCHAR |
byte array | BINARY , VAR BINARY |
java.sql.Date | DATE |
java.sql.Time | TIME |
java.sql.Timestamp | TIMESTAMP |
@Test
public void testQueryForCustomers(){
String sql = "select id,name,birth,email from customers where id = ?";
Customer customer = queryForCustomers(sql, 13);
System.out.println(customer);
sql = "select name,email from customers where name = ?";
Customer customer1 = queryForCustomers(sql,"周杰伦");
System.out.println(customer1);
}
public Customer queryForCustomers(String sql,Object...args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConnection();
ps = conn.prepareStatement(sql);
for(int i = 0;i < args.length;i++){
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
//获取结果集的元数据 :ResultSetMetaData
//什么是元数据,元数据就是修饰现有数据的数据
//就跟我们的元注解是修饰现有注解的注解一样。
ResultSetMetaData rsmd = rs.getMetaData();
//通过ResultSetMetaData获取结果集中的列数
int columnCount = rsmd.getColumnCount();
if(rs.next()){
Customer cust = new Customer();
//处理结果集一行数据中的每一个列
for(int i = 0;i
这是我们的order表
首先是指定查询格式版本的查询操作
@Test
public void testQuery1(){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConnection();
String sql = "select order_id,order_name,order_date from `order` where order_id = ?";
ps = conn.prepareStatement(sql);
ps.setObject(1, 1);
rs = ps.executeQuery();
if(rs.next()){
int id = (int) rs.getObject(1);
String name = (String) rs.getObject(2);
Date date = (Date) rs.getObject(3);
Order order = new Order(id, name, date);
System.out.println(order);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
JDBCUtils.closeResource(conn, ps, rs);
}
}
下面是针对于order表的通用的查询操作
/*
* 针对于表的字段名与类的属性名不相同的情况:
* 1. 必须声明sql时,使用类的属性名来命名字段的别名
* 2. 使用ResultSetMetaData时,需要使用getColumnLabel()来替换getColumnName(),
* 获取列的别名。
* 说明:如果sql中没有给字段其别名,getColumnLabel()获取的就是列名
*
*
*/
@Test
public void testOrderForQuery(){
//我们需要给字段其别名,来让我们order类中的属性的名字和我们的表中的列名一一对应。
String sql = "select order_id orderId,order_name orderName,order_date orderDate from `order` where order_id = ?";
Order order = orderForQuery(sql,1);
System.out.println(order);
}
public Order orderForQuery(String sql,Object...args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConnection();
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 = new Order();
for(int i = 0;i < columnCount;i++){
//获取每个列的列值:通过ResultSet
Object columnValue = rs.getObject(i + 1);
//通过ResultSetMetaData
//获取列的列名:getColumnName() --不推荐使用
//因为我们为了让我们列的名字和我们order对应的属性相同,我们需要使用别名,但是getcolumnName获取的
//是原来的列名,要获取别名的话我们需要使用getColumnLabel
//获取列的别名:getColumnLabel()
// String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
//通过反射,将对象指定名columnName的属性赋值为指定的值columnValue
Field field = Order.class.getDeclaredField(columnLabel);
field.setAccessible(true);
//给order这个对象当前的columnName的属性值赋值为columnValue
field.set(order, columnValue);
}
//将我们获取到的具有具体属性值的对象返回。
return order;
}
} catch (Exception e) {
e.printStackTrace();
}finally{
JDBCUtils.closeResource(conn, ps, rs);
}
return null;
}
针对于不同的表我们想要获取到的查询数据的结构一定是不同的,所以我们需要在查询的时候将我们查询的结构传入,然后利用反射机制构造出对应的对象,然后将对应的对象返回。
@Test
public void testGetInstance(){
String sql = "select id,name,email from customers where id = ?";
//由于是针对任意表,我们需要将我们的对应的类的类型传入
Customer customer = getInstance(Customer.class,sql,12);
System.out.println(customer);
//下面的这张表的字段名和我们类中的属性的名称不同,所以我们需要取别名统一一下。
String sql1 = "select order_id orderId,order_name orderName from `order` where order_id = ?";
Order order = getInstance(Order.class, sql1, 1);
System.out.println(order);
}
//Class中的T就是我们对应的运行时类,如果是Order类那我们的T就是order,
//然后我们返回的就是我们的T类,然后这个T是一个参数,所以我们要指定泛型方法
public T getInstance(Class clazz,String sql, Object... args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConnection();
ps = conn.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
// 获取结果集的元数据 :ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
// 通过ResultSetMetaData获取结果集中的列数
int columnCount = rsmd.getColumnCount();
if (rs.next()) {
//任何一个类在创建的时候都需要提供一个空参的构造器
//这里我们就是调用其对应的类的构造器
T t = clazz.newInstance();
// 处理结果集一行数据中的每一个列
for (int i = 0; i < columnCount; i++) {
// 获取列值
Object columValue = rs.getObject(i + 1);
// 获取每个列的列名
// String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
// 给t对象指定的columnName属性,赋值为columValue:通过反射
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t, columValue);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(conn, ps, rs);
}
return null;
}
想要多行查找的话我们就需要将我们的返回的数据用一个数组存放起来,然后将整一个数组返回并且打印出来。
@Test
public void testGetForList(){
String sql = "select id,name,email from customers where id < ?";
List list = getForList(Customer.class,sql,12);
list.forEach(System.out::println);
String sql1 = "select order_id orderId,order_name orderName from `order`";
List orderList = getForList(Order.class, sql1);
orderList.forEach(System.out::println);
}
//返回多条记录的话我们需要将我们的返回值用一个数组整起来。
//下面由于我们需要利用反射机制来构造我们输入的对应的类的对象,所以用Class,其中T就是我们对应的类
//然后List就是我们的返回类型,前面的指定了其中的每一个类型都是T
public List getForList(Class clazz,String sql, Object... args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConnection();
ps = conn.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
// 获取结果集的元数据 :ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
// 通过ResultSetMetaData获取结果集中的列数
int columnCount = rsmd.getColumnCount();
//创建集合对象
ArrayList list = new ArrayList();
while (rs.next()) {
T t = clazz.newInstance();
// 处理结果集一行数据中的每一个列:给t对象指定的属性赋值
for (int i = 0; i < columnCount; i++) {
// 获取列值
Object columValue = rs.getObject(i + 1);
// 获取每个列的列名
// String columnName = rsmd.getColumnName(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
// 给t对象指定的columnName属性,赋值为columValue:通过反射
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t, columValue);
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(conn, ps, rs);
}
return null;
}