分享
 
 
 

一个简单实用的数据库操作框架

王朝java/jsp·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

前言

这个小小的数据库操作封装框架是参考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); //添加符合条件记录

}

}

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有