zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Java DAO 模式

2023-09-27 14:28:59 时间

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 模式优势如下:

  1. 提高代码的复用性:通过将一系列相似的操作提炼并设计出一个接口,使得实现接口的类的特征更为鲜明,使得该类能更灵活地嵌入其他代码中发挥作用;
  2. 利于分工合作:通过将某一操作的具体实现隔离,可以使编写不同部分的程序员专注于自己该做的事情,通过调用对应的接口能更好地进行合作;
  3. 组件的替换更为方便:由于操作的具体实现被隔离开来,当该操作的具体实现发生较大改动时,对其他调用该操作的部分毫无影响。无需做大的变动,只需要修改具体实现的部分即可;
  4. 提高代码的拓展性:通过 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在连接时警告