有什么好的支持多种数据库的ORM框架

有什么好的支持多种数据库的ORM框架,第1张

1、传统的建表方式

其实为了方便我们对数据库表进行管理,Android本身就提供了一个帮助类:SQLiteOpenHelper。这个类集创建和升级数据库于一身,并且自动管理了数据库版本,算是一个非常好用的工具。

那我们现在就来试试SQLiteOpenHelper的用法吧。首先你要知道SQLiteOpenHelper是一个抽象类,这意味着如果我们想要使用它的话,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper中有两个抽象方法,分别是onCreate()和onUpgrade(),我们必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。本篇文章只需要把注意力放在创建数据库这里就行了,升级数据库我们会在下一篇文章中去讨论。

新建一个MySQLiteHelper类并让它继承SQLiteOpenHelper,这样一个最基本的数据库帮助类的代码如下所示:

[java] view plain copy

public class MySQLiteHelper extends SQLiteOpenHelper {

public MySQLiteHelper(Context context, String name, CursorFactory factory,

int version) {

super(context, name, factory, version)

}

@Override

public void onCreate(SQLiteDatabase db) {

}

@Override

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

}

}

其中,当数据库创建的时候会调用onCreate()方法,在这里去执行建表 *** 作就可以了。比如说我们想新建一张news表,其中有title,content,publishdate,commentcount这几列,分别代表着新闻标题、新闻内容、发布时间和评论数,那么代码就可以这样写:

[java] view plain copy

public class MySQLiteHelper extends SQLiteOpenHelper {

public static final String CREATE_NEWS = "create table news ("

+ "id integer primary key autoincrement, "

+ "title text, "

+ "content text, "

+ "publishdate integer,"

+ "commentcount integer)"

public MySQLiteHelper(Context context, String name, CursorFactory factory,

int version) {

super(context, name, factory, version)

}

@Override

public void onCreate(SQLiteDatabase db) {

db.execSQL(CREATE_NEWS)

}

...

}

可以看到,我们把建表语句定义成了一个常量,然后在onCreate()方法中去执行了这条建表语句,news表也就创建成功了。这条建表语句虽然简单,但是里面还是包含了一些小的细节,我来解释一下。首先,根据数据库的范式要求,任何一张表都应该是有主键的,所以这里我们添加了一个自增长的id列,并把它设为主键。然后title列和content列都是字符串类型的,commentcount列是整型的,这都很好理解,但是publishdate列该怎么设计呢?由于SQLite中并不支持存储日期这种数据类型,因此我们需要将日期先转换成UTC时间(自1970年1月1号零点)的毫秒数,然后再存储到数据库中,因此publishdate列也应该是整型的。

现在,我们只需要获取到SQLiteDatabase的实例,数据库表就会自动创建了,如下所示:

[java] view plain copy

SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1)

SQLiteDatabase db = dbHelper.getWritableDatabase()

LitePal的基本用法

虽说LitePal宣称是近乎零配置,但也只是“近乎”而已,它还是需要进行一些简单配置才可以使用的,那么我们第一步就先快速学习一下LitePal的配置方法。

快速配置

1. 引入Jar包或源码

首先我们需要将LitePal的jar包引入到项目当中,可以点击这里查看LitePal的最新版本,选择你需要的下载即可。下载好了jar包之后,把它复制到项目的libs目录中就算是引入成功了,如下图所示:

如果你不想用jar包的话,也可以把LitePal的源码下载下来,然后作为一个library库导入到Eclipse当中,再让我们的项目去引用这个library库就可以了。

2. 配置litepal.xml

接着在项目的assets目录下面新建一个litepal.xml文件,并将以下代码拷贝进去:

[java] view plain copy

<?xml version="1.0" encoding="utf-8"?>

<litepal>

<dbname value="demo" ></dbname>

<version value="1" ></version>

<list>

</list>

</litepal>

配置文件相当简单,<dbname>用于设定数据库的名字,<version>用于设定数据库的版本号,<list>用于设定所有的映射模型,我们稍后就会用到。

3. 配置LitePalApplication

由于 *** 作数据库时需要用到Context,而我们显然不希望在每个接口中都去传一遍这个参数,那样 *** 作数据库就显得太繁琐了。因此,LitePal使用了一个方法来简化掉Context这个参数,只需要在AndroidManifest.xml中配置一下LitePalApplication,所有的数据库 *** 作就都不用再传Context了,如下所示:

[java] view plain copy

<manifest>

<application

android:name="org.litepal.LitePalApplication"

...

>

...

</application>

</manifest>

当然,有些程序可能会有自己的Application,并在这里配置过了。比如说有一个MyApplication,如下所示:

[java] view plain copy

<manifest>

<application

android:name="com.example.MyApplication"

...

>

...

</application>

</manifest>

没有关系,这时只需要修改一下MyApplication的继承结构,让它不要直接继承Application类,而是继承LitePalApplication类,就可以使用一切都能正常工作了,代码如下所示:

[java] view plain copy

public class MyApplication extends LitePalApplication {

...

}

但是,有些程序可能会遇到一些更加极端的情况,比如说MyApplication需要继承另外一个AnotherApplication,并且这个AnotherApplication还是在jar包当中的,不能修改它的代码。这种情况应该算是比较少见了,但是如果你遇到了的话也不用急,仍然是有解释方案的。你可以把LitePal的源码下载下来,然后把src目录下的所有代码直接拷贝到你项目的src目录下面,接着打开LitePalApplication类,将它的继承结构改成继承自AnotherApplication,再让MyApplication继承自LitePalApplication,这样所有的Application就都可以在一起正常工作了。

仅仅三步,我们就将所有的配置工作全部完成了,并且这是一件一本万利的事情,自此以后,你就可以开心地体验LitePal提供的各种便利了,就让我们从建表开始吧。

开始建表

前面在介绍的时候已经说了,LitePal采取的是对象关系映射(ORM)的模式,那么什么是对象关系映射呢?简单点说,我们使用的编程语言是面向对象语言,而我们使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射了。

但是我们为什么要使用对象关系映射模式呢?这主要是因为大多数的程序员都很擅长面向对象编程,但其中只有少部分的人才比较精通关系型数据库。而且数据库的SQL语言晦涩难懂,就算你很精通它,恐怕也不喜欢经常在代码中去写它吧?而对象关系映射模式则很好地解决了这个问题,它允许我们使用面向对象的方式来 *** 作数据库,从而可以从晦涩难懂的SQL语言中解脱出来。

那么接下来我们就看一看LitePal中是如何建表的吧。根据对象关系映射模式的理念,每一张表都应该对应一个模型(Model),也就是说,如果我们想要建一张news表,就应该有一个对应的News模型类。新建一个News类,如下所示:

[java] view plain copy

package com.example.databasetest.model

public class News {

}

然后,表中的每一列其实就是对应了模型类中的一个字段,比如news表中有id、title、content、publishdate、commentcount这几个列,那么在News类中就也应该有这几个字段,代码如下所示:

[java] view plaincopy

[java] view plain copy

public class News {

private int id

private String title

private String content

private Date publishDate

private int commentCount

// 自动生成get、set方法

...

}

其中id这个字段可写可不写,因为即使不写这个字段,LitePal也会在表中自动生成一个id列,毕竟每张表都一定要有主键的嘛。

这里我要特别说明一下,LitePal的映射规则是非常轻量级的,不像一些其它的数据库框架,需要为每个模型类单独配置一个映射关系的XML,LitePal的所有映射都是自动完成的。根据LitePal的数据类型支持,可以进行对象关系映射的数据类型一共有8种,int、short、long、float、double、boolean、String和Date。只要是声明成这8种数据类型的字段都会被自动映射到数据库表中,并不需要进行任何额外的配置。

那么有的朋友可能会问了,既然是自动映射的话,如果News类中有一个字符串字段我并不想让它映射到数据库表中,这该怎么办呢?对此,LitePal同样采用了一种极为轻量的解决方案,只有声明成private修饰符的字段才会被映射到数据库表中,如果你有某一个字段不想映射的话,只需要将它改成public、protected或default修饰符就可以了。

现在模型类已经建好了,我们还差最后一步,就是将它配置到映射列表当中。编辑assets目录下的litepal.xml文件,在<list>标签中加入News模型类的声明:

[java] view plain copy

<?xml version="1.0" encoding="utf-8"?>

<litepal>

<dbname value="demo" ></dbname>

<version value="1" ></version>

<list>

<mapping class="com.example.databasetest.model.News"></mapping>

</list>

</litepal>

注意这里一定要填入News类的完整类名。

OK,这样所有的工作就都已经完成了,现在只要你对数据库有任何的 *** 作,news表就会被自动创建出来。比如说LitePal提供了一个便捷的方法来获取到SQLiteDatabase的实例,如下所示:

[java] view plain copy

SQLiteDatabase db = Connector.getDatabase()

好了,到目前为止你已经算是对LitePal的用法有点入门了,那么本篇文章的内容就到这里,下篇文章当中我们将学习使用LitePal进行升级表的 *** 作。感兴趣的朋友请继续阅读 Android数据库高手秘籍(三)——使用LitePal升级表 。

学习JDBC以来一直想实现一个简单的封装来方便编程但是由于水平有限一直没有较好的办法,看了IBM开发网上的两篇文章以后感觉作者的设计思想很好一定能扩充成一个实用的JDBC封装。所以我在文章提供的源码基础上加了一些功能这些功能包括支持多种数据类型,处理了空值,利用反射方便的在Row对象和值对象之间进行转换,还有加了一个我自认为通用的DAO类来方便用户的 *** 作。

我把源码提供出来有两个目的一鍪窍M�馨镏�任一钩跹У某跹д呤煜�DBC,另外就是请各位高手不吝赐教,改进程序中的错误如果能将你们的对JDBC的封装方法提供出来那就更好了(不要说你们只用EJB或者Hibernate,JDO什么的?)。

设计思想

把DBMS抽象成类Database,这个类负责管理数据库连接以及提供表对象。

把数据库中的一张或多张表抽象成类Table,这个类中提供对表的添加,修改,删除的JDBC封装。

将数据库表中的一条记录抽象成类Row,这个类用HashMap保存关系数据库中表格中一行数据的字段名和值并提供一些相关 *** 作。另外这个类还提供了两个静态方法用于在Row对象和ValueObject之间进行方便的转换。

把对个Row的集合抽象成RowSet,这个类中用一个vector把多个Row对象保存起来并提供一些相关 *** 作。

代码分析

由于已经给出源码所以我只对代码中关键的和需要注意的地方加以说明,大家可以执行源码一边演示一边体会。

Database类源码如下:

package com.gdr

j.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, Stri

ng user, String password)构造器进行初始化。如果是从应用服务器的连接池中取得连接的方式使用那么用Database(String datasource)构造器初始化,这样以后你使用这个对象进行getConnection和disConnection时就挥萌タ悸鞘贾毡3忠桓隽�?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, criter

ia, 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 = 0i <args.lengthi++)

{

pstmt.setO

bject(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 = 1i <= colsi++)

{

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 = 0i <row.length()++i)

{

String k = row.getKey(i)

ss += k

if (i != row.length() - 1)

{

ss += ", "

}

}

ss += ") VALUES ("

for (int j = 0j <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 = 0i <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 = 0i <row.length()i++)

{

if (row.get(i) != null)

{

//如果不是空则解析查询参数

st.setObject(++j, row.get(i))

//解析查询参数

}

}

if (args != null)

{

for (int i = 0i <args.lengthi++)

{

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 = "de

lete from " + name + " where "

for (int i = 0i <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 = 0i <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 co

nn = null

try

{

conn = database.getConnection()

PreparedStatement st = conn.prepareStatement(ss)

if (args != null)

{

�or (int i = 0i <args.lengthi++)

{

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)

}

�eturn 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

{

本文实例讲述了Yii2框架 *** 作数据库的方法。分享给大家供大家参考,具体如下:

准备数据库

DROP

TABLE

IF

EXISTS

`pre_user`

CREATE

TABLE

`pre_user`(

`id`

int(11)

AUTO_INCREMENT

PRIMARY

KEY,

`username`

varchar(255)

NOT

NULL,

`password`

varchar(32)

NOT

NULL

DEFAULT

'',

`password_hash`

varchar(255)

NOT

NULL

DEFAULT

'',

`email`

varchar(255)

NOT

NULL

DEFAULT

'',

`status`

smallint(6)

NOT

NULL

DEFAULT

10,

`created_at`

smallint(6)

NOT

NULL

DEFAULT

0,

`updated_at`

smallint(6)

NOT

NULL

DEFAULT

0

)ENGINE=InnoDB

DEFAULT

CHARSET=utf8mb4

配置连接

config\db.php

<?php

return

[

'class'

=>

'yii\db\Connection',

'dsn'

=>

'mysql:host=localhostdbname=yii2',

'username'

=>

'root',

'password'

=>

'root',

'charset'

=>

'utf8mb4',

'tablePrefix'

=>

'pre_'

]

查看数据库连接是否成功

控制器里打印:

var_dump(\Yii::$app->db)

怎么执行SQL语句?

增删改

//

接收表单的数据

$username

=

'jack'

$sql

=

"INSERT

INTO

{{%user}}

(username,status)

VALUES

(:username,:status)"

//

返回受影响行数

$row

=

\Yii::$app->db->createCommand($sql,['username'=>$username,'status'=>8])->execute()

//

获取自增ID

echo

\Yii::$app->db->getLastInsertID()

查询

$sql

=

"SELECT

*

FROM

{{%user}}

WHERE

id>:id"

//

查询结果是一个二维数组

$userArr

=

\Yii::$app->db->createCommand($sql,['id'=>1])->queryAll()

//

如果要查询一个

$user

=

\Yii::$app->db->createCommand($sql,['id'=>1])->queryOne()

//

如果要返回单值

//

例如

select

count(*)语句

$count

=

\Yii::$app->db->createCommand($sql,['id'=>1])->queryScalar()

echo

$count

更多关于Yii相关内容感兴趣的读者可查看本站专题:《Yii框架入门及常用技巧总结》、《php优秀开发框架总结》、《smarty模板入门基础教程》、《php面向对象程序设计入门教程》、《php字符串(string)用法总结》、《php+mysql数据库 *** 作入门教程》及《php常见数据库 *** 作技巧汇总》

希望本文所述对大家基于Yii框架的PHP程序设计有所帮助。

您可能感兴趣的文章:Yii2.0高级框架数据库增删改查的一些 *** 作Yii2——使用数据库 *** 作汇总(增删查改、事务)Yii2数据库 *** 作常用方法小结Yii2框架实现数据库常用 *** 作总结Yii2实现跨mysql数据库关联查询排序功能代码Yii+MYSQL锁表防止并发情况下重复数据的方法Yii

连接、修改

MySQL

数据库及phpunit

测试连接Yii实现MySQL多数据库和读写分离实例分析Yii *** 作数据库实现动态获取表名的方法Yii *** 作数据库的3种方法


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/sjk/9716652.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-01
下一篇 2023-05-01

发表评论

登录后才能评论

评论列表(0条)

保存