Nói về Statement và PreparedStatement [Tất cả trong một – JDBC trong Java]

Nếu bạn đã làm việc với java kết nối cơ sở dữ liệu bằng API JDBC thì bạn có thể biết rằng API JDBC cung cấp ba dạng interface để gửi câu lệnh SQL đến cơ sở dữ liệu và thực thi truy vấn, 3 interface này có tên là Statement , PreparedStatement và CallableStatement . Đầu tiên, Statement được sử dụng để thực thi các truy vấn SQL thông thường, ví dụ: chọn Count (*) từ các khóa học. Bạn cũng có thể sử dụng nó để thực thi các câu lệnh DDL, DML và DCL SQL. Với PreparedStatement chuyên dùng để thực hiện các truy vấn được tham số hóa, ví dụ: chọn * từ các khóa học trong đó CourseId =?, bạn có thể thực thi SQL này nhiều lần bằng cách thay đổi các tham số khóa học. Chúng được biên dịch và lưu trữ vào cuối cơ sở dữ liệu, do đó khá nhanh để thực hiện lặp lại.

Thành viên thứ ba của gia đình có nhiệm vụ chức năng tương tự này là CallableStatement , có mặt để thực thi hoặc gọi sử dụng stored procedures.

Vì vậy, bạn thấy, mỗi lớp thực hiện một chức năng ý nghĩa khác nhau, có một mục đích khác nhau và bạn nên sử dụng chúng cho những gì chúng đã thiết kế cho. Điều rất quan trọng là phải hiểu chúng là gì và mục đích của chúng là gì, cùng với cách sử dụng nó một cách chính xác.

Trong bài viết này, tôi sẽ tập trung vào việc tìm hiểu sự khác biệt giữa hai thành viên đầu tiên của gia đình này, Statement và PreparedStatement , để bạn có thể sử dụng chúng một cách hiệu quả.


Sự khác biệt giữa Statement vs PreparedStatement

Dù sao, không lãng phí thêm thời gian của bạn, hãy xem một số khác biệt chính giữa hai lớp này, chúng dựa trên cú pháp, mục đích, hiệu suất, bảo mật và khả năng.

1. Mục đích

Mục đích duy nhất của PreparedStatement là thực hiện các truy vấn liên kết. Nếu bạn cần thực hiện một truy vấn nhiều lần chỉ với các dữ liệu khác nhau thì hãy sử dụng PreparedStatement và sử dụng trình giữ chỗ, ký hiệu dấu hỏi (?) cho dữ liệu biến truyền vào tham số ? đó.

Khi bạn lần đầu tiên thực hiện truy vấn câu lệnh SQL.SQL đã chuẩn bị, cơ sở dữ liệu sẽ biên dịch nó và lưu trữ nó để sử dụng lại trong tương lai, lần sau bạn gọi cùng một truy vấn nhưng với một tham số khác, thì cơ sở dữ liệu sẽ trả về kết quả gần như ngay lập tức. Do quá trình biên dịch trước này, lớp này được gọi là PreparedStatement trong Java.

Rất hữu ích để xây dựng tìm kiếm và chèn truy vấn, ví dụ: nếu ứng dụng của bạn cung cấp giao diện để tìm kiếm một số dữ liệu, ví dụ: bạn cần tìm kiếm cho khóa học, trong đó có thể tìm theo tên khóa học, giá khóa học và tác giả người hướng dẫn. Bạn có thể tạo PreparedStatement để xử lý việc tìm kiếm đó để có hiệu suất tốt hơn.

Mặt khác, mục đích duy nhất của đối tượng Statement là thực thi truy vấn SQL . Bạn cung cấp cho họ bất kỳ truy vấn nào và nó sẽ thực hiện nó, nhưng không giống như PreparedStatement , nó sẽ không cung cấp tiền biên dịch.

2. Cú pháp

Cú pháp của Statement giống như truy vấn SQL, bạn thực sự có thể sao chép SQL từ trình soạn thảo SQL yêu thích của mình và chuyển nó dưới dạng String sang Statement để thực thi, nhưng đối với PreparedStatement , bạn cần bao gồm các dấu giữ chỗ hay nói cách khcs là tham số hóa tức là dấu hỏi (?) Trong truy vấn SQL

select count(*) from Books; // Sử dụng Statement để thực thi

select * from Books where book_id=?; // sử dụng PreparedStatement

Giá trị thực được đặt trước khi thực hiện truy vấn trong thời gian chạy bằng cách sử dụng các phương thức setXXX() khác nhau, ví dụ: nếu giữ chỗ tham chiếu đến một cột varchar thì bạn có thể sử dụng phương thức setString (value) để đặt giá trị. Tương tự, nếu giữ chỗ tham chiếu đến một cột số nguyên thì bạn có thể sử dụng phương thức setInt(value).

Bạn có thể xem thêm   Nền tảng Java: Làm việc với Cơ sở dữ liệu Sử dụng  khóa học JDBC trên Pluarlsight để tìm hiểu thêm về cách sử dụng các đối tượng này.

JDBC trong java
JDBC trong java

3. Hiệu suất

Nói chung, PreparedStatement cung cấp hiệu năng tốt hơn đối tượng Statement vì biên dịch trước truy vấn SQL trên máy chủ cơ sở dữ liệu.

Khi bạn sử dụng PreparedStatement, truy vấn được biên dịch lần đầu tiên nhưng sau đó nó được lưu trữ tại máy chủ cơ sở dữ liệu, làm cho lần chạy tiếp theo nhanh hơn.

Mặt khác, với đối tượng Statement , ngay cả khi bạn thực hiện cùng một truy vấn, chúng luôn được biên dịch trước và sau đó được thực thi, làm cho chúng chậm hơn so với các truy vấn PreparedStatement.

4. Bảo mật

Các PreparedStatement cũng cung cấp an toàn chống SQL injection, nhưng việc sử dụng không đúng cách có thể gây Statement SQL injection . Nếu bạn còn nhớ, nguyên nhân của việc tiêm SQL là mã SQL độc hại do các hacker tiêm vào.

Ví dụ: bạn có thể đã viết truy vấn ở trên trả về một cuốn sách sau khi chuyển Id như dưới đây:

String id = getFromUser();
String SQL = “select * from Books where book_id=” + id;

Nếu bạn chuyển đối tượng SQL này sang đối tượng Statement thì nó có thể gây ra lỗi SQL nếu người dùng gửi mã SQL độc hại dưới dạng id, ví dụ  1 == 1 OR id , sẽ trả về mỗi cuốn sách từ cơ sở dữ liệu.

Mặc dù sách, có thể không phải là dữ liệu nhạy cảm, nó cũng có thể xảy ra với bất kỳ dữ liệu nhạy cảm nào của người dùng. Tuy nhiên với bảng users, lưu trữ danh sách người dùng thì đây thật sự là một vấn đề trở ngại lớn. PreparedStatement bảo vệ chống lại điều này.


Dưới đây là bảng so sánh tổng quan giữa statement và preparedStatement, CallableStatement

Statement PreparedStatement CallableStatement
Nó được sử dụng để thực hiện các truy vấn SQL bình thường. ( truy vấn 1 lần) Được sử dụng để thực hiện các truy vấn SQL được tham số hóa hoặc động. (Truy vấn lặp và thay đổi giá trị) Nó được sử dụng sử dụng để gọi stored procedures.
Nó được ưa thích khi một truy vấn SQL cụ thể chỉ được thực hiện một lần. Nó được ưa thích khi một truy vấn cụ thể được thực hiện nhiều lần. Nó được ưa thích khi gọi stored procedures và functions.
Bạn không thể truyền tham số cho truy vấn SQL bằng giao diện này. Bạn có thể chuyển các tham số cho truy vấn SQL trong thời gian chạy bằng giao diện này.
Giao diện này chủ yếu được sử dụng cho các câu lệnh DDL như CREATE, SELECT khôn có tham số, ALTER, DROP, v.v. Nó được sử dụng cho bất kỳ loại truy vấn SQL nào sẽ được thực hiện nhiều lần. Nó được sử dụng sử dụng để gọi stored procedures.
Hiệu suất của giao diện này rất thấp. Hiệu suất của giao diện này tốt hơn giao diện Statement (khi được sử dụng để thực hiện nhiều truy vấn giống nhau). Hiệu suất của giao diện này cao.

Hiểu thêm và sâu hơn

Statement không thể được sử dụng để lưu trữ / truy xuất hình ảnh và tệp trong cơ sở dữ liệu (nghĩa là sử dụng kiểu dữ liệu BLOB, CLOB) trong java JDBC.
PreparedStatement có thể được sử dụng cho
lưu trữ / truy xuất hình ảnh và
Lưu trữ / truy xuất tệp trong cơ sở dữ liệu
(tức là bằng cách sử dụng các kiểu dữ liệu BLOB, CLOB ) trong java JDBC.
java.sql.Statement không cung cấp phương thức addBatch (), nó chỉ cung cấp phương thức addBatch (String sql).
Do đó, cùng một truy vấn SQL không thể được thực thi lặp lại trong Statement trong java JDBC
java.sql.PreparedStatement mở rộng Statement và kế thừa tất cả các phương thức từ Statement và thêm vào đó phương thức addBatch () .
Phương thức addBatch () – thêm một tập các tham số vào  lệnh của đối tượng PreparedStatement trong java JDBC.
Do đó, cùng một truy vấn SQL có thể được thực thi lặp lại trong PreparedStatement trong java JDBC .
Hiệu suất của giao diện này rất thấp. Hiệu suất của giao diện này tốt hơn giao diện Statement (khi được sử dụng để thực hiện nhiều truy vấn giống nhau). Hiệu suất của giao diện này cao.



5. Chứng minh SQL Injection với Statement cực kỳ nguy hiểm và khuyến cáo không nên dùng nối chuỗi hay truyền tham số vào câu lệnh truy vấn

Chuẩn bị cơ sở dự liệu với table users:

Chứng minh và ví dụ về SQL Injection
Chứng minh và ví dụ về SQL Injection

Thông thường khi đăng nhập ta có form login để đăng nhập vào trang cá nhân hay trang quản trị viên nào đó, đòi hỏi các bạn cần nhập thông tin username cũng như password vào.

Form login
Form login

Chẳng hạn như một trường hợp tôi đã biết được username của người dùng hoặc thông thường với một table users ta luôn có tài khoản username có tên là admin.

Trường hợp tôi là môt người dùng cố tính truy cập vào một tài khoản khi chưa biết mật khẩu. Các bạn tin không nếu tôi có thể đăng nhập vào được nhỉ?

Tôi đoán rằng với câu lệnh kiểm tra đăng nhập vào đúng sai như sau:

String sql = “SELECT * FROM users WHERE username = ‘”+username + “‘ AND password = ‘”+password+”‘”;

câu lệnh truy vấn trên chúng ta đang nối chuỗi với các biến bằng cách sử dụng dấu + trong java như chúng ta đã biết.

Giải thích câu lệnh truy vấn trên như sau:

Câu lệnh truy vấn trên dùng đẻ kiểm tra đăng nhập vào thành công hay không. Với hai tham số: username và password là hai biến tương ứng hai giá trị khi người dùng nhập vào form.Có hai trường hợp xảy ra:

TH1:  Người dùng đăng nhập sai, nghĩa là password hoặc username sai, thì lúc này tất nhiên trả về 1 bảng tạm không có dòng dữ liệu nào. Khi các bạn viết phương thức trong java thì sẽ trả về một object User. Tương ứng trường hợp này user đang bằng null.

TH2:  Người dùng đăng nhập sai, nghĩa là password và username đúng, thì lúc này tất nhiên trả về 1 bảng tạm  có lớn hơn 1 dòng dữ liệu. Khi các bạn viết phương thức trong java thì sẽ trả về một object User. Tương ứng trường hợp này user đang khác bằng null.

Dựa vào câu lệnh truy vấn trên tôi có thể nhập vào thông tin của username và password như sau.

Test:username : adminpassword: password’ || ‘1

Như vậy câu lệnh SQL trên tương ứng như sau:

String sql = “SELECT * FROM users WHERE username = ‘admin’
AND password =
‘password’ || ‘1’ “;

Khi truy vấn và thực hiện câu lệnh SQL trên, kết quả trả về như sau:

Như các bạn thấy, mặc dù tôi không biết password của username là admin nhưng tôi có thể đoán được và có thể login thành công.

Đây được gọi là SQL Injection, SQL.

Định nghĩa: 

SQL injection là một kỹ thuật cho phép những kẻ tấn công lợi dụng lỗ hổng của việc kiểm tra dữ liệu đầu vào trong các ứng dụng web và các thông báo lỗi của hệ quản trị cơ sở dữ liệu trả về để inject (tiêm vào) và thi hành các câu lệnh SQL bất hợp pháp. SQL injection có thể cho phép những kẻ tấn công thực hiện các thao tác, delete, insert, update, v.v. trên cơ sở dữ liệu của ứng dụng, thậm chí là server mà ứng dụng đó đang chạy. SQL injection thường được biết đến như là một vật trung gian tấn công trên các ứng dụng web có dữ liệu được quản lý bằng các hệ quản trị cơ sở dữ liệu như SQL Server, MySQL, Oracle, DB2, Sysbase…

Như vậy, các bạn thấy tác hại và hạn chế khi sử dụng Statement chưa?

Statement không có tham số hóa ? nên muốn truyền vào tham số chúng ta cần thêm dấu + để nối với 1 biến nào đó. Từ đó hacker hay người dùng xấu có thể cố tình và  truy cập đăng nhập thành công.

Vậy PreparedStatement sinh ra để khắc phục nhược điểm trên, các bạn chỉ cần truyền vào câu lệnh SQL trên như sau:

String sql = “SELECT * FROM users WHERE username = ? AND password =  ?”;

Từ đó ta có thể set giá trị cho các biến được tham số hóa dấu ?.

PreparedStatement sẽ tự loại bỏ những ký tự đặc biệt và phù hợp với câu lệnh truy vấn của bạn, tránh xảy ra SQL Injection.


6. Chứng minh về hiệu suất của Statement và PreparedStatement 

Các bạn có thể review đoạn code bên dưới để kiểm chứng:

File : TestDemo.java

package test;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;

import model.bean.Category;
import utils.ConnectDBUtil;

public class TestDemo {
	private Connection conn;
	private Statement st;
	private ResultSet rs;
	private PreparedStatement pst;

	public static void main(String[] args) throws SQLException {
		TestDemo demo = new TestDemo();
		demo.getItemsWithPreparedStatement();
		demo.getItemsWithStatement();
		System.out.println("--------------------------");
		System.out.println("creating data...");
		ArrayList<Category> createData = demo.createData(100);
		System.out.println("Created data!");
		demo.insertItemsWithPreparedStatement(createData);
		demo.insertItemsWithStatement(createData);
	}

	public ArrayList<Category> getItemsWithStatement() {
		ArrayList<Category> list = new ArrayList<Category>();
		conn = ConnectDBUtil.getConnection();
		final String SQL = "SELECT * FROM danhmuctin ORDER BY ID_DanhMucTin DESC";
		// start
		long startTime = System.nanoTime();
		try {
			st = conn.createStatement();
			rs = st.executeQuery(SQL);
			while (rs.next()) {
				Category category = new Category(rs.getInt("ID_DanhMucTin"), rs.getString("TenDanhMucTin"));
				list.add(category);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			ConnectDBUtil.close(rs, st, conn);
		}
		long endTime = System.nanoTime();
		long resultTime = endTime - startTime;
		double seconds = (double) resultTime / 1_000_000_000.0;
		System.out.println("Select -->>Nano seconds Statement: " + resultTime);
		System.out.println("Select -->>seconds Statement: " + seconds);
		return list;
	}

	public ArrayList<Category> getItemsWithPreparedStatement() {
		ArrayList<Category> list = new ArrayList<Category>();
		conn = ConnectDBUtil.getConnection();
		final String SQL = "SELECT * FROM danhmuctin ORDER BY ID_DanhMucTin DESC";
		// start
		long startTime = System.nanoTime();
		try {
			pst = conn.prepareStatement(SQL);
			rs = pst.executeQuery();
			while (rs.next()) {
				Category category = new Category(rs.getInt("ID_DanhMucTin"), rs.getString("TenDanhMucTin"));
				list.add(category);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			ConnectDBUtil.close(rs, pst, conn);
		}
		long endTime = System.nanoTime();
		long resultTime = endTime - startTime;
		double seconds = (double) resultTime / 1_000_000_000.0;
		System.out.println("Select -->>Nano seconds PreparedStatement : " + resultTime);
		System.out.println("Select -->>seconds PreparedStatement: " + seconds);
		return list;
	}

	public void insertItemsWithPreparedStatement(ArrayList<Category> list) {
		conn = ConnectDBUtil.getConnection();
		final String SQL = "INSERT INTO cat(name) VALUES(?)";
		// start
		long startTime = System.nanoTime();
		try {
			pst = conn.prepareStatement(SQL);
			for (Category category : list) {
				pst.setString(1, category.getName());
				pst.executeUpdate();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			ConnectDBUtil.close(pst, conn);
		}
		long endTime = System.nanoTime();
		long resultTime = endTime - startTime;
		double seconds = (double) resultTime / 1_000_000_000.0;
		System.out.println("Insert -->>Nano seconds PreparedStatement : " + resultTime);
		System.out.println("Insert -->>seconds PreparedStatement: " + seconds);
	}

	public void insertItemsWithStatement(ArrayList<Category> list) {
		conn = ConnectDBUtil.getConnection();

		// start
		long startTime = System.nanoTime();
		try {
			st = conn.createStatement();
			for (Category category : list) {
				final String SQL = "INSERT INTO cat(name) VALUES('" + category.getName() + "')";
				st.executeUpdate(SQL);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			ConnectDBUtil.close(st, conn);
		}
		long endTime = System.nanoTime();
		long resultTime = endTime - startTime;
		double seconds = (double) resultTime / 1_000_000_000.0;
		System.out.println("Insert -->> Nano seconds Statement : " + resultTime);
		System.out.println("Insert -->> Seconds Statement: " + seconds);
	}

	public ArrayList<Category> createData(int n) {
		ArrayList<Category> list = new ArrayList<Category>();
		for (int i = 1; i <= n; i++) {
			Category category = new Category(0, "Category - " + i);
			list.add(category);
		}
		return list;
	}
}

File : Category.java

package model.bean;

public class Category {
	private int id;
	private String name;

	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 Category(int id, String name) {
		super();
		this.id = id;
		this.name = name;
	}

	public Category() {
	}
}

Bên dưới là kết quả:

Select -->>Nano seconds PreparedStatement : 9649164
Select -->>seconds PreparedStatement: 0.009649164
Select -->>Nano seconds Statement: 575073
Select -->>seconds Statement: 5.75073E-4
--------------------------
creating data...
Created data!
Insert -->>Nano seconds PreparedStatement : 388186614
Insert -->>seconds PreparedStatement: 0.388186614
Insert -->> Nano seconds Statement : 400520534
Insert -->> Seconds Statement: 0.400520534

Như vậy các bạn có thể thấy, đối với những trường hợp truy vấn với SELECT không có điều kiện WHERE hay những câu lệnh không có tham số và chỉ truy vấn một lần duy nhất mà không lặp thì nên sử dụng Statement, còn lại đối với những trường hợp lặp lại câu lệnh truy vấn và chỉ thay đổi giá trị, hoặc đối với những trường hợp có tham số thì nên sử dụng PreparedStatement.

7. Tổng kết

Đó là tất cả về sự khác biệt giữa Statement và PreparedStatement trong Java . Bạn có thể sử dụng Statement để thực thi các truy vấn sử dụng một lần, trong trường hợp sử dụng câu lệnh SQL đó nhiều lần chỉ thay đổi dữ liệu sau mỗi lần truy vấn thì điều đó không được khuyến khích.Sử dụng PreparedStatement, cách tiếp cận nhanh hơn và an toàn hơn để lấy dữ liệu từ cơ sở dữ liệu. Nếu bạn phải truyền tham số luôn sử dụng PreparedStatment, cũng như dùng câu lệnh SQL đó nhiều lần. Không bao giờ tạo truy vấn SQL động bằng cách nối chuỗi, điều đó không an toàn và dễ bị tấn công SQL Injection.

0 0 đánh giá
Đánh giá bài viết
Theo dõi
Thông báo của
guest
0 Góp ý
Phản hồi nội tuyến
Xem tất cả bình luận
x