1. 程式人生 > 資料庫 >JDBC【4】-- jdbc預編譯與拼接sql對比

JDBC【4】-- jdbc預編譯與拼接sql對比

  • 在jdbc中,有三種方式執行sql,分別是使用Statement(sql拼接),PreparedStatement(預編譯),還有一種CallableStatement(儲存過程),在這裡我就不介紹CallableStatement了,我們來看看Statement與PreparedStatement的區別。
1. 建立資料庫,資料表

資料庫名字是test,資料表的名字是student,裡面有四個欄位,一個是id,也就是主鍵(自動遞增),還有名字,年齡,成績。最後先使用sql語句插入六個測試記錄。

CREATE DATABASE `test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE `student` ( `id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(20) NOT NULL , 
`age` INT NOT NULL , `score` DOUBLE NOT NULL , PRIMARY KEY (`id`)) ENGINE = MyISAM; 
INSERT INTO `student` VALUES (1, '小紅', 26, 83);
INSERT INTO `student` VALUES (2, '小白', 23, 93);
INSERT INTO `student` VALUES (3, '小明', 34, 45);
INSERT INTO `student` VALUES (4, '張三', 12, 78);
INSERT INTO `student` VALUES (5, '李四', 33, 96);
INSERT INTO `student` VALUES (6, '魏紅', 23, 46);

建立對應的學生類:

/**
 * student類,欄位包括id,name,age,score
 * 實現無參構造,帶參構造,toString方法,以及get,set方法
 * @author 秦懷
 */
public class Student {
	private int id;
	private String name;
	private int age;
	private double score;
	
	public Student() {
		super();
		// TODO Auto-generated constructor stub
	}
	public Student(String name, int age, double score) {
		super();
		this.name = name;
		this.age = age;
		this.score = score;
	}
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public double getScore() {
		return score;
	}
	public void setScore(double score) {
		this.score = score;
	}
	@Override
	public String toString() {
		return "Student [id=" + id + ", name=" + name + ", age=" + age
				+ ", score=" + score + "]";
	}
	
}

2.Statement

先來看程式碼,下面是獲取資料庫連線的工具類 DBUtil.class

public class DBUtil {
	private static String URL="jdbc:mysql://127.0.0.1:3306/test";
	private static String USER="root";
	private static String PASSWROD ="123456";
	private static Connection connection=null;
	static{
		try {
			Class.forName("com.mysql.jdbc.Driver");
			// 獲取資料庫連線
			connection=DriverManager.getConnection(URL,USER,PASSWROD);
			System.out.println("連線成功");
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	// 返回資料庫連線
	public static Connection getConnection(){
		return connection;
	}
}

下面是根據id查詢學生資訊的程式碼片段,返回student物件就能輸出了:

	public Student selectStudentByStatement(int id){
	    // 拼接sql語句
		String sql ="select * from student where id = "+id;
		try {
		    // 獲取statement物件
			Statement statement = DBUtil.getConnection().createStatement();
			// 執行sql語句,返回 ResultSet
			ResultSet resultSet = statement.executeQuery(sql);
			Student student = new Student();
			// 一條也只能使用resultset來接收
			while(resultSet.next()){
				student.setId(resultSet.getInt("id"));
				student.setName(resultSet.getString("name"));
				student.setAge(resultSet.getInt("age"));
				student.setScore(resultSet.getDouble("score"));
			}
			return student;
		} catch (SQLException e) {
			// TODO: handle exception
		}
		return null;
	}

我們可以看到整個流程是先獲取到資料庫的連線Class.forName("com.mysql.jdbc.Driver"); connection=DriverManager.getConnection(URL,USER,PASSWROD);獲取到連線之後通過連接獲取statement物件,通過statement來執行sql語句,返回resultset這個結果集,Statement statement = DBUtil.getConnection().createStatement();ResultSet resultSet = statement.executeQuery(sql);,值得注意的是,上面的sql是已經拼接好,寫固定了的sql,所以很容易被注入,比如這句:

sql = "select * from user where name= '" + name + "' and password= '" + password+"'";

如果有人

  • name = "name' or '1'= `1"
  • password = "password' or '1'='1",那麼整個語句就會變成:
sql = "select * from user where name= 'name' or '1'='1' and password= 'password' or '1'='1'";

那麼就會返回所有的資訊,所以這是很危險的。
還有更加危險的,是在後面加上刪除表格的操作,不過一般我們都不會把這些許可權開放的。

// 如果password = " ';drop table user;select * from user where '1'= '1"
// 後面一句不會執行,但是這已經可以刪除表格了
sql = "select * from user where name= 'name' or '1'='1' and password= '' ;drop table user;select * from user where '1'= '1'";

所以預編譯顯得尤為重要了。

3.PreparedStatement預編譯

我們先來看看預編譯的程式碼:

	// 根據id查詢學生
	public Student selectStudent(int id){
		String sql ="select * from student where id =?";
		try {
			PreparedStatement preparedStatement = DBUtil.getConnection()..prepareStatement(sql);
			preparedStatement.setInt(1, id);
			ResultSet resultSet = preparedStatement.executeQuery();
			Student student = new Student();
			// 一條也只能使用resultset來接收
			while(resultSet.next()){
				student.setId(resultSet.getInt("id"));
				student.setName(resultSet.getString("name"));
				student.setAge(resultSet.getInt("age"));
				student.setScore(resultSet.getDouble("score"));
			}
			return student;
		} catch (SQLException e) {
			// TODO: handle exception
		}
		return null;
	}

預編譯也是同樣需要獲取到資料庫連線物件connection,但是sql語句拼接的時候使用了佔位符?,將含有佔位符的sql當引數傳進去,獲取到PreparedStatement預編譯的物件,最後是通過set來繫結引數,然後再去使用execute執行預編譯過的程式碼。這樣就避免了sql注入的問題,同時,由於sql已經編譯過快取在資料庫中,所以執行起來不用再編譯,速度就會比較快。

4.為什麼預編譯可以防止sql注入
  • 在使用佔位符,或者說引數的時候,資料庫已經將sql指令編譯過,那麼查詢的格式已經訂好了,也就是我們說的我已經明白你要做什麼了,你要是將不合法的引數傳進去,會有合法性檢查,使用者只需要提供引數給我,引數不會當成指令部分來執行,也就是預編譯已經把指令以及引數部分割槽分開,引數部分不允許傳指令進來。
    這樣的好處查詢速度提高,因為有了預編譯快取,方便維護,可讀性增強,不會有很多單引號雙引號,容易出錯,防止大部分的sql注入,因為引數和sql指令部分資料庫系統已經區分開。百度文庫裡面提到:傳遞給PreparedStatement物件的引數可以被強制進行型別轉換,使開發人員可以確保在插入或查詢資料時與底層的資料庫格式匹配。
    要是理解不透徹可以這麼來理解:
select * from student where name= ?

預編譯的時候是先把這句話編譯了,生成sql模板,相當於生成了一個我知道你要查名字了,你把名字傳給我,你現在想耍點小聰明,把字串'Jame' or '1=1'傳進去,你以為他會變成下面這樣麼:

select * from student where name= 'Jame' or '1=1'

放心吧,不可能的,這輩子都不可能的啦,資料庫都知道你要幹嘛了,我不是有sql模板了麼,資料庫的心裡想的是我叫你傳名字給我,行,這名字有點長,想害我,可以,我幫你找,那麼資料庫去名字這一欄位幫你找一個叫'Jame' or '1=1'的人,他心裡想這人真逗,沒有這個人,沒有!!!
所以這也就是為什麼預編譯可以防止sql注入的解釋了,它是經過了直譯器解釋過的,解釋的過程我就不囉嗦了,只要是對引數做轉義,轉義之後讓它在拼接時只能表示字串,不能變成查詢語句。

此文章僅代表自己(本菜鳥)學習積累記錄,或者學習筆記,如有侵權,請聯絡作者刪除。人無完人,文章也一樣,文筆稚嫩,在下不才,勿噴,如果有錯誤之處,還望指出,感激不盡~

技術之路不在一時,山高水長,縱使緩慢,馳而不息。

公眾號:秦懷雜貨店