DAO 模式
DAO (DataAccessobjects) 數據存取對象是指位於業務邏輯和持久化數據之間,實現對持久化數據的訪問的工作模式。
Java 和 python 進行通信
剛開始看到這個定義我一臉懵,所以我不會直接去解釋 DAO 模式是個什么玩意,這里會通過一個網絡互聯的例子做類比。
在網絡剛剛被搞出來的年代,通常只有同一個廠家生產的設備才能彼此通信,不同的廠家的設備不能兼容。這是因為沒有統一的標准去要求不同的廠家按照相同的方式進行通信,所以不同的廠家都閉門造車,使得大型網絡的搭建變得困難。為了解決這個問題,后來就產生出參考模型的概念。參考模型是描述如何完成通信的概念模型,它指出了完成高效通信所需要的全部步驟,並將這些步驟划分為稱之為“層”的邏輯組。
分層最大的優點是為上層隱藏下層的細節,即對於開發者來說,如果他們要開發或實現某一層的協議,則他們只需要考慮這一層的功能即可。無論是哪個廠家生產的設備,還是用什么技術開發的程序,只要做出來的東西具備其實現的協議的所有要求就行。其它層都無需考慮,因為其它層的功能有其它層的協議來完成,上層只需要調用下層的接口即可。
我們運行一對客戶機——服務器模型的應用,客戶端使用 Java 實現,代碼如下:
import java.net.*;
import java.io.*;
public class GreetingClient
{
@SuppressWarnings("deprecation")
public static void main(String [] args)
{
String serverName = "";
int port = 15000;
try
{
Socket client = new Socket(serverName, port); //創建套接字對象
OutputStream outToServer = client.getOutputStream(); //返回此套接字的輸出流
DataOutputStream out = new DataOutputStream(outToServer); //創建數據輸出流
out.writeUTF("Java"); //向輸出流寫入數據
InputStream inFromServer = client.getInputStream(); //返回此套接字的輸入流
DataInputStream in = new DataInputStream(inFromServer);
System.out.println("PythonServer:" + in.readLine()); //回顯服務器的響應
client.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
服務器則使用 Python 實現,代碼如下:
from socket import *
serverPort = 15000
serverSocket = socket(AF_INET,SOCK_STREAM)
serverSocket.bind(('',serverPort))
serverSocket.listen(1) #監聽接口,准備接受 TCP 連接請求
print("准備就緒,可以接收分組!")
while True:
connectionSocket,addr = serverSocket.accept() #接受 TCP 連接請求
sentence = connectionSocket.recv(1024).decode() #綁定連接套接字
capitalizedSentence = "Hello," + sentence +"! My name is Python.\n"
connectionSocket.send(capitalizedSentence.encode()) #向 JavaClient 發送數據
connectionSocket.close()
同時運行這 2 個程序,發現它們是可以進行通信的,即使是 2 門不同的語言編寫的程序。這是因為無論是 Java 的 Socket 類還是 Python 的 Socket 類,它們的工作方式都是調用運輸層實現 TCP 的程序接口,因為有參考模型和協議的約束,二者采取的行動和產生的數據分組都是相同的。
此時對於應用層來說,應用層認為這是對等層之間的通信,通過的是運輸層的可靠數據傳輸信道。同樣應用層對下層的細節一無所知,因為那是由其他層的協議和實現協議的程序負責的。
程序與數據庫間的“通信”
我所理解的 DAO 模型和網絡互聯模型的工作原理是很相似的,只是 DAO 模型的通信目標是數據庫。如果不使用 DAO 模型,則需要操作數據庫的方法很可能會和其他的方法雜糅在一起,無論是維護代碼還是其他類調用這部分方法,都是一件痛苦的事情。
而且這不是最大的問題,要命的是如果我數據庫因為某種原因重建了,或者是我換了一種數據庫,那么這個類的大量方法將被直接廢棄!這個時候我們別無選擇,只能重新寫一個類了,萬一原來的類代碼耦合度過高,將約等於從頭來過。
此時使用 DAO 模型就完全不同了,簡單地來說,我們提前設計好程序可能對數據庫采取的操作,然后將這些動作定義為一個“協議”。通過這種方式,我們將數據處理的模塊和對數據庫進行操作的模塊進行分離,數據處理模塊只負責發起數據庫連接和接受數據,而對數據庫的具體的操作交給數據庫操作的模塊負責。
感覺和“活字印刷術”有異曲同工之妙,因為不同的數據庫操作模塊訪問數據庫的接口是一樣的,因此這 2 個模塊可以做到隔離。當我數據庫需要大改或者替換時,對其他模塊的正常工作沒有任何影響,只需要把對應的數據庫操作模塊替換掉就行。需要調用數據庫的其他方法對此事毫無感知,因為它管調用接口就行了。
DAO 模式的優勢
首先 DAO 模式隔離了數據訪問代碼和業務邏輯代碼。業務邏輯代碼直接調用 DAO 方法即可,完全感覺不到數據庫表的存在。分工明確,數據訪問層代碼變化不影響業務邏輯代碼,這符合單一職能原則,降低了藕合性,提高了可復用性。
第二 DAO 模式隔離了不同數據庫實現。采用面向接口編程,如果底層數據庫變化,如由 MySQL 變成 Oracle 只要增加 DAO 接口的新實現類即可,原有 MySQ 實現不用修改。這符合 "開-閉" 原則。該原則降低了代碼的藕合性,提高了代碼擴展性和系統的可移植性。——菜鳥教程-Java DAO 模式
個人理解的 DAO 模式優勢如下:
- 提高代碼的復用性:通過將一系列相似的操作提煉並設計出一個接口,使得實現接口的類的特征更為鮮明,使得該類能更靈活地嵌入其他代碼中發揮作用;
- 利於分工合作:通過將某一操作的具體實現隔離,可以使編寫不同部分的程序員專注於自己該做的事情,通過調用對應的接口能更好地進行合作;
- 組件的替換更為方便:由於操作的具體實現被隔離開來,當該操作的具體實現發生較大改動時,對其他調用該操作的部分毫無影響。無需做大的變動,只需要修改具體實現的部分即可;
- 提高代碼的拓展性:通過 DAO 模式可以向其他部分隱藏細節,這就使得隔離出的部分也能夠添加更多的組件來提供更好的服務;
某種程度上說,通過 DAO 模式也能提高程序的安全性。例如名震江湖的 SQL 注入的重要防御手段,就是在連接數據庫是盡可能使用更多的用戶,這些用戶往往具有單一的權限。通過有限權限的用戶,可以防止 SQL 惡意注入取得過多的權限而造成數據庫被攻擊。在實際需要連接數據庫時,可以根據操作的不同使用不同權限的用戶進行連接,例如專門設置一個用戶用於查找操作。這些細節對於調用這個操作的代碼一無所知,在其他代碼看來只是從數據庫獲取到了數據而已,而不知道我們為此設置了很多的用戶用於連接。
實例解析
通過看一個實例對 DAO 模式進行系統的理解。
Student 類
例如我現在有個 Student 類,接下來我將基於該類實現一個簡易的學生管理小程序。
package stumanagement;
public class Student {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student [name=" + name + "]";
}
}
StudentDao 接口
StudentDao 是一個接口類,里面沒有具體的代碼,而是羅列出了一些列未具體實現的方法。通過學習我們知道接口用於描述類應該做什么,而不是指定類具體要怎么實現,一個類中可以實現多個接口。在有些情況下,我們的需求符合這些接口的描述,就可以使用實現這個接口的類的對象。
package stumanagement;
public interface StudentDao {
public boolean addStudent(Student student); //增加學生
public Student getStuByName(String name); //查詢學生
public void diplayAllStudents(); //顯示所有學生
}
當我實現學生管理時,這些操作都會或多或少地使用,我們這里等於把他們歸納出來。無論使用什么結構來存儲學生對象,都必須為這些結構提供上述的方法進行工作。
順序表 OR 鏈表?
無論我使用哪種結構進行存儲,我都可以使用 DAO 模型進行邏輯的隔離。無論是使用什么結構來存儲,這些結構都應該單獨封裝一個類,並且實現 StudentDao 接口中的方法。此時學生管理系統的核心代碼就不需要關心 Student 類使用什么結構存儲,直接調用增加學生、查詢學生和顯示所有學生的操作即可。
StudenDaoListImpl 類
該類使用 List 容器對 Student 類進行存儲,並且支持 StudentDao 接口中的所有操作。getStuByName 方法和 diplayAllStudents 方法,都使用了 for-each 對 List 中的元素進行遍歷。getStuByName 方法查詢時,使用 equals 方法對 name 屬性進行判斷。addStudent 方法添加學生時,可以使用 List 容器的 add 方法方便地在表尾加入新元素。
import java.util.*;
public class StudenDaoListImpl implements StudentDao {
private List<Student> students = new ArrayList<Student>();
@Override
public Student getStuByName(String name) {
Student temp = null;
for(Student e:students){
if(e.getName().equals(name)){
temp = e;
}
}
return temp;
}
@Override
public boolean addStudent(Student student) {
students.add(student);
return true;
}
@Override
public void diplayAllStudents(){
for(Student e:students){
if (e != null)
System.out.println(e);
}
}
}
StudentDaoArrayImpl 類
該類使用數組 Array 對 Student 類進行存儲,並且支持 StudentDao 接口中的所有操作。注意數組的大小是有限的,需要先使用 StudentDaoArrayImpl 預先分配空間。getStuByName 方法使用 for 遍歷下標,對每個元素比較姓名,注意這時要考慮元素為空的情況。當實現 addStudent 方法時,需要遍歷找到值為 null 元素插入,總體來說使用數組這種比較弱的結構去存儲,要盡可能考慮周全。
public class StudentDaoArrayImpl implements StudentDao {
private Student[] students;
public StudentDaoArrayImpl(int size) {
students = new Student[size];
}
@Override
public Student getStuByName(String name) {
Student temp = null;
for(int i=0; i<students.length;i++){
if(students[i]!= null){
if (students[i].getName().equals(name)){
temp = students[i];
break;
}
}
}
return temp;
}
@Override
public boolean addStudent(Student student) {
boolean success = false;
for(int i=0; i<students.length;i++){
if(students[i]==null){
students[i] = student;
success = true;
break;
}
}
return success;
}
@Override
public void diplayAllStudents(){
for(Student e:students){
if (e != null)
System.out.println(e);
}
}
}
測試 StudentDao 接口
先直接運行下述代碼,然后再使用注釋掉的代碼替換存儲結構,我們發現其他的代碼都不需要變動,依然能實現我們的需求。這就是使用 DAO 的好處了,這里 StudentDao 接口把數據存儲和對數據的操作完全隔離掉了,其他代碼對數據存儲的方式一無所知,只需要懂得調用 StudentDao 接口支持的操作即可。
public class Test {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("Tom");
students[1]= new Student("Jerry");
students[2] = new Student("Sophia");
StudentDao sdm = new StudentDaoArrayImpl(50);//使用數組實現
//StudentDao sdm = new StudenDaoListImpl();//使用列表實現
System.out.println("===========寫入學生========");
for(Student e:students){
if (!sdm.addStudent(e)){
System.out.println("添加學生失敗");
}else{
System.out.println("插入成功!!");
}
}
System.out.println("===========顯示所有學生========");
sdm.diplayAllStudents();
System.out.println("===========查詢學生========");
Student temp = sdm.getStuByName("Tom") ;
if(temp == null){
System.out.println("查無此人");
}else{
System.out.println(temp);
}
}
}
DAO 模式應用:UserDAO 接口編程
UserDAO 接口
解析完樣例代碼后,我們來具體事件一個簡單的 User 類。例如在購物車程序設計時,我們要針對不同的用戶保存購物車信息,此時基於用戶的各種操作就很有必要,例如用戶登錄、注冊用戶和修改密碼等。當用戶信息保存在數據庫中時,就需要建立數據庫連接進行 SQL 增刪查改系列操作。基於 DAO 模式的思想,我們將這些用戶和數據庫交互的操作總結出來,設計出 UserDAO 接口。
package user;
public interface UserDAO {
public boolean registerUser(String username, String password); //注冊用戶
public boolean changePassword(String username, String password, String new_passwd); //更改密碼
public boolean signIn(String username, String password); //用戶登錄
public boolean logOffUser(String username); //注銷用戶
}
可以明顯地看到,這 4 個方法分別對應了數據庫增、刪、查、改四個基本操作。
UserMysql 類
UserMysql 類顧名思義,是 UserDAO 接口基於 MySQL 數據庫的具體實現。該類需要把 sql import 進來,接下來的測試暫時不會用到類的各個屬性。
package user;
import java.sql.*;
import java.util.*;
public class UserMysql implements UserDAO{
private int id; // 用戶名
private String userName; // 用戶名
private boolean sessionStatus; //會話狀態
public UserMysql() {
this.id = 13;
this.userName = null;
this.sessionStatus = false;
}
}
registerUser 方法
registerUser 方法用於注冊用戶,注意在注冊用戶之間需要先判斷用戶名是否存在。這里采用的做法是先使用一個 SELECT 語句查詢用戶名,若返回結果行數為 0 則可以注冊,使用 INSERT 語句向 user 表中插入一個記錄。
@Override
public boolean registerUser(String username, String password) {
Connection conn = null; //創建 Connection 數據庫連接對象
Statement statement = null; //創建靜態 SQL 語句 Statement 對象
ResultSet rs = null; //創建 ResultSet 結果集對象
boolean flag = false;
try {
conn = MysqlConnect.connectDatabase(); //數據庫連接
statement = conn.createStatement(); //初始化靜態 SQL 語句
String sqlSelect = "SELECT username FROM users WHERE binary username = '%s';";
//查詢用戶名是否存在,是則重復,無法注冊
rs = statement.executeQuery(String.format(sqlSelect, username));
rs.last();
if(rs.getRow() > 0) {
System.out.println("用戶名重復");
flag = false; //操作不能正常運行
}
else {
//關閉 2 條現有的資源
rs.close();
statement.close();
flag = true; //操作可以正常運行
System.out.println("用戶名可用");
//再次初始化,進行插入操作
statement = conn.createStatement();
String sqlInsert = " INSERT INTO users(id, username, password) values(%d,'%s','%s'); ";
if(statement.executeUpdate(String.format(sqlInsert, id, username, password)) != 0) {
System.out.println("注冊成功");
}
else {
System.out.println("注冊失敗");
}
}
}catch (SQLException sqle) {
sqle.printStackTrace();
}catch(Exception e){
e.printStackTrace();
}finally{ //關閉所有資源
MysqlConnect.close(conn);
MysqlConnect.close(rs);
MysqlConnect.close(statement);
}
return flag; //true 為操作成功,反之操作失敗
}
changePassword 方法
changePassword 方法用於修改用戶的密碼,改密碼之前需要先判斷用戶是否存在,且原密碼驗證正確。此時先使用一個 SELECT 語句查詢用戶名是否存在,我們不能對不存在的記錄改字段,然后時候 UPDATE 語句更新 password 的值。
@Override
public boolean changePassword(String username, String password, String new_passwd) {
Connection conn = null;
Statement statement = null;
ResultSet rs = null;
boolean flag = false;
try {
conn = MysqlConnect.connectDatabase();
statement = conn.createStatement();
//先進行改密用戶的認證
String sqlSelect = "SELECT username,password FROM users WHERE binary username = '%s' AND password = '%s';";
rs = statement.executeQuery(String.format(sqlSelect, username, password));
rs.last();
if(rs.getRow() == 1) { //只能對已經存在的用戶改密碼
rs.close();
statement.close();
statement = conn.createStatement();
flag = true; //操作可以正常運行
System.out.println("驗證通過");
//以 username 為過濾條件,將密碼替換
String sqlUpdate = "UPDATE users SET password='%s' WHERE username = '%s'";
if(statement.executeUpdate(String.format(sqlUpdate, new_passwd, username)) != 0) {
System.out.println("密碼更改成功");
}
else {
System.out.println("密碼更改失敗");
}
}
else { //不能對不存在的用戶改密碼
System.out.println("驗證失敗");
flag = false; //操作不能正常運行
}
}catch (SQLException sqle) {
sqle.printStackTrace();
}catch(Exception e){
e.printStackTrace();
}finally{
MysqlConnect.close(conn);
MysqlConnect.close(rs);
MysqlConnect.close(statement);
}
return flag; //true 為操作成功,反之操作失敗
}
signIn 方法
signIn 方法在用戶登錄時調用,方法根據輸入的用戶名和密碼進行查找,如果有查找到 1 條記錄則登錄成功,改變 UserMysql 對象的屬性。
@Override
public boolean signIn(String username, String password) {
Connection conn = null;
Statement statement = null;
ResultSet rs = null;
boolean flag = false;
try {
conn = MysqlConnect.connectDatabase();
statement = conn.createStatement();
String sql = "SELECT username,password FROM users WHERE binary username = '%s' AND password = '%s';";
rs = statement.executeQuery(String.format(sql, username, password));
rs.last();
if(rs.getRow() == 1) { //紀錄存在,說明可以讓用戶登錄,修改對象的各個屬性
this.sessionStatus = true;
this.userName = username;
flag = true;
}
else {
flag = false;
}
}catch (SQLException sqle) {
sqle.printStackTrace();
}catch(Exception e){
e.printStackTrace();
}finally{
MysqlConnect.close(conn);
MysqlConnect.close(rs);
MysqlConnect.close(statement);
}
return flag; //true 表示登錄成功,反之為登錄失敗
}
logOffUser 方法
logOffUser 方法一旦被調用,就要在數據庫把對應的用戶記錄刪掉,使用 DELETE 語句實現。
@Override
public boolean logOffUser(String username) {
Connection conn = null;
Statement statement = null;
try {
conn = MysqlConnect.connectDatabase();
statement = conn.createStatement();
String sqlDelete = "DELETE FROM users WHERE username = '%s'";
if(statement.executeUpdate(String.format(sqlDelete, username)) != 0) {
System.out.println("用戶注銷成功");
}
else {
System.out.println("用戶注銷失敗");
}
}catch (Exception e) {
e.printStackTrace();
}finally{
MysqlConnect.close(conn);
MysqlConnect.close(statement);
}
return true;
}
MysqlConnect 類
MysqlConnect 類是配合 UserMysql 類工作的輔助類,主要任務是負責建立和 MySQL 數據庫的連接。注意 close 方法重復部分較多,這里只放出 Connection 對象的釋放,PreparedStatement、ResultSet、Statement 對象都應該有個 close 方法釋放掉。
package user;
import java.sql.*;
public class MysqlConnect {
//設置 JDBC 驅動名及數據庫 URL
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
//?useSSL = false 表示不啟用 SSL
static final String DB_URL = "<數據庫 URL>?useSSL=false";
//數據庫的用戶名與密碼,結合自己機子上的配置
static final String USER = "";
static final String PASS = "";
//連接數據庫
public static Connection connectDatabase() throws SQLException{
Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
return conn; //返回 Connection 連接對象
}
public static void close(Connection conn) {
if(conn != null) { //conn 對象不為 null 時要釋放掉
try {
conn.close(); //調用對象本身的 close 方法,這里套個異常處理
} catch (SQLException e) {
e.printStackTrace();
}
}
測試 UserMysql 類
數據庫配置
這里我直接使用 sqli-labs 靶場的 security 數據庫中的 users 表,表中有 id, username 和 password 字段,測試代碼將連接到該數據庫進行各個方法的調用。users 表的對象配置如下:
uesrs 表中已有的記錄如下:
測試代碼
測試代碼將按照注冊用戶、用戶登錄、修改密碼和注銷用戶的流程進行調用,每一個操作都是用死循環保證正確執行,然后再進入下一步。
public static void main(String[] args) {
/*需要替換數據庫等大動作時,只需要修改這行代碼*/
UserMysql a_user = new UserMysql();
/*只要替換的類實現了 UserDAO 接口,后面的代碼都不用換*/
Scanner sc = new Scanner(System.in);
String username = null;
String password = null;
//注冊用戶測試
while(true) {
System.out.print("注冊用戶,請輸入用戶名:");
username = sc.next();
System.out.print("請輸入密碼:");
password = sc.next();
if(a_user.registerUser(username, password)) {
break;
}
}
//用戶登錄測試
while(true) {
System.out.print("登錄用戶,請輸入用戶名:");
username = sc.next();
System.out.print("請輸入密碼:");
password = sc.next();
if(a_user.signIn(username, password)) {
System.out.println("登陸成功");
break;
}
else {
System.out.println("登錄失敗");
}
}
//密碼修改測試
while(true) {
System.out.print("修改密碼,請輸入新密碼:");
String new_passwd = sc.next();
if(a_user.changePassword(username, password, new_passwd)) {
break;
}
}
//賬號注銷測試
System.out.println("注銷賬號");
if(a_user.logOffUser(username)) {
System.out.println("測試結束");
}
}
測試流程
注冊用戶測試
用戶名已存在時,Eclipse 調試界面:
成功注冊時,Eclipse 調試界面:
數據庫狀態:
用戶登錄測試
登錄失敗時,Eclipse 調試界面:
登陸成功時,Eclipse 調試界面:
密碼修改測試
密碼修改后,Eclipse 調試界面:
數據庫狀態:
賬號注銷測試
賬號注銷后,Eclipse 調試界面:
數據庫狀態:
總結
在測試代碼中,我們發現當需要替換數據庫時,只要我們替換的類實現了 UserDAO 接口,就只需要修改第一行代碼初始化其他類的對象。這得益於 DAO 模式良好的封裝性,通過隔離數據的邏輯操作和數據庫的連接,使得我們能很輕松地替換組件。
參考資料
網絡技術:網絡互聯模型
Java 面向對象:接口
MySQL——SELECT
MySQL——增、刪、改
菜鳥教程-Java DAO 模式
菜鳥教程-Java MySQL 連接
java對mysql的操作
mysql查詢不區分大小寫
解決MySQL在連接時警告