Statement vs PreparedStatement vs Batch, Security and Performance
Jika kita membuat
aplikasi yang memanipulasi database dengan plain JDBC, Statement dan
PreparedStatement object mempunyai peranan yang penting. Keduanya
sama-sama dapat digunakan untuk mengirim perintah query ke database.
Sekarang, kita akan coba melihat apa yang membuat mereka berbeda.
Note: Source code untuk class DBManager bisa didapat disini.
1. Security attack: SQL Injection
Yang pertama, dilihat dari sudut pandang
keamanan, khususnya terhadap serangan yang disebut SQL injection.
Penyerang biasanya mencoba untuk menembus sistem dengan cara merusak SQL
query yang kita buat. Triknya adalah dengan menggunakan susunan
karakter, yang bila digabungkan dengan query sebenarnya akan
menghasilkan nilai yang berbeda.
Berikut adalah tabel yang akan kita gunakan untuk percobaan.
1
2
3
4
5
6
| CREATE TABLE `myblog`.`user_security` ( `username` VARCHAR (50) NOT NULL DEFAULT '' , `pswd` VARCHAR (255) NOT NULL DEFAULT '' , PRIMARY KEY (`username`) ) ENGINE = InnoDB; |
Dengan kode berikut, kita akan mencoba untuk menjalankan test menggunakan Statement dan PreparedStatement dengan input yang sama, yaitu:
username: aaa
pswd: bbb’ OR ’1′=’1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| package suhearie.blog.sql_injection; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Statement; import suhearie.blog.jdbc.DBManager; public class TesSqlInjection { private DBManager manager; private Connection conn; public TesSqlInjection() throws Exception { manager = new DBManager(); conn = manager.getConnection(); } private void testStatement(String user, String pswd) throws Exception { System.out.println( "Test menggunakan Statement" ); String sql = "SELECT * FROM user_security WHERE username = '" +user+ "' AND pswd = '" +pswd+ "'" ; Statement statement = conn.createStatement(); ResultSet rs = statement.executeQuery(sql); if (rs.next()) { System.out.println( "User: " +user+ ", Pswd:" +pswd+ " BENAR, login valid" ); } else { System.out.println( "User: " +user+ ", Pswd:" +pswd+ " SALAH, login invalid" ); } } private void testPreparedStatement(String user, String pswd) throws Exception { System.out.println( "Test menggunakan PreparedStatement" ); String sql = "SELECT * FROM user_security WHERE username = ? AND pswd = ?" ; PreparedStatement preparedStatement = conn.prepareStatement(sql); preparedStatement.setString( 1 , user); preparedStatement.setString( 2 , pswd); ResultSet rs = preparedStatement.executeQuery(); if (rs.next()) { System.out.println( "User: " +user+ ", Pswd:" +pswd+ " BENAR, login valid" ); } else { System.out.println( "User: " +user+ ", Pswd:" +pswd+ " SALAH, login invalid" ); } } public static void main(String[] args) throws Exception { TesSqlInjection test = new TesSqlInjection(); String user = "aaa" ; String pswd = "bbb' OR '1'='1" ; test.testStatement(user, pswd); test.testPreparedStatement(user, pswd); } } |
Test menggunakan Statement
User: aaa, Pswd:bbb’ OR ’1′=’1 BENAR, login valid
Test menggunakan PreparedStatement
User: aaa, Pswd:bbb’ OR ’1′=’1 SALAH, login invalid
Mengapa hasil dari test Statement bisa demikian ?
Jawabannya adalah karena bila query awal dan input user digabung, query akan menjadi:
1
| SELECT * FROM user_security WHERE username = 'aaa' AND pswd = 'bbb' OR '1' = '1' |
Dari test tersebut, jelas bahwa Statement sangat rentan terhadap SQL injection, karena input dari user akan di menjadi bagian dari query. Sementara jika kita menggunakan PreparedStatement, input dari user akan menjadi parameter saja, sehingga tidak mempengaruhi hasil query.
2. Performance
Disini kita akan menguji performance dari 3 metode tersebut untuk melakukan manipulasi data di database. Saya sudah melakukan percobaan untuk melakukan operasi insert pada tabel sederhana berikut:
1
2
3
4
| CREATE TABLE `myblog`.`st_vs_pst` ( `COL1` int (10) unsigned NOT NULL default '0' , `COL2` int (10) unsigned NOT NULL default '0' ) ENGINE=InnoDB DEFAULT CHARSET=latin1; |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
| package suhearie.blog.stmt_vs_pstmt; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.Statement; import suhearie.blog.jdbc.DBManager; public class TestPerformance { private DBManager manager; private Connection conn; private static final String TABLE_NAME = "ST_VS_PST" ; private static final int TOTAL = 1000 ; private static final int INTERVAL = 200 ; public TestPerformance() throws Exception { manager = new DBManager(); conn = manager.getConnection(); } private void truncateTable() throws Exception { System.out.println( "Truncating table" ); Statement statement = conn.createStatement(); statement.executeUpdate( "TRUNCATE TABLE " +TABLE_NAME); } private void testStatement( boolean turnOffAutoCommit) throws Exception { System.out.println(); System.out.println( "Starting testStatement, turnOffAutoCommit:" +turnOffAutoCommit); truncateTable(); long start = System.currentTimeMillis(); if (turnOffAutoCommit) { conn.setAutoCommit( false ); } Statement statement = conn.createStatement(); for ( int i= 0 ; i<=TOTAL; i++) { statement.executeUpdate( "INSERT INTO " +TABLE_NAME+ " (COL1,COL2) VALUES (" +i+ "," +i* 2 + ")" ); if (i%INTERVAL == 0 ) { System.out.print( "Row:" +i+ "; " ); } } if (turnOffAutoCommit) { conn.commit(); conn.setAutoCommit( true ); } long diff = System.currentTimeMillis() - start; System.out.println( "testStatement finish: " +diff+ " ms" ); } private void testPreparedStatement( boolean turnOffAutoCommit) throws Exception { System.out.println(); System.out.println( "Starting testPreparedStatement, turnOffAutoCommit:" +turnOffAutoCommit); truncateTable(); long start = System.currentTimeMillis(); if (turnOffAutoCommit) { conn.setAutoCommit( false ); } PreparedStatement preparedStatement = conn.prepareStatement( "INSERT INTO " +TABLE_NAME+ " (COL1,COL2) VALUES (?,?)" ); for ( int i= 0 ; i<=TOTAL; i++) { preparedStatement.setInt( 1 , i); preparedStatement.setInt( 2 , i* 2 ); preparedStatement.executeUpdate(); if (i%INTERVAL == 0 ) { System.out.print( "Row:" +i+ "; " ); } } if (turnOffAutoCommit) { conn.commit(); conn.setAutoCommit( true ); } long diff = System.currentTimeMillis() - start; System.out.println( "testPreparedStatement finish: " +diff+ " ms" ); } private void testBatch( boolean turnOffAutoCommit) throws Exception { System.out.println(); System.out.println( "Starting testBatch, turnOffAutoCommit:" +turnOffAutoCommit); truncateTable(); long start = System.currentTimeMillis(); if (turnOffAutoCommit) { conn.setAutoCommit( false ); } PreparedStatement preparedStatement = conn.prepareStatement( "INSERT INTO " +TABLE_NAME+ " (COL1,COL2) VALUES (?,?)" ); for ( int i= 0 ; i<=TOTAL; i++) { preparedStatement.setInt( 1 , i); preparedStatement.setInt( 2 , i* 2 ); preparedStatement.addBatch(); if (i%INTERVAL == 0 ) { System.out.print( "Row:" +i+ "; " ); preparedStatement.executeBatch(); } } if (turnOffAutoCommit) { conn.commit(); conn.setAutoCommit( true ); } long diff = System.currentTimeMillis() - start; System.out.println( "testBatch finish: " +diff+ " ms" ); } public static void main(String[] args) throws Exception { TestPerformance test = new TestPerformance(); test.testStatement( true ); test.testStatement( false ); test.testPreparedStatement( true ); test.testPreparedStatement( false ); test.testBatch( true ); test.testBatch( false ); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| Starting testStatement, turnOffAutoCommit: true Truncating table Row: 0 ; Row: 200 ; Row: 400 ; Row: 600 ; Row: 800 ; Row: 1000 ; testStatement finish: 297 ms Starting testStatement, turnOffAutoCommit: false Truncating table Row: 0 ; Row: 200 ; Row: 400 ; Row: 600 ; Row: 800 ; Row: 1000 ; testStatement finish: 24203 ms Starting testPreparedStatement, turnOffAutoCommit: true Truncating table Row: 0 ; Row: 200 ; Row: 400 ; Row: 600 ; Row: 800 ; Row: 1000 ; testPreparedStatement finish: 250 ms Starting testPreparedStatement, turnOffAutoCommit: false Truncating table Row: 0 ; Row: 200 ; Row: 400 ; Row: 600 ; Row: 800 ; Row: 1000 ; testPreparedStatement finish: 23250 ms Starting testBatch, turnOffAutoCommit: true Truncating table Row: 0 ; Row: 200 ; Row: 400 ; Row: 600 ; Row: 800 ; Row: 1000 ; testBatch finish: 219 ms Starting testBatch, turnOffAutoCommit: false Truncating table Row: 0 ; Row: 200 ; Row: 400 ; Row: 600 ; Row: 800 ; Row: 1000 ; testBatch finish: 23297 ms |
Dari output yang dihasilkan, tampak bahwa performance Batch > PreparedStatement > Statement. Alasannya:
1. Statement
Setiap SQL statement yang dieksekusi dikirim secara utuh ke database.
Setiap SQL statement yang dieksekusi dikirim secara utuh ke database.
2. PreparedStatement
SQL query dikirim hanya satu kali, selanjutnya yang dikirim ke database hanya parameted querynya saja.
SQL query dikirim hanya satu kali, selanjutnya yang dikirim ke database hanya parameted querynya saja.
3. PreparedStatement Batch:
Seperti halnya PreparedStatement, tapi kita bisa mengakumulasi parameter-parameter dengan method addBatch() kemudian mengirim kumpulan parameter itu sekaligus dengan method executeBatch().
Seperti halnya PreparedStatement, tapi kita bisa mengakumulasi parameter-parameter dengan method addBatch() kemudian mengirim kumpulan parameter itu sekaligus dengan method executeBatch().
Tapi perbedaan paling signifikan yang
memeberikan perbedaan performance paling besar didapat ketika kita
mematikan option autocommit di database. Dari test di atas, kecepatannya bisa mencapai 100 kali lipat !!
Mengapa bisa begitu? Sekedar
analisa/asumsi logika sederhana, jika kita menggunakan fitur autocommit,
setiap kita melakukan insert satu baris, maka database akan langsung
melakukan perubahan permanen pada tabel. Sebaliknya, jika database tidak
melakukan autocommit, maka kita bisa melakukan ribuan operasi insert,
dan mungkin database akan memprosesnya di memory/buffer. Kemudian pada
saat kita meng-commit perubahan, database akan melakukan proses tersebut
untuk ribuan bari sekaligus.
Kesimpulannya, jika Anda mau melakukan
insert atau update dalam jumlah yang besar, pilihan terbaik adalah
menggunakan PreparedStatement Batch, dan menonaktifkan option
autocommit.
Posting Komentar