ฐานข้อมูล (database) คือที่สำหรับเก็บข้อมูลที่เราใช้งานกับแอพพลิเคชั่น เช่น ฐานข้อมูลของแอพพลิเคชั่นจัดการร้านค้าก็จะประกอบด้วย รายการสินค้า รายชื่อลูกค้า รายการซื้อขายสินค้า เป็นต้น ในการออกแบบฐานข้อมูลที่ดีจะไม่นำข้อมูลที่ไม่เกี่ยวข้องกันจริงๆมาเก็บไว้ด้วยกัน เพื่อลดความซ้ำซ้อนของข้อมูลและเพิ่มประสิทธิภาพการใช้งานฐานข้อมูล ดังนั้นข้อมูลในฐานข้อมูลจะแบ่งออกเป็นกลุ่มของข้อมูลเรียกว่าตาราง (table) โดยข้อมูลแต่ละรายการ (record) ในตาราง จะประกอบด้วยข้อมูลย่อยๆเรียกว่าฟิลด์ (field) ซึ่งเป็นชนิดข้อมูลแบบต่างๆ เช่น ข้อความ (string) ตัวเลข (integer) วันที่ (date) เป็นต้น ตัวอย่างเช่น รายการสินค้า จะเก็บไว้ในตารางรายการสินค้า ข้อมูลสินค้าแต่ละชนิดจะประกอบด้วยฟิลด์ รหัสสินค้า ชื่อสินค้า ขนาดสินค้า ราคา เป็นต้น ถ้าเราต้องการดูโครงสร้างของฐานข้อมูลว่ามีตารางอะไรบ้างและแต่ละตารางประกอบด้วยด้วยฟิลด์อะไรบ้าง เราจะดูจาก data dictionary ของฐานข้อมูลซึ่งอาจจะอยู่ในรูปแบบเอกสารของผู้ที่ออกแบบฐานข้อหรือดูจากแอพพลิเคชั่นที่ใช้จัดการฐานข้อมูล
เนื่องจากแต่ละตารางเป็นข้อมูลที่ไม่เกี่ยวข้องกันในการใช้งานเราจะเอาข้อมูลเหล่านี้มาใช้ด้วยกันเพื่อให้เกิดเป็นข้อมูลที่เรานำไปใช้งานได้ เช่น เอาข้อมูลจากรายการสินค้าและรายชื่อลูกค้ามาใช้ด้วยกันเมื่อลูกค้าซื้อสินค้า โดยเพิ่มฟิลด์จำนวนที่ซื้อ และราคารวม กลายเป็นตารางรายการซื้อขายสินค้า
ฐานข้อมูลที่มีโครงสร้างและการใช้งานตามที่กล่าวมานี้เราเรียกว่าระบบฐานข้อมูลเชิงสัมพันธ์ (Relational Database) และระบบที่ใช้จัดการฐานข้อมูลเชิงสัมพันธ์เราเรียกว่า RDBMS (Relational Database Management System) ในการใช้งานระบบฐานข้อมูลเชิงสัมพันธ์จะมีภาษาที่เป็นมาตรฐานให้เราใช้ในการจัดการฐานข้อมูลเรียกว่า SQL (Structure Query Language) บางครั้งเราจึงเรียกระบบฐานข้อมูลเชิงสัมพันธ์ว่าฐานข้อมูลแบบ SQL (SQL Database) ตัวอย่างระบบจัดการฐานข้อมูลเชิงสัมพันธ์ เช่น Microsoft SQL Server, MySQL, PostgreSQL, SQLite, Oracel SQL Dabase เป็นต้น นอกจากฐานข้อมูลแบบ SQL แล้วในโลกนี้ยังมีฐานข้อมูลที่ไม่ได้มีโครงสร้างที่ชัดเจน โดยขึ้นอยู่กับข้อมูลที่เก็บเรียกว่าฐานข้อมูลแบบ No-SQL เช่น MongoDB, Hadoop เป็นต้น
ภาพของฐานข้อมูลในรูปแบบตารางและฟิลด์เป็นภาพจำลองเพื่อให้เราเข้าใจว่าข้อมูลมีโครงสร้งและความสัมพันธ์กันอย่างไร และจะเรียกใช้งานได้อย่างไร แต่ในความเป็นจริงข้อมูลต่างๆถูกเก็บเป็นไฟล์ซึ่งอาจจะมีไฟล์เดียวหรือหลายไฟล์ และโครงสร้างของข้อมูลในไฟล์เป็นอย่างไรขึ้นอยู่กับแต่ละแบรนด์ของระบบจัดการฐานข้อมูล
ในการเรียนรู้การเชื่อมต่อกับฐานข้อมูล เราจะใช้ SQLite ซึ่งเป็นระบบฐานข้อมูลที่ถูกออกแบบมาเพื่อฝังตัวไปกับแอพพลิเคชั่น SQLite เป็นไลบรารี่เพื่อให้โปรแกรมเรียกใช้เพื่อจัดการฐานข้อมูล มีโปรแกรมเชลล์ซึ่งเป็นโปรแกรมที่เราใช้จัดการฐานข้อมูลในแบบบรรทัดคำสั่ง (CLI :Command Liner Interface) ฐานข้อมูลของ SQLite จะเก็บไว้ในไฟล์เพียงไฟล์เดียวและอยู่บนเครื่องเดียวกันกับแอพพลิเคชั่นที่ใช้งาน
ในการเชื่อมต่อกับฐานข้อมูลเราจะใช้ JDBC (Java Database Conectivity) โดยภาษาจาวาจะจัดเตรียม JDBC API ซึ่งเป็นคลาสและคลาสแบบอินเตอร์เฟสที่จะต้องใช้คู่กับ JDBC Driver เพื่อติดต่อกับฐานข้อมูล โดย JDBC Driver นั้นเป็นคลาสที่แบรนด์ฐานข้อมูลต่างๆสร้างขึ้นมาโดยอ้างอิง JDBC API ไฟล์ JDBC Driver จะเป็นไฟล์ที่มีนามสกุล jar โดยในการใช้งานเราจะโหลดไฟล์ดังกล่าวไว้ในไลบรารี่ (class path) ของโครงงานที่เราเขียนโปรแกรม
ใน JDBC API ประกอบด้วยแพคเกจ java.sql ซึ่งเป็นคลาสหลักของ JDBC (Core JDBC) และ javax.sql สำหรับการติดต่อกับ Database Server
การเพิ่ม SQLite ในไลบรารี่ของโครงงาน
ดาวน์โหลด SQLite JDBC Driver จาก https://github.com/xerial/sqlite-jdbc เปิดโครงงานที่ต้องการ ไปที่เมนู File > ProjectStructure … ในหน้าต่าง Project Structure เลือก Libraries และเลือก + จากนั้นเลือก Java
จากหน้าต่าง Select Library Files เลือกไฟล์ sqlite-jdbcxxxx.jar ที่ดาวน์โหลดมาและเลือก OK
ในหน้าต่าง Choose Modules ให้เลือกชื่อโครงงานของเรา และเลือก OK
เมื่อกลับมาที่หน้าต่าง Project Structure เลือก OK
จะเห็นว่ามีไลบรารี่ของ sqlite อยู่ในโครงงานของเราแล้ว
การสร้างฐานข้อมูล
ในการเชื่อมต่อฐานข้อมูลด้วย SQLite หากยังไม่มีไฟล์ฐานข้อมูลอยู่ ไฟล์ฐานข้อมูลจะถูกสร้างขึ้นมาโดยอัตโนมัติ ตัวอย่างด้านล่างเป็นการสร้างการเชื่อมต่อกับฐานข้อมูล โดยเมื่อสั่ง run โปรแกรมจะเห็นว่ามีไฟล์ฐานข้อมูลถูกสร้างขึ้นมาในหน้าต่างโครงงาน
จากตัวอย่างด้านบนในบรรทัดที่ 9 เราเชื่อมต่อกับฐานข้อมูลโดยใช้เมธอด DriverManager.getConnection (“connection string”) โดย connection string คือที่อยู่ของฐานข้อมูลในรูปแบบที่ขึ้นต้นด้วย jdbc: ตามด้วยแบรนด์ของฐานข้อมูล ที่อยู่ของฐานข้อมูลและอาจจะต้องระบุชื่อผู้ใช้และรหัสผ่านที่มีสิทธเข้าใช้ฐานข้อมูล เช่น MySQL จะมีรูปแบบ jdbc:mysql:/[serverName]:[port]
/[file] ของ Microsoft SQL Server จะมีรูปแบบ jdbc:sqlserver://[serverName
[\instanceName][:portNumber]][;property=value[;property=value]] ถ้าเราดูใน IntelliJ IDEA จะเห็นว่ามีหลายแบบตามที่กล่าวมา
สำหรับการเชื่อมต่อกับฐานข้อมูลของระบบขนาดใหญ่ (enterprise application) ซึ่งต้องใช้ความสามารถพิเศษของระบบฐานข้อมูล เช่น ใช้งาน connection pooling หรือ distributed transaction เราจะใช้เมธอด DataSource.getConnection() ซึ่งจะได้ผลลัพธ์เป็นออบเจกต์ Connection เช่นเดียวกัน
สำหรับการเชื่อมต่อกับ sqlite เราใช้ jdbc:sqlite:[path][file] ซึ่งในที่นี้เราเก็บไฟล์ไว้ที่เดียวกับโครงงานจึงไม่ได้ระบุพาธใดๆ ผลลัพธ์ที่ได้คือออบเจกต์ชนิด Connection ซึ่งเป็นออบเจกต์ที่ทำหน้าที่เชื่อมต่อกับฐานข้อมูล
เมื่อเราสร้างไฟล์ฐานข้อมูลขึ้นมาแล้ว เราสร้างตารางเพื่อเก็บข้อมูลได้โดยใช้ออบเจกต์ Statement ซึ่งเป็นออบเจกต์ที่จะรับคำสั่งในภาษา SQL ไปจัดการฐานข้อมูล โดยจะทำงานควบคู่กับออบเจกต์ Connectionเราสร้างออบเกจต์ Statement โดยใช้เมธอด createStatement() ของออบเจกต์ Connection
จากตัวอย่างด้านล่าง ในบรรทัดที่ 10 เป็นการสร้างออบเจกต์ Connection ขึ้นมาเพื่อเชื่อมต่อกับฐานข้อมูล บรรทัดที่ 11 เป็นการสร้างออบเจกต์ Statement เพื่อใช้ประมวลผลภาษา SQL และบรรทัดที่ 12 เราสร้างตารางโดยใช้คำสั่งในภาษา SQL คือ “CREATE TABLE IF NOT EXISTS contacts(name TEXT, phone INTEGER, email TEXT)” หมายถึง สร้างตารางชื่อ contacts ซึ่งมีฟิลด์ ชื่อ เบอร์โทร และอีเมล์ เราระบุ IF NOT EXIST เพื่อบอกว่าให้สร้างตารางถ้ายังไม่มีอยู่ ซึ่งตารางจะถูกสร้างขึ้นมาในกรณีที่เป็นการใช้งานโปรแกรมครั้งแรก
การเพิ่ม แก้ไข ลบ และอ่านข้อมูล
เมื่อเราสร้างตารางได้แล้ว เราสามารถเพิ่มข้อมูล แก้ไขข้อมูล ลบข้อมูล และอ่านข้อมูลจากตารางได้โดยใช้ภาษา SQL ผ่านเมธอด Statement.execute() ได้เช่นเดียวกับการสร้างตาราง จากตัวอย่างด้านล่างในบรรทัดที่ 13 เราเพิ่มข้อมูลด้วยคำสั่ง “INSERT INTO contacts (name, phone, email) VALUES (‘Somchai’, 0819999999, ‘somchai@email.com‘)” บรรทัดที่ 15 อ่านข้อมูลจากตารางด้วยคำสั่ง “SELECT * FROM contacts” ซึ่งในบรรทัดที่ 16 เราต้องสร้างออบเจกต์ ResultSet ขึ้นมาเพื่อรับข้อมูลที่อ่านมาจากตารางด้วยเมธอด Statement.getResultSet() บรรทัดที่ 17 – 21 เป็นการวนรอบเพื่อแสดงข้อมูลโดยใช้เมธอด next() ของออบเจกต์ ResultSet ซึ่งจะคืนค่าเป็น false ถ้าหมดข้อมูลแล้ว บรรทัดที่ 23 – 32 เป็นการเพิ่มข้อมูลและพิมพ์ข้อมูลออกมาดูเช่นเดียวกัน สังเกตุว่าในบรรทัดที่ 27 เราสามารถนำออบเจกต์ ResultSet มาใช้ใหม่ได้เพียงเรียกเมธอด Statement.getResultSet() เพื่อรับค่าใหม่เข้ามา
ในบรรทัดที่ 35 เป็นการแก้ไขข้อมูลด้วยคำสั่ง “UPDATE contacts SET phone = 0812222222 WHERE name = ‘Somchai’ ” และในบรรทัดที่ 36 – 42 ก็เป็นการวนรอบเพื่อแสดงข้อมูลเช่นกัน
ในบรรทัดที่ 45 เป็นการลบข้อมูลด้วยคำสั่ง “DELETE FROM contacts WHERE name = ‘Somchai’ ” และในบรรทัดที่ 46 – 52 ก็เป็นการวนรอบเพื่อแสดงข้อมูลเช่นกัน
และเมื่อเราใช้งานเรียบร้อบแล้ว เราต้องคืนทรัพยากรให้กับระบบโดยการปิดออบเจกต์ ResultSet, Statement และ Connection ด้วยเมธอด close() ตามตัวอย่างในบรรทัดที่ 54 – 56 หรือเราอาจจะใช้บล๊อก try-with-resource เพื่อให้แน่ใจว่าทรัพยากรจะถูกคืนให้ระบบทุกครั้งหลังใช้งานเสร็จ
ในการเพิ่ม แก้ไข ลบ ข้อมูลจากฐานข้อมูล เราจำเป็นต้องยืนยัน (commit) ว่าต้องการทำอย่างนั้นจริง ไม่เช่นนั้นเมื่อเราปิดการเชื่อมต่อกับฐานข้อมูล ข้อมูลเหล่านั้นจะหายไป ในโปรแกรมของเราจะไม่เห็นส่วนที่ต้องยืนยันเพราะออบเจกต์ Connection ที่สร้างขึ้นมาจะมีค่าตั้งต้นของฟิลด์ auto commit เป็น true ดังนั้นออบเจกต์ Statement จะ commit ให้เราอัตโนมัติทันที่ที่ดำเนินการตามคำสั่งเสร็จ
หากเราต้องการอ่านข้อมูลเพียงอย่างเดียว เราสามารถรวบกระชับคำสั่งการสร้างออบเจกต์ ResultSet และการอ่านข้อมูลไวด้วยกันได้โดยใช้เมธอด Statement.executeQuery ดังตัวอย่าง ResultSet results = statement.executeQuery(“SELECT * FROM contacts”);
สำหรับข้อมูลเกี่ยวกับฐานข้อมูล เช่น มีตารางอะไรบ้าง มีฟิลด์อะไรบ้าง รองรับชนิดข้อมูลอะไรบ้าง เราจำเป็นต้องรู้ก่อนที่จะพัฒนาโปรแกรม สำหรับภาษา SQL สามารถศึกษาได้ที่ https://www.w3schools.com/sql/
การป้องกัน SQL Injection เบื้องต้น
การใช้งานฐานข้อมูลจะเป็นการรองรับการใช้งานจากผู้ใช้ ดังนั้นเราจะไม่ได้ให้ผู้ใช้งานส่งคำสั่งภาษา SQL ให้โปรแกรมเราโดยตรง แต่จะเป็นการรับค่าจากผู้ใช้งานเพื่อดำเนินการต่างๆ จากตัวอย่างด้านล่างเราสร้างคลาสเพื่อติดต่อกับฐานข้อมูลและมีเมธอดเพื่อแสดงข้อมูล โดยคลาส Datasource จะประกอบด้วยเมธอด connectDB() เพื่อเชื่อมต่อกับฐานข้อมูล เมธอด closeDB() เพื่อคืนทรัพยากรเมื่อเลิกใช้งาน เมธอด printSelectedInfo() เพื่อดูข้อมูลเฉพาะรายการที่ต้องการ และเมธอด printAllInfo() เพื่อแสดงข้อมูลทั้งหมด
ส่วนคลาส Main จะเรียกใช้ออบเจกต์ Datasource เพื่อเชื่อมต่อกับฐานข้อมูลเชื่อมต่อกับฐานข้อมูล แสดงเมนูให้ผู้ใช้งานเลือกดูข้อมูลทั้งหมดหรือดูเฉพาะบางรายการ และรับค่าผ่านออบเจกต์ Scanner
จากตัวอย่าง เมื่อผู้ใช้งานเลือกข้อ 2 จะต้องระบุชื่อที่ต้องการดูข้อมูล ซึ่งชื่อจะถูกนำไปรวมกับคำสั่ง SQLที่เตรียมไว้เพื่อดำเนินการต่อไป ซึ่งการยอมให้ระบุข้อมูลเพื่อนำไปใช้เป็นส่วนหนึ่งของคำสั่ง SQL เป็นการเปิดช่องให้ผู้ไม่หวังดีส่งคำสั่ง SQL เพื่อวัตถุประสงค์อื่นได้เรียกว่าการทำ SQL Injection เช่น หากผู้ใช้งานระบุค่าว่า Varaporn” or 1=1 or ” จะทำให้โปรแกรมแสดงข้อมูลทั้งหมดแทนที่จะเป็นข้อมูลของ Varaporn เท่านั้น ประโยค ” or 1=1 or ” เป็นข้อความคลาสสิคของการทำ SQL Injection เพื่อแสดงข้อมูลออกมาทั้งฐานข้อมูล
เราสามารถป้องกันการทำ SQL Injection ในเบื้องต้นได้โดยเปลี่ยนตัวดำเนินการคำสั่ง SQL มาเป็นคลาส PreparedStatement แทนที่จะใช้ Statement.execute() โดยในบรรทัดที่ 7 เราเพิ่มตัวแปรเพื่อเก็บออบเจกต์ PreparedStatement
ในบรรทัดที่ 29 เราย้ายคำสั่ง SQL มาเก็บในตัวแปรและกำหนดใช้ wild card ? ตรงตำแหน่งที่ต้องใช้ค่าที่รับเข้ามา ซึ่ง ? หมายถึงต้องการเพียงข้อความเดียว บรรทัดที่ 30 สร้างออบเจกต์ PreparedStatement โดยใช้เมธอด Connection.prepareStatement() ซึ่งจะรับคำสั่ง SQL เป็นพารามิเตอร์เพื่อนำไปคอมไพล์ก่อนใช้งาน บรรทัดที่ 31 ใช้เมธอด PreparedStatement.setString เพื่อรับค่ามาใช้กับคำสั่ง SQL ที่คอมไพล์ไว้ และบรรทัดที่ 32 เป็นการดำเนินการตามคำสั่ง SQL จะเห็นว่าแทนที่จะแสดงข้อมูลทั้งหมดจะกลายเป็นไม่แสดงอะไรเลยเพราะการใช้ PreparedStatement จะมองสิ่งที่ป้อนเข้ามาเป็นข้อความเดียวคือ “Varaporn or 1-1 or” ซึ่งไม่ตรงกับชื่อใดๆในฐานข้อมูล
การบันทึกรายการแบบ Transaction
รายการแบบ transaction หมายถึงรายการที่ประกอบด้วยคำสั่งมากกว่าหนึ่งคำสั่งประกอบกันเพื่อให้เกิดการทำรายการที่สมบูรณ์ เช่น การโอนเงินจากบัญชี A ไปบัญชี B จะเกิดคำสั่งในลดยอดคงเหลือจากบัญชี A และคำสั่งเพิ่มยอดคงเหลือในบัญชี B ดังนั้นการ commit เพื่อบันทึกข้อมูลลงในฐานข้อมูลจะต้องแน่ใจว่าทุกคำสั่งสำเร็จ
ข้อกำหนดของรายการแบบ transaction ตามมาตรฐาน ACID-compliant คือ 1).atomicity : transaction ต้องเป็น atomicity หมายถึง ชุดของคำสั่งที่เกี่ยวข้องกันต้องถูก commit ทั้งหมดหรือไม่ถูก commit ทั้งหมด 2). consistency : transaction จะต้องถูกต้องตรงกันเสมอ หมายถึง transaction จะต้องทำให้ข้อมูลในฐานข้อมูลถูกต้องอยู่เสมอทั้งก่อนและหลัง commit 3). isolation : transaction จะต้องไม่เกี่ยวข้องกับสิ่งอื่นใด หมายถึง จนกว่าจะ commit สมบูรณ์ ข้อมูล transaction จะไม่ถูกเข้าถึงและไม่เกี่ยวข้องกับ transaction อื่นๆ 4). durability เมื่อ Transaction ถูก commit เรียบร้อยแล้ว ข้อมูลจะต้องคงอยู่ในฐานข้อมูลแม้ว่าโปรแกรมหรือฐานข้อมูลจะล่ม แต่เมื่อกลับมาใช้งานได้ ข้อมูลจะต้องยังคงอยู่อย่างถูกต้อง
ดังนั้นการจัดการกับรายการแบบ transaction เราจะต้องปิดการ commit อัตโนมัติ จากนั้นจึงดำเนินการและตรวจสอบแต่ละคำสั่งว่าสำเร็จแล้วจึงสั่ง commit หรือสั่ง rollback ในกรณีมีคำสั่งที่ดำเนินการไม่สำเร็จ