[自己动手]用Java的反射实现DAO

(难得,我在JavaEye博客的第一篇Java文章。)

Java: 一种语言。具体的说:
引用
来自http://james-iry.blogspot.com/2009/05/brief-incomplete-and-mostly-wrong.html

1996 - James Gosling invents Java. Java is a relatively verbose, garbage collected, class based, statically typed, single dispatch, object oriented language with single implementation inheritance and multiple interface inheritance. Sun loudly heralds Java's novelty.

西元1996年,詹姆斯·高斯林(James Gosling)发明了Java语言。Java是一个相对罗嗦一点的有垃圾回收的基于类的静态类型的单指派的单实现继承的多接口继承的面向对象的语言。【译注:咳……咳……咳咳……我靠,憋死我了】。Sun公司高声宣布:Java牛逼!


Reflection:反射。具体的说,就是允许程序在运行时查询“某个对象属于什么类?这个类有什么域?有什么方法?”并且可以根据以上查询,修改某个域,或者调用某个方法。

Data Access Object:简称DAO,数据访问对象。一种设计模式,将上层应用中的数据访问和下层的数据库管理系统(DBMS)分离开。我的理解:DAO的一个方面就是让上层应用不用再关心怎么写SQL语句。

写数据库程序免不了和DBMS打交道,尤其是RDBMS。然后,也许每次访问数据都要写一堆SQL语句。反正我是不太喜欢直接写SQL,尤其是当表的数量很大的时候,可能要给每个表写至少4个SQL语句(也就是CRUD:Create(INSERT),Read(SELECT),Update,Delete)。一个有6个表的小数据库就够让我copy&paste一共20次。随着数据库规模扩大,工作量复杂度虽然是线性的,但是这个乘数太大了(起码对于我这种应付课程作业,不能全职coding的学生而言),而且修改的代价也是线性的。

程序里假设有这样的模型:
数据库里有employee表:
CREATE TABLE employee (
    id INTEGER PRIMARY KEY,
    ename VARCHAR(32),
    salary FLOAT,
    birth_date DATE
);


为了方便,Java里做一个类,存放这个表中的元组:
class Employee {
    public int id;
    public String ename;
    public double salary;
    public java.sql.Date birth_date; // 原谅我没有服从Java的命名规范。
    // 因为我的数据库就是这么写的。(这有办法补救,可以做到既服从Java的namingConvention,又满足数据库中的不同命名)
}


然后呢,我做一个EmployeeDAO类访问这个Employee表:
class EmployeeDAO {
    public static ArrayList<Employee> select() throws SQLException {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        ArrayList<Employee> al = null;
        try {
            conn = DriverManager.getConnection("jdbc:....",null);
            st = conn.createStatement();
            rs = st.executeQuery("select * from employee");

            al = new ArrayList<Employee>();

            while (rs.next()) {
                Employee e = new Employee();
                e.id = rs.getInt("id");
                e.ename = rs.getString("ename");
                e.salary = rs.getFloat("salary");
                e.birth_date = rs.getDate("birth_date");
                al.add(e);
            }
        } finally {
            if (rs != null)
                rs.close();
            if (st != null)
                st.close();
            if (conn != null)
                conn.close();
        }

        return al;
    }    
}

为了简单,我们暂且只实现SELECE操作。我们看看我们的上述实现。它创建连接,执行语句,解析结果。其中,有两项和employee表相关:
1. 那个SQL语句是和employee表相关的:“SELECT * FROM employee”。
2. ResultSet的解析也和employee表相关:我们需要知道employee表中有哪些列,也需要知道Employee类中有哪些域。(它们实际上是相对应的)

我们目前只有一个表employee。但是,随着我们增加更多的表,DAO的数据也会增加。

我们加一个project表
CREATE TABLE project (
    id INTEGER PRIMARY KEY,
    pname STRING,
    leader_id INTEGER
);


然后,紧接着用一个Java类对应这个表:
class Project {
    public int id;
    public String pname;
    public int leader_id;
}


然后,还需要一个DAO:
public class ProjectDAO {
    public static ArrayList<Project> select() throws SQLException {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        ArrayList<Project> al = null;
        try {
            conn = DriverManager.getConnection("jdbc:....",null);
            st = conn.createStatement();
            rs = st.executeQuery("select * from project");

            al = new ArrayList<Project>();

            while (rs.next()) {
                Project p = new Project();
                p.id = rs.getInt("id");
                p.pname = rs.getString("pname");
                p.salary = rs.getInt("leader_id");
                al.add(p);
            }
        } finally {
            if (rs != null)
                rs.close();
            if (st != null)
                st.close();
            if (conn != null)
                conn.close();
        }

        return al;
    }    
}

我承认上面的代码是copy&paste了EmployeeDAO的代码。仔细观察,其实区别也只有两部分:
1. SQL语句中的employee变成了project
2. ResultSet的处理,只是列/域不同而已。


------- 分割线 -------


对于懒惰的我,怎么会忍心copy&paste呢?如果有更多的表,每个表有更多的操作,要改多少地方?一致性怎么维护?改错了怎么办?

下面介绍Java的“反射”:Reflection

“反射,简单的说,就是在运行时查看某个类有什么成员”。它可以列举所有的域、方法、内部类、枚举等等,并可以修改/调用/实例化它们。

---- java.lang.Class类 ----

在Java里有一个特殊的类叫Class,全称是java.lang.Class。注意C是大写的,小写的class是Java的一个关键字。这个类的每个对象表示Java中的每一个类。有点绕?这么解释吧;如果
Class cls;

那么,Class是一个类。cls是Class类的一个对象。每个cls表示一个类。

如何“表示一个类”?
Class cls1 = int.class;
Class cls2 = String.class;

java的语法:<类名>.class,可以得到一个Class类的对象。这里,cls1是一个Class对象,表示int这个数据类型(确切的说int不是类);cls2也是一个Class对象,标识String这个类。

如果已知一个对象:
Object obj = new Object();
String str = "blah";
Employee emp = new Employee();

Class cls1 = obj.getClass();
Class cls2 = str.getClass();
Class cls3 = emp.getClass();

凡是Object类及其子类的对象,都具有getClass()方法。返回一个Class对象,表示它所属的类。

可以用Class.getName()方法获得类名。
cls1.getName() // "java.lang.Object"
cls1.getSimpleName() // "Object"


---- java.lang.reflect.Field类 ----

Field类,全称java.lang.reflect.Field,表示一个类中的一个域。

可以用Class.getField()和Class.getFields()方法获得一个类中的域的Field对象。
可以用Field.getName()查看该域的名称,getType()方法获得类型。
注意:Class.getField()只能返回public的域。
Class cls = Employee.class;
Field[] fields = cls.getFields();
Field field = cls.getField("ename");
field.getName(); // "ename"
field.getType(); // 返回值等于String.class


注意:Field对象并不和某个对象绑定:它只标识一个“类”中的域,而不是一个“对象”中的域。如果获得某个域的值,或修改一个域,需要指定对象。
Employee e = new Employee();
Class cls = e.getClass();

Field f1 = cls.getField("id");
Field f2 = cls.getField("ename");
Field f3 = cls.getField("salary");

int id = f1.getInt(e);
String ename = f2.get(e); // 注意:字符串是对象;对象一律用get取值
double salary = f3.getDouble(e);
Double salary2 = f3.get(e); // 用封装的基础类型也可以。

f1.setInt(e,1);
f2.set(e, "foobar"); // 对象一律用set赋值
f3.setDouble(e, 345.67);
f3.set(e, new Double(345.67)); // 用封装的基础类型也可以。


==== “内窥”某个对象 ====

这里举一个例子,怎么用以上的Class和Field类,查看一个对象的内部。

public void introspectObject(Object obj) throws Exception {
  Class cls = obj.getClass();  // 先获得Class对象
  System.out.println(cls.getName());  // 打印类名。对象是没有名字的,但类有。

  Field[] fields = cls.getFields(); // 获得域
  for(Field field : fields) {
    String name = field.getName(); // 获得域名
    Object value = field.get(obj); // 获得域值
    String valueStr = value==null?"null":value.toString(); // 对null要特殊处理。
    System.out.format("%s: %s\n", name, value);
  }
}



--------- 分割线 ---------


有了上述准备,我们可以开始利用“反射”优化我们的DAO了。

好啦,现在就是从copy&paste的噩梦中逃脱出来的时候了!

---- Java数据类型与SQL数据类型的对应关系 ----

每种SQL数据类型都有对应的Java数据类型。请参考你的DBMS的手册。这里以Apache Derby为例:
SQL的INTEGER,对应Java的int。
SQL的FLOAT,对应Java的double。
SQL的VARCHAR,对应Java的String。
SQL的DATE,对应Java的java.sql.Date。

所以,设计对象的时候要注意这一点。比如Employee类的birth_date域的类型是java.sql.Date。

而ResultSet有getObject和setObject两个方法,可以略过列的数据类型。

我们再看看Employee类的定义:
class Employee {
    public int id;
    public String ename;
    public double salary;
    public java.sql.Date birth_date;
}


我们通过反射,可以知道这个对象的所有的域的名称和类型。这样,就可以构造SQL语句。然后通过ResultSet.getObject方法获得SQL表中元组的成员,然后用Field.set方法设置对象的域。

---- 用反射重新实现DAO ----

下面,我们写一个新的DAO。这个DAO不与任何一个具体的表(如employee或project)绑定。它可以用于任何表。这样,我们就不用为增加一个表而增加工作量了。

public class GenericDAO {
    private final Class tableClass;

    public GenericDAO(Class cls) {
        this.tableClass = cls; // 我们需要记录对象对应的类的Class对象
    }

    public static ArrayList<? extends Object> select() throws Exception {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        ArrayList<Object> al = null;
        try {
            conn = DriverManager.getConnection("jdbc:....",null);
            st = conn.createStatement();
            // rs = st.executeQuery("select * from employee");

我们不能这样做:我们的表不一定叫employee。所以,需要替换掉。
            rs = st.executeQuery(String.format(
                 "select * from %s",tableClass.getSimpleName()
                 ));

接下来,是从ResultSet中提取域。
注意:Class.getFields()方法获得的Field对象的顺序是 不可预知的。所以,对于列,应该用列名表示,而不是位置。
            al = new ArrayList<Object>();

            Field[] fields = tableClass.getFields(); // 获得域列表

            while (rs.next()) {
                Project p = new Project();
                for (Field field : fields) {
                    Object value = rs.getObject(field.getName());
                    field.set(p, value);  // 设定域值
                }
                al.add(p);
            }

接下来做一些善后工作就行了。
        } finally {
            if (rs != null)
                rs.close();
            if (st != null)
                st.close();
            if (conn != null)
                conn.close();
        }

        return al;
    }    
}


看,这个代码和具体的表没有直接联系。使用的时候:
ArrayList<Employee> employees = new genericDAO(Employee.class).select();
ArrayList<Project> projects = new genericDAO(Project.class).select();


---- 插入(INSERT)操作 ----

有了这样的方法,我们也不用怕增加一种操作了。下面是INSERT操作的代码。
protected void insert(Object obj) throws Exception {
    Connection conn = null;
    PreparedStatement st = null;
    try {
        conn = DbProvider.getConnection(site);

        Field[] fields = tableClass.getFields();

        // 下面一段代码准备SQL语句的两部分。
        StringBuilder sb1 = new StringBuilder();
        StringBuilder sb2 = new StringBuilder();

        for (int i = 0; i < fields.length; i++) {
            if(i>0) {
                sb1.append(",");
                sb2.append(",");
            }
            sb1.append(fields[i].getName());
            sb2.append("?");
        }

        String commaSeparatedFieldNames = sb1.toString();
        String commaSeparatedQuestionMarks = sb2.toString();

        // 安全起见,我们需要用prepareStatement处理用户输入。
        // 但是因为类的名称是可以由程序员控制的,我们用String.format生成语句
        st = conn.prepareStatement(String.format(
                    "INSERT INTO %s(%s) values(%s)",
                    tableClass.getSimpleName(), commaSeparatedFieldNames,
                    commaSeparatedQuestionMarks));

        // 然后,填充这个PreparedStatement
        for (int i = 0; i < fields.length; i++) {
            st.setObject(i + 1, fields[i].get(obj));
        }

        st.executeUpdate();
    } finally {
        if (st != null)
            st.close();
        if (conn != null) {
            conn.close();
        }
    }
}



------- 分割线 -------

总结一下:Java的反射机制在这种情况下给我带来了很大的方便。优点就是方便,减少工作量;缺点是有运行时效率代价,而且少了一些类型检查。

反射对于懒人尤其适用。我常常以“好的程序员总是懒的“为借口。不过,谁愿意像巨兽一样在焦泥潭里挣扎,越挣扎陷得越深呢?

最后,有个问题还是没有解决:像我们这些学生,很多人很忙碌——实验室的项目、实习、找工作,还有作业、考试……
引用

因为忙,所以没有时间;没有时间,就需要一些快速完成任务的方法;想快速完成人物,就需要恰当的技术(如上述的反射);要技术,就要学习知识;要学习知识,就需要时间……

可是我们没有时间!

所以不能学习新的知识,没有知识也就没有技术,没有技术也就不能快速完成任务,不能完成任务就没有时间,没有时间就忙。越忙,就越没有时间学习,然后就越来越忙。


这是怎样的恶性循环!怎样才能摆脱这个无休止的噩梦呢。。。。。




你可能感兴趣的:(java,DAO,sql,jdbc,Derby)