- 浏览: 335227 次
- 来自: ...
文章分类
最新评论
-
Severus-zhang:
太棒了 顶你
第七讲 Swing用户界面设计 -
liyanhui:
...
我在上海奋斗五年 从月薪3500到700万 -
duanyong:
new SimpleDateFormat("yyyy ...
字串日期格式转换 -
huntingbaby:
鼓掌 非常的有味道呢! 谢谢您的分享 爱就是希望所在
我在上海奋斗五年 从月薪3500到700万 -
shhnuu:
GDAT 证券数据引擎。
http://www.cnitblo ...
获取股票实时交易数据的方法
这个小小的数据库操作封装框架是参考IBM开发网上的两篇文章并在其基础上扩充了一些功能而得到的。所以首先要感谢两篇文章的作者。
学习JDBC以来一直想实现一个简单的封装来方便编程但是由于水平有限一直没有较好的办法,看了IBM开发网上的两篇文章以后感觉作者的设计思想很好一定能扩充成一个实用的JDBC封装。所以我在文章提供的源码基础上加了一些功能这些功能包括支持多种数据类型,处理了空值,利用反射方便的在Row对象和值对象之间进行转换,还有加了一个我自认为通用的DAO类来方便用户的操作。
我把源码提供出来有两个目的一个是希望能帮助比我还初学的初学者熟悉JDBC,另外就是请各位高手不吝赐教,改进程序中的错误如果能将你们的对JDBC的封装方法提供出来那就更好了(不要说你们只用EJB或者Hibernate,JDO什么的?)。
IBM开发网的那两篇文章分别是《一个简单的 JDBC 包装器》《对一个简单的 JDBC 包装器的扩展及应用》,我的邮箱是xsimple2003@yahoo.com.cn有事请与我联系。
设计思想
? 把DBMS抽象成类Database,这个类负责管理数据库连接以及提供表对象。
? 把数据库中的一张或多张表抽象成类Table,这个类中提供对表的添加,修改,删除的JDBC封装。
? 将数据库表中的一条记录抽象成类Row,这个类用HashMap保存关系数据库中表格中一行数据的字段名和值并提供一些相关操作。另外这个类还提供了两个静态方法用于在Row对象和ValueObject之间进行方便的转换。
? 把对个Row的集合抽象成RowSet,这个类中用一个vector把多个Row对象保存起来并提供一些相关操作。
代码分析
由于已经给出源码所以我只对代码中关键的和需要注意的地方加以说明,大家可以执行源码一边演示一边体会。
? Database类源码如下:
package com.gdrj.util.database;
import java.sql.*;
import javax.sql.*;
import com.gdrj.util.servicelocator.*;
public class Database {
/**
* 这个数据库连接成员只有在与数据库直接建立连接的情况下是有效的
*/
private Connection conn = null;
/**
* 当这个参数有效时,表明程序是直接与数据库建立的连接而不是从连接池里取得连接
*/
private String url, user, password;
/**
* 当这个参数有效时,表明程序是从连接池里取得连接。
*/
private String datasource;
/**
* 用数据库地址,用户名,密码初始化数据库对象,这个构造器用于程序是直接
* 与数据库建立连接的情况。
* @param url
* @param user
* @param password
*/
public Database(String url, String user, String password) {
this.url = url;
this.user = user;
this.password = password;
}
/**
* 用JNDI数据源名初始化数据库对象,这个构造器用于从连接池取数据库连接的情况。
* @param datasource
*/
public Database(String datasource) {
this.datasource = datasource;
}
/**
* 得到数据库连接,对于是否从连接池里取连接做了自动处理即根据用户调用了哪个构造器
* 来判断是否直接与数据库建立连接还是从连接池里取连接。
* 对于用户来说不用考虑程序是从那里取得连接,他只管正确的初始化数据库对象。
* @return
* @throws SQLException
*/
public Connection getConnection() throws Exception {
if (datasource == null) { //直接与数据库建立连接
if (conn == null) {
conn = DriverManager.getConnection(url, user, password);
}
}
else { //从应用服务器的连接池里取得连接
ServiceLocator sl = ServiceLocator.getInstance();
DataSource ds = sl.getDataSource(datasource);
return ds.getConnection();//每调用一次都返回一个连接池中的数据库连接
}
return conn;
}
/**
* 释放连接,如果是直接与数据库连接的情况则什么也不做
* 如果是从连接池中取得的连接那么释放传来的连接
* @param conn
*/
public void disConnect(Connection connection) {
if (datasource != null) { //只处理从连接池取连接的情况
try {
if (connection != null) {
connection.close();
}
}
catch (Exception ex) {}
}
}
/**
* 得到与参数名对应的表对象,注意这里不作任何数据库操作
* @param name
* @return
*/
public Table getTable(String name) {
return new Table(this, name);
}
}
这个类是对DBMS的抽象,所以使用时应用程序中只要有一个Database对象就够了,如果你是以与数据库之间建立连接的方式使用那么你用Database(String url, String user, String password)构造器进行初始化。如果是从应用服务器的连接池中取得连接的方式使用那么用Database(String datasource)构造器初始化,这样以后你使用这个对象进行getConnection和disConnection时就不用去考虑始终保持一个连接(C/S方式),还是将连接返回连接池了因为在disConnection中已经做了处理。集体使用方法将Table类。在getConnection中的从连接池中取连接的代码你只要参考以下《J2EE核心模式》中的服务定位器模式就知道是怎么回事了,你在用Database(String url, String user, String password)初始化时其中的代码不起作用。
? Table类源码如下:
package com.gdrj.util.database;
import java.sql.*;
import java.util.*;
import com.gdrj.util.*;
public class Table {
/**
* 通过这个数据库对象得到数据库连接
*/
private Database database;
/**
* 数据库中一个或多个(只限查询)表的名
*/
private String name;
/**
* 初始化表对象,此时不作任何数据库相关操作
* 一般通过database的getTable调用
* @param database
* @param name
*/
public Table(Database database, String name) {
this.database = database;
this.name = name;
}
/**
* 查询某一行
* @return
*/
public Row getRow(String fields, String criteria, Object[] args) throws
DBAccessException {
RowSet rows = executeQuery(fields, criteria, args);
if (rows == null) {
return null;
}
return rows.get(0);
}
/**
* 得到一个多行记录
* @param criteria 查询条件
* @param args 查询条件的参数列表
* @return
*/
public RowSet getRows(String fields, String criteria, Object[] args) throws
DBAccessException {
return executeQuery(fields, criteria, args);
}
/**
* 执行SQL查询
* @param fields 要查询的字段,如果传入null则表示查询表中所有字段
* @param criteria用户输入的查询Where条件
* @param args 用到的参数数组
* @return 返回符合结果行集
*/
private RowSet executeQuery(String fields, String criteria, Object[] args) throws
DBAccessException {
Connection conn = null;
RowSet rows = new RowSet();
String sql = null;
if (fields == null) {
fields = "*";
}
try {
conn = database.getConnection(); //取得数据库连接,在方法内部对不同的连接情况进行了处理
sql = "select " + fields + " from " + name +
( (criteria == null) ? "" :
(" where " + criteria));
PreparedStatement pstmt = conn.prepareStatement(sql);
if (args != null) { //如果有查询参数则设置参数
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
}
ResultSet rs = pstmt.executeQuery();
ResultSetMetaData rsmd = rs.getMetaData();
int cols = rsmd.getColumnCount();
/**@todo 判断是否为零*/
if (cols == 0) {
return null;
}
while (rs.next()) {
Row row = new Row();
for (int i = 1; i <= cols; i++) {
String name = rsmd.getColumnName(i);
Object value = rs.getObject(i); //作通用类型处理,这样row中的类型都是Object型的。
/**
* 这里要做空值处理,因为在进行RowToValueObject转换时如果是空值则不能得到值的类型
* 所以如果是空值那么把value设置成类型信息
*/
if (value == null) {
value = Class.forName(rsmd.getColumnClassName(i));
}
// System.out.println(value.getClass());//用于得到数据库中的类型对应Java中的什么类型
row.put(name, value);
}
rows.add(row);
}
rs.close();
pstmt.close();
}
catch (Exception ex) {
throw new DBAccessException(InforGeter.getErrorInfor(this, "executeQuery",
ex, "执行SQL(" + sql + ")查询时出错!"));
}
finally {
database.disConnect(conn); //调用数据库对象的释放连接方法(此方法内对取得连接方式的不同情况做了处理)
}
return rows;
}
/**
* 增加一行
* @param row
*/
public int putRow(Row row) throws DBAccessException {
return putRow(row, null, null);
}
/**
* 修改一行(没有条件就是增加)
* @param row
* @param conditions
*/
public int putRow(Row row, String conditions, Object[] args) throws
DBAccessException {
String ss = "";
int affectableRow = 0; //执行SQL后影响的行数
if (conditions == null) {
ss = "INSERT INTO " + name + "(";
for (int i = 0; i < row.length(); ++i) {
String k = row.getKey(i);
ss += k;
if (i != row.length() - 1) {
ss += ", ";
}
}
ss += ") VALUES (";
for (int j = 0; j < row.length(); ++j) {
ss += (row.get(j) == null) ? "null" : "?"; //如果row中有空值则设置为null,否则设置为查询参数
if (j != row.length() - 1) {
ss += ", ";
}
}
ss += ")";
}
else {
ss = "UPDATE " + name + " SET ";
for (int i = 0; i < row.length(); ++i) {
String k = row.getKey(i);
ss += k + "=" + ( (row.get(i) == null) ? "null" : "?"); //设置查询参数
if (i != row.length() - 1) {
ss += ", ";
}
}
ss += " WHERE ";
ss += conditions;
}
Connection conn = null;
try {
conn = database.getConnection();
PreparedStatement st = conn.prepareStatement(ss);
int j = 0; //查询参数计数器
for (int i = 0; i < row.length(); i++) {
if (row.get(i) != null) { //如果不是空则解析查询参数
st.setObject(++j, row.get(i)); //解析查询参数
}
}
if (args != null) {
for (int i = 0; i < args.length; i++) {
st.setObject(++j, args[i]);//预定的规则,null不能放到查询参数中要以name=null的静态形式存放
}
}
affectableRow = st.executeUpdate();
st.close();
}
catch (Exception ex) {
ex.printStackTrace();
throw new DBAccessException(InforGeter.getErrorInfor(this, "putRow", ex,
"更新表" + name + "中的数据时出错!"));
}
finally {
database.disConnect(conn);
}
return affectableRow;
}
/**
* 删除一行
* @param row
*/
public int delRow(Row row) throws DBAccessException {
String ss = "";
int affectableRow = 0;
ss = "delete from " + name + " where ";
for (int i = 0; i < row.length(); ++i) {
String k = row.getKey(i);
ss += k + ((row.get(i) == null)?" is null":"=?"); //设置查询参数有空值处理
if (i != row.length() - 1) {
ss += " and ";
}
}
Connection conn = null;
try {
conn = database.getConnection();
PreparedStatement st = conn.prepareStatement(ss);
int j = 0;//查询参数计数器
for (int i = 0; i < row.length(); i++) {
if (row.get(i) != null) {
st.setObject(++j, row.get(i)); //解析查询参数
}
}
affectableRow = st.executeUpdate();
st.close();
}
catch (Exception ex) {
throw new DBAccessException(InforGeter.getErrorInfor(this, "delRow", ex,
"删除表" + name + "中的数据时出错!"));
}
finally {
database.disConnect(conn);
}
return affectableRow;
}
/**
* 有条件的删除即删除多行
* @param condition
* @param args
*/
public int delRow(String condition, Object[] args) throws DBAccessException {
String ss = "";
int affectableRow = 0;
ss = "delete from " + name + " where ";
ss += condition;
Connection conn = null;
try {
conn = database.getConnection();
PreparedStatement st = conn.prepareStatement(ss);
if (args != null) {
for (int i = 0; i < args.length; i++) {
st.setObject(i + 1, args[i]);
}
}
affectableRow = st.executeUpdate();
st.close();
}
catch (Exception ex) {
throw new DBAccessException(InforGeter.getErrorInfor(this, "delRow", ex,
"删除表" + name + "中的数据时出错!"));
}
finally {
database.disConnect(conn);
}
return affectableRow;
}
}
使用时可以用Database对象的getTable方法传入数据库表的名称来得到一个Table对象。得到这个对象后就可以对这个数据库表进行操作了,这个类提供了六个方法根据传过来的参数对数据库表进行添加修改删除操作。代码中没有特别难懂的地方,需要注意的是我在原有代码的基础上对空值进行的处理,在查询时如果表中的数据是空值的话那么我把字段对应的Java类型放到Row对象里,因为在进行Row对象到值对象的转换时用到了java反射API必须知道Row中的字段值的类型才能去调用值对象的setXXXX方法(见Row对象的toValueObject方法)。
? 行对象的源码如下:
package com.gdrj.util.database;
import java.util.*;
import java.math.BigDecimal;
import java.lang.reflect.*;
public class Row {
/**
* 排序,由于Hashtable不提供通过索引取得值的方法,并且其中的键值对也不是按照put上去时的顺序排列的。
* 注意:Vector中加入的对象是有序的,即按加入的顺序排列并且能够根据索引访问,可以看成是可变大小的数组
* List可以取代Vector
*/
private Vector ordering = new Vector();
/**
* 存放键值对(表中字段名称与字段值)
*/
private HashMap map = new HashMap();
public Row() {
}
/**
* 向HashMap中追加键值对,即字段名称与字段值
* @param name
* @param value
*/
public void put(String name, Object value) {
if (!map.containsKey(name)) {
ordering.addElement(name); //将键保存起来
}
map.put(name, value);
}
/**
* 得到行对象中字段的个数
* @return
*/
public int length() {
return map.size();
}
/**
* 根据字段名称取得字段值
* @param name
* @return
*/
public Object get(String name) {
return map.get(name);
}
/**
* 根据字段在HashMap中的编号取得字段值
* @param which
* @return
*/
public Object get(int which) {
String key = (String) ordering.elementAt(which);
return map.get(key);
}
/**
* 根据字段序号取得字段名称
* @param which
* @return
*/
public String getKey(int which) {
String key = (String) ordering.elementAt(which);
return key;
}
/**
* 打印,用于调试
*/
public void dump() {
for (Iterator e = map.keySet().iterator(); e.hasNext(); ) {
String name = (String) e.next();
Object value = map.get(name);
System.out.print(name + "=" + value + ", ");
}
System.out.println("");
}
/**
* 将行对象转换成值对象
* @param row
* @param type值对象类型
* @return
* @throws java.lang.Exception 这里的异常一般在DAO中处理,因为DAO调用
* 这个方法进行Row和ValueObject的转换
*/
public static Object toValueObject(Row row, Class type) throws Exception {
Object vo = type.newInstance(); //创建一个值对象
Field[] fields = type.getDeclaredFields(); //得到值对象中所有字段
for (int i = 0; i < fields.length; i++) {
String name = fields[i].getName(); //得到JavaBean的字段名
String nameInRow = toInRowName(name);//在此进行值对象名称到行对象名称的转换
Object value = row.get(nameInRow); //得到从数据库中取出的与字段名对应的值
String methodName = "set" + Character.toUpperCase(name.charAt(0)) +
name.substring(1); //得到setXXXX方法名
Class argClass = null;
if (value instanceof Class) {
argClass = (Class)value;
value = null;
}else{
argClass = value.getClass();
}
Method method = type.getMethod(methodName, new Class[] {argClass}); //得到set方法
method.invoke(vo, new Object[] {value});//调用setXXXX方法
}
return vo;
}
/**
* 根据传过来的值对象和类型把值对象转换到行对象中
* @param vo
* @return
* @throws java.lang.Exception 这里的异常一般在DAO中处理,因为DAO调用
* 这个方法进行Row和ValueObject的转换
*/
public static Row fromValueObject(Object vo) throws Exception {
Row row = new Row();
Class type = vo.getClass(); //得到Class用于进行反射处理
Field[] fields = type.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
String name = fields[i].getName();
String methodName = "get" + Character.toUpperCase(name.charAt(0)) +
name.substring(1);
Method method = type.getMethod(methodName, new Class[] {});
Object value = method.invoke(vo, new Object[] {});
String nameInRow = toInRowName(name);//在此进行值对象中的名称向行对象中的名称转换
row.put(nameInRow, value);
}
return row;
}
/**
* 将值对象中属性名转换成对应的行对象中的字段名(因为行对象中的字段名
* 在更新数据库时必须与数据库表中字段名完全匹配)
* 一般规则为 fsiId ---> fsi_id(现在假设的情况是如果出现有两个单词
* 以上的值对象属性名则数据库表中的字段名一定是有下划线的)
* @param voName
* @return
*/
public static String toInRowName(String voName) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < voName.length(); i++) { //遍历voName如果有大写字母则将大写字母转换为_加小写
char cur = voName.charAt(i);
if (Character.isUpperCase(cur)) {
sb.append("_");
sb.append(Character.toLowerCase(cur));
}
else {
sb.append(cur);
}
}
return sb.toString();
}
}
Row对象中用了一个HashMap对象存放着对应数据库表中的字段名和对应值,由于Map对象的无序性,所以用了一个vector(当然也可以用List代替)来存放字段名(按用户添加的顺序)这样就可以提供get(int i)方法来顺序取得Map中的值了。要注意的是三个静态辅助方法toValueObject,fromVauleObject,toInRowName。toValueObject方法用于将一个行对象转换为值对象方法中利用了Java的多态和反射机制(请大家参考反射API)。FromValueObject是上一个方法的逆操作,toInRowName方法是实现值对象中的属性名向数据库表中字段名的转换,因为一般在数据库建表时是用的这种形式stu_id,而Java中JavaBean的属性是这样的stuId。
? RowSet的代码如下:
package com.gdrj.util.database;
import java.util.*;
public class RowSet {
private Vector vector = new Vector();
public RowSet() {
}
public void add(Row row) {
vector.addElement(row);
}
public int length() {
return vector.size();
}
public Row get(int which) {
if (length() < 1) {
return null;
}
else {
return (Row) vector.elementAt(which);
}
}
public void dump() {
for (Enumeration e = vector.elements(); e.hasMoreElements(); ) {
( (Row) e.nextElement()).dump();
}
}
}
这个类就是把Row对象放到Vector以便操作。就不多说了。
? 为了方便使用我写了一个GeneralDAO类(我对DAO模式还在理解中请各位高手批评指教)代码如下:
package com.gdrj.util.database;
import java.util.*;
public class GeneralDAO {
/**
* 这个DAO对应的表对象
*/
private Table table;
/**
* 默认构造函数
*/
public GeneralDAO() {
}
/**
* 用数据库对象和表名初始化DAO
* @param db
* @param tableName
*/
public GeneralDAO(Database db, String tableName) {
getTable(db, tableName);
}
private void getTable(Database db, String name) {
table = db.getTable(name);
}
/**
* 根据条件将查找到的数据以值对象集合的形式返回
* @param fields 要查找的字段(*或null表示所有字段)
* @param criteria查询条件
* @param args与查询条件对应的参数数组
* @param voType值对象的类型
* @return
* @throws java.lang.Exception
*/
public Collection findDatas(String fields, String criteria, Object[] args,
Class voType) throws Exception {
RowSet rows = table.getRows(fields, criteria, args);
Collection col = new ArrayList();
for (int i = 0; i < rows.length(); i++) {
Object vo = Row.toValueObject(rows.get(i), voType); //返回一个值对象,注意是voType类型的对象
col.add(vo);
}
return col;
}
/**
* 向表中插入一条数据
* @param vo 与表对象对应的值对象
* @return
* @throws java.lang.Exception
*/
public int insertData(Object vo) throws Exception {
return table.putRow(Row.fromValueObject(vo));
}
/**
* 更新一条数据
* @param vo 与表对象对应的值对象
* @param criteria更新条件
* @param args与更新条件对应的参数数组
* @return
* @throws java.lang.Exception
*/
public int updateData(Object vo, String criteria, Object[] args) throws
Exception {
return table.putRow(Row.fromValueObject(vo), criteria, args);
}
/**
* 删除一条数据(条件比较严格各个字段的值必须与值对象中属性的值匹配)
* @param vo
* @return
* @throws java.lang.Exception
*/
public int deleteData(Object vo) throws Exception {
return table.delRow(Row.fromValueObject(vo));
}
/**
* 删除多条数据
* @param condition 删除条件
* @param args与条件对应的参数数组
* @return
* @throws java.lang.Exception
*/
public int deleteDatas(String condition, Object[] args) throws Exception {
return table.delRow(condition, args);
}
}
这个DAO类是对Table类的一个方便的封装。用户如果向操作数据库只要创建一个Database对象,一个DAO对象,一个值对象(对应表结构),然后就可以进行方便的数据库操作了,下面给出一个实例来演示这个小小框架的用法。
演示程序
首先建立一个teacher表,语法如下
create table teacher (
id int not null,
name varchar(20) not null,
birthday smalldatetime null,
address varchar(100) null,
income money null,
constraint id PRIMARY KEY NONCLUSTERED ( id )
)
然后建立一个与teacher表对应的值对象类。
public class TeacherVO implements Serializable {
private Integer id;
private String name;
private String address;
private BigDecimal income;
private java.sql.Timestamp birthday;
public TeacherVO() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public java.sql.Timestamp getBirthday() {
return birthday;
}
public void setBirthday(java.sql.Timestamp birthday) {
this.birthday = birthday;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public java.math.BigDecimal getIncome() {
return income;
}
public void setIncome(java.math.BigDecimal income) {
this.income = income;
}
public String toString(){
return " 编号:" + id + " 姓名:" + name + " 生日:" + birthday
+ " 地址:" + address + " 收入:" + income;
}
}
最后主程序的源码如下:
package org.together.jdbcwrap.test;
import java.util.*;
import com.gdrj.util.database.*;
public class GeneralDAOExample {
public static void main(String[] args)throws Exception {
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Database db = new Database("jdbc:odbc:emmis","sa","815023");
GeneralDAO dao = new GeneralDAO(db,"teacher");
/**
* 利用GeneralDAO进行查询
*/
Collection col = dao.findDatas("*","birthday is null",null,TeacherVO.class);
for (Iterator iter = col.iterator(); iter.hasNext(); ) {
Object item = iter.next();
System.out.println("item = " + item);
}
/**
* 利用GeneralDAO进行添加
*/
TeacherVO vo = new TeacherVO();
vo.setAddress("沈阳");
vo.setBirthday(new java.sql.Timestamp(0));
vo.setId(new Integer(11));
vo.setIncome(new java.math.BigDecimal(1000));
vo.setName("陶小川");
// dao.insertData(vo); //添加一条记录
// dao.updateData(vo,"id=10",null); //更新一条记录
// dao.deleteData(vo); //删除一条记录
// dao.deleteDatas("id>5",null); //添加符合条件记录
}
}
学习JDBC以来一直想实现一个简单的封装来方便编程但是由于水平有限一直没有较好的办法,看了IBM开发网上的两篇文章以后感觉作者的设计思想很好一定能扩充成一个实用的JDBC封装。所以我在文章提供的源码基础上加了一些功能这些功能包括支持多种数据类型,处理了空值,利用反射方便的在Row对象和值对象之间进行转换,还有加了一个我自认为通用的DAO类来方便用户的操作。
我把源码提供出来有两个目的一个是希望能帮助比我还初学的初学者熟悉JDBC,另外就是请各位高手不吝赐教,改进程序中的错误如果能将你们的对JDBC的封装方法提供出来那就更好了(不要说你们只用EJB或者Hibernate,JDO什么的?)。
IBM开发网的那两篇文章分别是《一个简单的 JDBC 包装器》《对一个简单的 JDBC 包装器的扩展及应用》,我的邮箱是xsimple2003@yahoo.com.cn有事请与我联系。
设计思想
? 把DBMS抽象成类Database,这个类负责管理数据库连接以及提供表对象。
? 把数据库中的一张或多张表抽象成类Table,这个类中提供对表的添加,修改,删除的JDBC封装。
? 将数据库表中的一条记录抽象成类Row,这个类用HashMap保存关系数据库中表格中一行数据的字段名和值并提供一些相关操作。另外这个类还提供了两个静态方法用于在Row对象和ValueObject之间进行方便的转换。
? 把对个Row的集合抽象成RowSet,这个类中用一个vector把多个Row对象保存起来并提供一些相关操作。
代码分析
由于已经给出源码所以我只对代码中关键的和需要注意的地方加以说明,大家可以执行源码一边演示一边体会。
? Database类源码如下:
package com.gdrj.util.database;
import java.sql.*;
import javax.sql.*;
import com.gdrj.util.servicelocator.*;
public class Database {
/**
* 这个数据库连接成员只有在与数据库直接建立连接的情况下是有效的
*/
private Connection conn = null;
/**
* 当这个参数有效时,表明程序是直接与数据库建立的连接而不是从连接池里取得连接
*/
private String url, user, password;
/**
* 当这个参数有效时,表明程序是从连接池里取得连接。
*/
private String datasource;
/**
* 用数据库地址,用户名,密码初始化数据库对象,这个构造器用于程序是直接
* 与数据库建立连接的情况。
* @param url
* @param user
* @param password
*/
public Database(String url, String user, String password) {
this.url = url;
this.user = user;
this.password = password;
}
/**
* 用JNDI数据源名初始化数据库对象,这个构造器用于从连接池取数据库连接的情况。
* @param datasource
*/
public Database(String datasource) {
this.datasource = datasource;
}
/**
* 得到数据库连接,对于是否从连接池里取连接做了自动处理即根据用户调用了哪个构造器
* 来判断是否直接与数据库建立连接还是从连接池里取连接。
* 对于用户来说不用考虑程序是从那里取得连接,他只管正确的初始化数据库对象。
* @return
* @throws SQLException
*/
public Connection getConnection() throws Exception {
if (datasource == null) { //直接与数据库建立连接
if (conn == null) {
conn = DriverManager.getConnection(url, user, password);
}
}
else { //从应用服务器的连接池里取得连接
ServiceLocator sl = ServiceLocator.getInstance();
DataSource ds = sl.getDataSource(datasource);
return ds.getConnection();//每调用一次都返回一个连接池中的数据库连接
}
return conn;
}
/**
* 释放连接,如果是直接与数据库连接的情况则什么也不做
* 如果是从连接池中取得的连接那么释放传来的连接
* @param conn
*/
public void disConnect(Connection connection) {
if (datasource != null) { //只处理从连接池取连接的情况
try {
if (connection != null) {
connection.close();
}
}
catch (Exception ex) {}
}
}
/**
* 得到与参数名对应的表对象,注意这里不作任何数据库操作
* @param name
* @return
*/
public Table getTable(String name) {
return new Table(this, name);
}
}
这个类是对DBMS的抽象,所以使用时应用程序中只要有一个Database对象就够了,如果你是以与数据库之间建立连接的方式使用那么你用Database(String url, String user, String password)构造器进行初始化。如果是从应用服务器的连接池中取得连接的方式使用那么用Database(String datasource)构造器初始化,这样以后你使用这个对象进行getConnection和disConnection时就不用去考虑始终保持一个连接(C/S方式),还是将连接返回连接池了因为在disConnection中已经做了处理。集体使用方法将Table类。在getConnection中的从连接池中取连接的代码你只要参考以下《J2EE核心模式》中的服务定位器模式就知道是怎么回事了,你在用Database(String url, String user, String password)初始化时其中的代码不起作用。
? Table类源码如下:
package com.gdrj.util.database;
import java.sql.*;
import java.util.*;
import com.gdrj.util.*;
public class Table {
/**
* 通过这个数据库对象得到数据库连接
*/
private Database database;
/**
* 数据库中一个或多个(只限查询)表的名
*/
private String name;
/**
* 初始化表对象,此时不作任何数据库相关操作
* 一般通过database的getTable调用
* @param database
* @param name
*/
public Table(Database database, String name) {
this.database = database;
this.name = name;
}
/**
* 查询某一行
* @return
*/
public Row getRow(String fields, String criteria, Object[] args) throws
DBAccessException {
RowSet rows = executeQuery(fields, criteria, args);
if (rows == null) {
return null;
}
return rows.get(0);
}
/**
* 得到一个多行记录
* @param criteria 查询条件
* @param args 查询条件的参数列表
* @return
*/
public RowSet getRows(String fields, String criteria, Object[] args) throws
DBAccessException {
return executeQuery(fields, criteria, args);
}
/**
* 执行SQL查询
* @param fields 要查询的字段,如果传入null则表示查询表中所有字段
* @param criteria用户输入的查询Where条件
* @param args 用到的参数数组
* @return 返回符合结果行集
*/
private RowSet executeQuery(String fields, String criteria, Object[] args) throws
DBAccessException {
Connection conn = null;
RowSet rows = new RowSet();
String sql = null;
if (fields == null) {
fields = "*";
}
try {
conn = database.getConnection(); //取得数据库连接,在方法内部对不同的连接情况进行了处理
sql = "select " + fields + " from " + name +
( (criteria == null) ? "" :
(" where " + criteria));
PreparedStatement pstmt = conn.prepareStatement(sql);
if (args != null) { //如果有查询参数则设置参数
for (int i = 0; i < args.length; i++) {
pstmt.setObject(i + 1, args[i]);
}
}
ResultSet rs = pstmt.executeQuery();
ResultSetMetaData rsmd = rs.getMetaData();
int cols = rsmd.getColumnCount();
/**@todo 判断是否为零*/
if (cols == 0) {
return null;
}
while (rs.next()) {
Row row = new Row();
for (int i = 1; i <= cols; i++) {
String name = rsmd.getColumnName(i);
Object value = rs.getObject(i); //作通用类型处理,这样row中的类型都是Object型的。
/**
* 这里要做空值处理,因为在进行RowToValueObject转换时如果是空值则不能得到值的类型
* 所以如果是空值那么把value设置成类型信息
*/
if (value == null) {
value = Class.forName(rsmd.getColumnClassName(i));
}
// System.out.println(value.getClass());//用于得到数据库中的类型对应Java中的什么类型
row.put(name, value);
}
rows.add(row);
}
rs.close();
pstmt.close();
}
catch (Exception ex) {
throw new DBAccessException(InforGeter.getErrorInfor(this, "executeQuery",
ex, "执行SQL(" + sql + ")查询时出错!"));
}
finally {
database.disConnect(conn); //调用数据库对象的释放连接方法(此方法内对取得连接方式的不同情况做了处理)
}
return rows;
}
/**
* 增加一行
* @param row
*/
public int putRow(Row row) throws DBAccessException {
return putRow(row, null, null);
}
/**
* 修改一行(没有条件就是增加)
* @param row
* @param conditions
*/
public int putRow(Row row, String conditions, Object[] args) throws
DBAccessException {
String ss = "";
int affectableRow = 0; //执行SQL后影响的行数
if (conditions == null) {
ss = "INSERT INTO " + name + "(";
for (int i = 0; i < row.length(); ++i) {
String k = row.getKey(i);
ss += k;
if (i != row.length() - 1) {
ss += ", ";
}
}
ss += ") VALUES (";
for (int j = 0; j < row.length(); ++j) {
ss += (row.get(j) == null) ? "null" : "?"; //如果row中有空值则设置为null,否则设置为查询参数
if (j != row.length() - 1) {
ss += ", ";
}
}
ss += ")";
}
else {
ss = "UPDATE " + name + " SET ";
for (int i = 0; i < row.length(); ++i) {
String k = row.getKey(i);
ss += k + "=" + ( (row.get(i) == null) ? "null" : "?"); //设置查询参数
if (i != row.length() - 1) {
ss += ", ";
}
}
ss += " WHERE ";
ss += conditions;
}
Connection conn = null;
try {
conn = database.getConnection();
PreparedStatement st = conn.prepareStatement(ss);
int j = 0; //查询参数计数器
for (int i = 0; i < row.length(); i++) {
if (row.get(i) != null) { //如果不是空则解析查询参数
st.setObject(++j, row.get(i)); //解析查询参数
}
}
if (args != null) {
for (int i = 0; i < args.length; i++) {
st.setObject(++j, args[i]);//预定的规则,null不能放到查询参数中要以name=null的静态形式存放
}
}
affectableRow = st.executeUpdate();
st.close();
}
catch (Exception ex) {
ex.printStackTrace();
throw new DBAccessException(InforGeter.getErrorInfor(this, "putRow", ex,
"更新表" + name + "中的数据时出错!"));
}
finally {
database.disConnect(conn);
}
return affectableRow;
}
/**
* 删除一行
* @param row
*/
public int delRow(Row row) throws DBAccessException {
String ss = "";
int affectableRow = 0;
ss = "delete from " + name + " where ";
for (int i = 0; i < row.length(); ++i) {
String k = row.getKey(i);
ss += k + ((row.get(i) == null)?" is null":"=?"); //设置查询参数有空值处理
if (i != row.length() - 1) {
ss += " and ";
}
}
Connection conn = null;
try {
conn = database.getConnection();
PreparedStatement st = conn.prepareStatement(ss);
int j = 0;//查询参数计数器
for (int i = 0; i < row.length(); i++) {
if (row.get(i) != null) {
st.setObject(++j, row.get(i)); //解析查询参数
}
}
affectableRow = st.executeUpdate();
st.close();
}
catch (Exception ex) {
throw new DBAccessException(InforGeter.getErrorInfor(this, "delRow", ex,
"删除表" + name + "中的数据时出错!"));
}
finally {
database.disConnect(conn);
}
return affectableRow;
}
/**
* 有条件的删除即删除多行
* @param condition
* @param args
*/
public int delRow(String condition, Object[] args) throws DBAccessException {
String ss = "";
int affectableRow = 0;
ss = "delete from " + name + " where ";
ss += condition;
Connection conn = null;
try {
conn = database.getConnection();
PreparedStatement st = conn.prepareStatement(ss);
if (args != null) {
for (int i = 0; i < args.length; i++) {
st.setObject(i + 1, args[i]);
}
}
affectableRow = st.executeUpdate();
st.close();
}
catch (Exception ex) {
throw new DBAccessException(InforGeter.getErrorInfor(this, "delRow", ex,
"删除表" + name + "中的数据时出错!"));
}
finally {
database.disConnect(conn);
}
return affectableRow;
}
}
使用时可以用Database对象的getTable方法传入数据库表的名称来得到一个Table对象。得到这个对象后就可以对这个数据库表进行操作了,这个类提供了六个方法根据传过来的参数对数据库表进行添加修改删除操作。代码中没有特别难懂的地方,需要注意的是我在原有代码的基础上对空值进行的处理,在查询时如果表中的数据是空值的话那么我把字段对应的Java类型放到Row对象里,因为在进行Row对象到值对象的转换时用到了java反射API必须知道Row中的字段值的类型才能去调用值对象的setXXXX方法(见Row对象的toValueObject方法)。
? 行对象的源码如下:
package com.gdrj.util.database;
import java.util.*;
import java.math.BigDecimal;
import java.lang.reflect.*;
public class Row {
/**
* 排序,由于Hashtable不提供通过索引取得值的方法,并且其中的键值对也不是按照put上去时的顺序排列的。
* 注意:Vector中加入的对象是有序的,即按加入的顺序排列并且能够根据索引访问,可以看成是可变大小的数组
* List可以取代Vector
*/
private Vector ordering = new Vector();
/**
* 存放键值对(表中字段名称与字段值)
*/
private HashMap map = new HashMap();
public Row() {
}
/**
* 向HashMap中追加键值对,即字段名称与字段值
* @param name
* @param value
*/
public void put(String name, Object value) {
if (!map.containsKey(name)) {
ordering.addElement(name); //将键保存起来
}
map.put(name, value);
}
/**
* 得到行对象中字段的个数
* @return
*/
public int length() {
return map.size();
}
/**
* 根据字段名称取得字段值
* @param name
* @return
*/
public Object get(String name) {
return map.get(name);
}
/**
* 根据字段在HashMap中的编号取得字段值
* @param which
* @return
*/
public Object get(int which) {
String key = (String) ordering.elementAt(which);
return map.get(key);
}
/**
* 根据字段序号取得字段名称
* @param which
* @return
*/
public String getKey(int which) {
String key = (String) ordering.elementAt(which);
return key;
}
/**
* 打印,用于调试
*/
public void dump() {
for (Iterator e = map.keySet().iterator(); e.hasNext(); ) {
String name = (String) e.next();
Object value = map.get(name);
System.out.print(name + "=" + value + ", ");
}
System.out.println("");
}
/**
* 将行对象转换成值对象
* @param row
* @param type值对象类型
* @return
* @throws java.lang.Exception 这里的异常一般在DAO中处理,因为DAO调用
* 这个方法进行Row和ValueObject的转换
*/
public static Object toValueObject(Row row, Class type) throws Exception {
Object vo = type.newInstance(); //创建一个值对象
Field[] fields = type.getDeclaredFields(); //得到值对象中所有字段
for (int i = 0; i < fields.length; i++) {
String name = fields[i].getName(); //得到JavaBean的字段名
String nameInRow = toInRowName(name);//在此进行值对象名称到行对象名称的转换
Object value = row.get(nameInRow); //得到从数据库中取出的与字段名对应的值
String methodName = "set" + Character.toUpperCase(name.charAt(0)) +
name.substring(1); //得到setXXXX方法名
Class argClass = null;
if (value instanceof Class) {
argClass = (Class)value;
value = null;
}else{
argClass = value.getClass();
}
Method method = type.getMethod(methodName, new Class[] {argClass}); //得到set方法
method.invoke(vo, new Object[] {value});//调用setXXXX方法
}
return vo;
}
/**
* 根据传过来的值对象和类型把值对象转换到行对象中
* @param vo
* @return
* @throws java.lang.Exception 这里的异常一般在DAO中处理,因为DAO调用
* 这个方法进行Row和ValueObject的转换
*/
public static Row fromValueObject(Object vo) throws Exception {
Row row = new Row();
Class type = vo.getClass(); //得到Class用于进行反射处理
Field[] fields = type.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
String name = fields[i].getName();
String methodName = "get" + Character.toUpperCase(name.charAt(0)) +
name.substring(1);
Method method = type.getMethod(methodName, new Class[] {});
Object value = method.invoke(vo, new Object[] {});
String nameInRow = toInRowName(name);//在此进行值对象中的名称向行对象中的名称转换
row.put(nameInRow, value);
}
return row;
}
/**
* 将值对象中属性名转换成对应的行对象中的字段名(因为行对象中的字段名
* 在更新数据库时必须与数据库表中字段名完全匹配)
* 一般规则为 fsiId ---> fsi_id(现在假设的情况是如果出现有两个单词
* 以上的值对象属性名则数据库表中的字段名一定是有下划线的)
* @param voName
* @return
*/
public static String toInRowName(String voName) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < voName.length(); i++) { //遍历voName如果有大写字母则将大写字母转换为_加小写
char cur = voName.charAt(i);
if (Character.isUpperCase(cur)) {
sb.append("_");
sb.append(Character.toLowerCase(cur));
}
else {
sb.append(cur);
}
}
return sb.toString();
}
}
Row对象中用了一个HashMap对象存放着对应数据库表中的字段名和对应值,由于Map对象的无序性,所以用了一个vector(当然也可以用List代替)来存放字段名(按用户添加的顺序)这样就可以提供get(int i)方法来顺序取得Map中的值了。要注意的是三个静态辅助方法toValueObject,fromVauleObject,toInRowName。toValueObject方法用于将一个行对象转换为值对象方法中利用了Java的多态和反射机制(请大家参考反射API)。FromValueObject是上一个方法的逆操作,toInRowName方法是实现值对象中的属性名向数据库表中字段名的转换,因为一般在数据库建表时是用的这种形式stu_id,而Java中JavaBean的属性是这样的stuId。
? RowSet的代码如下:
package com.gdrj.util.database;
import java.util.*;
public class RowSet {
private Vector vector = new Vector();
public RowSet() {
}
public void add(Row row) {
vector.addElement(row);
}
public int length() {
return vector.size();
}
public Row get(int which) {
if (length() < 1) {
return null;
}
else {
return (Row) vector.elementAt(which);
}
}
public void dump() {
for (Enumeration e = vector.elements(); e.hasMoreElements(); ) {
( (Row) e.nextElement()).dump();
}
}
}
这个类就是把Row对象放到Vector以便操作。就不多说了。
? 为了方便使用我写了一个GeneralDAO类(我对DAO模式还在理解中请各位高手批评指教)代码如下:
package com.gdrj.util.database;
import java.util.*;
public class GeneralDAO {
/**
* 这个DAO对应的表对象
*/
private Table table;
/**
* 默认构造函数
*/
public GeneralDAO() {
}
/**
* 用数据库对象和表名初始化DAO
* @param db
* @param tableName
*/
public GeneralDAO(Database db, String tableName) {
getTable(db, tableName);
}
private void getTable(Database db, String name) {
table = db.getTable(name);
}
/**
* 根据条件将查找到的数据以值对象集合的形式返回
* @param fields 要查找的字段(*或null表示所有字段)
* @param criteria查询条件
* @param args与查询条件对应的参数数组
* @param voType值对象的类型
* @return
* @throws java.lang.Exception
*/
public Collection findDatas(String fields, String criteria, Object[] args,
Class voType) throws Exception {
RowSet rows = table.getRows(fields, criteria, args);
Collection col = new ArrayList();
for (int i = 0; i < rows.length(); i++) {
Object vo = Row.toValueObject(rows.get(i), voType); //返回一个值对象,注意是voType类型的对象
col.add(vo);
}
return col;
}
/**
* 向表中插入一条数据
* @param vo 与表对象对应的值对象
* @return
* @throws java.lang.Exception
*/
public int insertData(Object vo) throws Exception {
return table.putRow(Row.fromValueObject(vo));
}
/**
* 更新一条数据
* @param vo 与表对象对应的值对象
* @param criteria更新条件
* @param args与更新条件对应的参数数组
* @return
* @throws java.lang.Exception
*/
public int updateData(Object vo, String criteria, Object[] args) throws
Exception {
return table.putRow(Row.fromValueObject(vo), criteria, args);
}
/**
* 删除一条数据(条件比较严格各个字段的值必须与值对象中属性的值匹配)
* @param vo
* @return
* @throws java.lang.Exception
*/
public int deleteData(Object vo) throws Exception {
return table.delRow(Row.fromValueObject(vo));
}
/**
* 删除多条数据
* @param condition 删除条件
* @param args与条件对应的参数数组
* @return
* @throws java.lang.Exception
*/
public int deleteDatas(String condition, Object[] args) throws Exception {
return table.delRow(condition, args);
}
}
这个DAO类是对Table类的一个方便的封装。用户如果向操作数据库只要创建一个Database对象,一个DAO对象,一个值对象(对应表结构),然后就可以进行方便的数据库操作了,下面给出一个实例来演示这个小小框架的用法。
演示程序
首先建立一个teacher表,语法如下
create table teacher (
id int not null,
name varchar(20) not null,
birthday smalldatetime null,
address varchar(100) null,
income money null,
constraint id PRIMARY KEY NONCLUSTERED ( id )
)
然后建立一个与teacher表对应的值对象类。
public class TeacherVO implements Serializable {
private Integer id;
private String name;
private String address;
private BigDecimal income;
private java.sql.Timestamp birthday;
public TeacherVO() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public java.sql.Timestamp getBirthday() {
return birthday;
}
public void setBirthday(java.sql.Timestamp birthday) {
this.birthday = birthday;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public java.math.BigDecimal getIncome() {
return income;
}
public void setIncome(java.math.BigDecimal income) {
this.income = income;
}
public String toString(){
return " 编号:" + id + " 姓名:" + name + " 生日:" + birthday
+ " 地址:" + address + " 收入:" + income;
}
}
最后主程序的源码如下:
package org.together.jdbcwrap.test;
import java.util.*;
import com.gdrj.util.database.*;
public class GeneralDAOExample {
public static void main(String[] args)throws Exception {
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Database db = new Database("jdbc:odbc:emmis","sa","815023");
GeneralDAO dao = new GeneralDAO(db,"teacher");
/**
* 利用GeneralDAO进行查询
*/
Collection col = dao.findDatas("*","birthday is null",null,TeacherVO.class);
for (Iterator iter = col.iterator(); iter.hasNext(); ) {
Object item = iter.next();
System.out.println("item = " + item);
}
/**
* 利用GeneralDAO进行添加
*/
TeacherVO vo = new TeacherVO();
vo.setAddress("沈阳");
vo.setBirthday(new java.sql.Timestamp(0));
vo.setId(new Integer(11));
vo.setIncome(new java.math.BigDecimal(1000));
vo.setName("陶小川");
// dao.insertData(vo); //添加一条记录
// dao.updateData(vo,"id=10",null); //更新一条记录
// dao.deleteData(vo); //删除一条记录
// dao.deleteDatas("id>5",null); //添加符合条件记录
}
}
相关推荐
总结,这个JSP数据库操作框架通过面向对象的设计,实现了数据库连接的管理、表操作的封装,以及数据行和数据集的抽象,降低了开发复杂性,提高了代码可读性和复用性。同时,它也体现了良好的软件设计原则和实践,为...
从提供的压缩包文件名"upfor-juggler-7b1f55f"来看,这可能是一个名为Juggler的PHP数据库框架的版本。尽管没有具体代码可供分析,但可以推测Juggler可能包含了一些上述特性,如查询构建、ORM、事务处理等,旨在简化...
XAndrDB数据库操作框架:基于Android开发,包含48个文件,包括34个...该项目是一个安卓端的数据库操作框架,实现了ORM(对象关系映射),旨在为开发者提供一个简单、易用的数据库操作解决方案,类似于安卓端的MyBatis。
以上就是一个简单的Android数据库完美框架,通过这样的封装,可以使数据库操作更加简洁、易于维护。在实际项目中,还可以考虑添加事务处理、异步操作等功能,以提高性能和用户体验。在`DemoDateBase`压缩包中,应该...
创建一个继承自DaoSession的类,这个类将包含所有数据库操作的方法。 4. 初始化GreenDAO。在应用启动时,使用DaoMaster.DevOpenHelper创建并升级数据库,然后通过DaoSession管理数据库操作。 基本操作包括: 1. ...
GYDataCenter 是一个 SQLite 数据库框架,提供了一套简单易用的面向对象的数据操作接口,同时保留了 SQL 查询的灵活性。GYDataCenter 简单易上手,相对于 CoreData,GYDataCenter 的学习成本更低。
本项目是一个基于Python语言的PostgreSQL数据库操作框架设计源码,包含27个文件,包括16个Python源文件、2个Markdown文档、2个ReStructuredText文件、2个批处理脚本、1个Git忽略规则文件、1个许可证文件、1个DTD文件...
总的来说,这个基于Go语言仿GORM的数据库操作框架提供了一个抽象层,使数据库操作变得更加简单。学习和使用这个框架,不仅需要熟悉Go语言,还需要理解数据库原理和ORM的工作方式。同时,开发者应当关注性能优化和...
在Python web开发中,Flask是一个轻量级的Web服务程序框架,而MySQL是一种广泛使用的开源关系型数据库管理系统。本示例将介绍如何在Flask框架中操作MySQL数据库,包括数据库连接、表格创建、数据的增删改查等基本...
4. 查询数据:使用查询方法,可以基于各种条件获取数据,返回的结果通常是一个对象列表。 5. 更新和删除:同样,通过对象的方法或框架的API,实现数据的更新和删除操作。 6. 处理事务:如果需要,可以使用框架的...
【标题】"自己动手模仿Hibernate写数据库框架"揭示了本次讨论的核心内容——尝试构建一个类似于Hibernate的数据库操作框架。Hibernate是一个流行的Java ORM(对象关系映射)框架,它简化了数据库与Java对象之间的...
本知识点主要聚焦于一个专为Android设计的数据库操作框架,该框架简化了数据库的创建、管理以及对数据的增删改查操作,并且支持数据库的分库和升级功能。下面我们将深入探讨这些核心概念。 1. **SQLite数据库**: ...
它允许开发者以Java对象的方式操作SQLite数据库,极大地简化了数据库操作。接下来,我们将深入探讨GreenDao的使用方法及其核心特性。 1. **GreenDao简介** GreenDao是由德国的greenrobot团队开发的,它的主要特点...
3. GreenDao:GreenDao是另一个流行的Android ORM框架,它直接将Java对象映射到SQLite表,提供了高效的性能。开发者需要生成 Dao 和 Entity 类,然后通过这些类与数据库进行交互,减少手动编写SQL的繁琐工作。 4. ...
总之,OrmLite作为Android开发中的一个ORM框架,为开发者提供了便捷的数据库操作方式,使得在处理数据时可以更专注于业务逻辑,而非繁琐的SQL语法。通过良好的设计和适当的扩展,它可以有效地提高开发效率并降低维护...
在这个特定的项目中,"nodejs做的一个树形菜单框架和数据库文件"是一个利用Node.js构建的系统,它实现了树形菜单结构,这种结构常用于Web应用程序的左侧导航栏,以组织和展示多级的菜单项。用户可以通过点击这些菜单...