用SQLJ开发数据库

80酷酷网    80kuku.com

  数据|数据库

SQLJ的概述

  今天的企业级应用程序通常运行在多数据库平台上,例如Oracle、DB2、Sybase、SQL Server、Informix等等。在这个环境里,代码的可移殖性和可维护性正变得越来越重要。从一个数据库平台到另一个数据库平台移植代码是非常复杂的并且要花费大量时间,因为数据库供应厂商都使用他们自己专有的过程语言(例如Oracle使用PL/SQL,而Sybase和SQL Server使用Transact - SQL)。

  近几年来,Oracle开发人员都一直在使用PL/SQL|(一种提供了到关系数据库语言SQL的过程扩展部分的语言)来构建管理大量的数据阵列的复杂系统。不幸的是,用PL/SQL写的存储过程只能在Oracle数据库运行。但是SQL开发者有了一种写代码的强有力的工具,可以很容易的移植到其他的数据库-这就是Java,因为它在跨平台开发和国际互联网络上的强大功能是它成为流行的开发语言。Java承诺的统一的、可移植的应用软件开发解决办法可以在简单的、低成本的的IT基本设施上执行,所以主要的开发工具供应厂商和设备供应者都支持Java。居于领导地位的软件供应厂商,例如Oracle和IBM,都在他们的数据库和其他的应用程序平台上整合了Java虚拟机(JVM)。ORACLE数据库管理系统公司在Oracle 8i中引入了对Java的扩展支持功能。在Oracle中,有两种使用Java的基本方法:

  JDBC :就像ODBC一样,它提供了一个基于驱动程序的接口,允许从Java应用程序中访问Oracle数据库。

  SQLJ :这是多个厂商共同努力的结果,是一种新的语言,能提供在Java代码中静态SQL的支持。理论上,它提供了比JDBC更大程度的程序员劳动生产率。

  在Oracle 8i数据库服务器里集成JVM是在最近几年中Oracle引进的最重要的技术革新之一。集成JVM的Oracle,叫做JServer(亦称Aurora JVM), 支持两种不同的程序设计模型∶

  和SQL集成,允许用户使用Java编写传统的数据库存储过程、函数和触发器。

  用于分布式Java组件的事务服务器平台,称作企业JavaBeans,允许程序员开发可重复使用的服务器端应用程序组件。

  在本文中,我主要讲述一下在Oracle 8i中SQL和Java集成。也就是说, Java可以调用SQL和PL/SQL,PL/SQL和SQL也可以调用Java。Java程序调用它们使用JDBC驱动程序的SQL和PL/SQL复本,而JDBC驱动程序是嵌入驻留在Oracle 8i数据库中的JVM体系结构中的。另一方面,从SQL和PL/SQL到Java,Oracle 8i提供了二个特色。在Oracle 8i中,Java名称空间映射到数据库模式,易于从属物允许Java被保存在数据库中。Oracle 8i也提供扩展的Data Definition Language(数据定义语言DDL),例如CREATE PROCEDURE AS JAVA命令,因此Java代码很容易内嵌入Oracle 8i中。

  SQLJ是什么?

  SQLJ是一种允许把静态的 SQL语句以文本形式嵌入Java程序中的语言。在写一个SQLJ应用程序时,我们编写一段Java程序然后遵循某些特定的标准法则把SQL语句嵌入在其中,这些法则定义了SQL语句怎样写入Java程序(具体情况请看Oracle 8i SQLJ开发指南与参考,你可以在Documentation Library光盘上找到)。
接下来,我们运行一个SQLJ翻译器,通过把嵌入式结构化查询语句替换为调用调用SQLJ运行时程序库的方式把SQLJ程序转换成一个标准的Java程序。生成的Java程序就可以使用任何标准的Java编译程序(例如javac)来编译了然后就可以配合数据库使用了。SQLJ运行期环境是由一个瘦(即没有额外系统开销)SQLJ运行时程序库组成,也就是说用纯Java实现调用相应的数据库(Oracle, DB2等等)的JDBC驱动程序。

  SQLJ类似于其他的嵌入式结构化查询语言的实现,像Oracle Pro * C (嵌入C语言环境的SQL)。SQLJ语言设计的目的就是帮助基于Java的程序员构建数据库应用程序。SQLJ是一个ISO和ANSI标准,也就是说由领头的数据库与软件供应厂商开发和支持的,包括ORACLE数据库管理系统公司,国际商业机器公司,美国赛贝斯公司, Informix公司,美国康柏公司等。所有这些公司合作开发兼容的SQLJ翻译器来实现使用不同的数据库。

SQLJ的优点

  一个编译过的SQLJ应用程序是一个标准的Java程序,可以在任何具备三个组件的环境中运行,这三个组件是:JVM, SQLJ运行时程序库和JDBC驱动程序。

  它提供了下列好处∶

  紧凑的代码 SQLJ代码比JDBC代码更加紧凑并且无差错。在编译时对语法和语义进行检查。SQLJ编译器提供了类型检查和模式对象检查来找出在SQL语句中的语法错误或遗漏或拼错这样的错误,这是在编译过程中进行而不是在运行过程中进行。因此,使用SQLJ编写的程序比使用JDBC编写的程序更加健壮。

  多厂商互用性 SQLJ语法是由主要的软件供应厂商开发和支持的。因为SQLJ程序使用运行时JDBC调用访问数据库,所以SQLJ可以访问任何JDBC驱动程序可以实现的数据库服务器。

  灵活的部署 因为SQLJ运行时程序库是基于Java的程序,所以SQLJ应用程序可以在任何JDBC配置环境中配置,例如瘦客户端,中间层或是数据库服务器上等。

  供应厂商具体定制 SQLJ通过后续的Java字节码的定制支持供应厂商具体产品的特色和扩展。它可以被用来改善SQL查询语言的执行性能,使用具体供应厂商提供的性能或功能上的扩展,而不用考虑SQLJ程序如何变化,以及调试和运行记录等情况。
 

  使用SQLJ的开发步骤

  下面是开发和运行一个SQLJ应用程序所需要做的事情:

  使用SQLJ编译器编译SQLJ源文件。这一步生成调用SQLJ运行时的Java文件,以及二进制的SQLJ描述文件--包括存在于SQLJ源文件中的静态SQL语句的有关信息。使用Java编译程序编译Java代码。在编辑完成之后,生成的描述文件是使用特定数据库数据类型,扩展和特征性能定制的。

  运行应用程序,使用SQLJ运行时程序库和特定数据库的JDBC驱动程序。举例来说,如果你的主.sqlj文件定义类MyClass,那么源文件名必须是MyClass.sqlj。编译器生成MyClass.java源文件,然后编译程序生成MyClass.class类文件。而且,翻译器和编译程序两者都生成profile - key类,MyClass_SJProfileKeys.class。翻译器根据你怎样声明它们来命名迭代程序类与连接上下文类。举例来说,如果你声明一个迭代程序,MyIter,将生成一个MyIter.class类文件。

  下面是配置SQLJ的必要条件∶

   SQLJ运行时程序库

   JDBC驱动程序,例如:JDBC/ODBC桥,Oracle JDBC/OCI驱动程序,Oracle瘦JDBC驱动程序,DB2 JDBC驱动程序等等。

   SQLJ程序将执行的JVM

  现在让我们比较SQLJ和JDBC,并比较SQLJ/JDBC和PL/SQL。

  SQLJ 与JDBC

  SQLJ开发的目的是完善动态JDBC SQL结构化查询语言模型和静态的SQL结构化查询语言模型。与ODBC和JDBC动态模型不同,静态模型提供强类型应用程序翻译时间检查。这些不仅要求进行SQL语法的编译时检验和SQL语句使用的主机变量的类型兼容,而且查询本身的正确性与数据库模型中的表,视图,存储过程等等的定义有关。因为所有的SQL语句都要被编译,SQLJ可以作为一个性能更好的中间媒质。

SQLJ代码与JDBC代码

  对于有输入参数的SQL语句,SQLJ类通常比等价的动态的SQL语句( JDBC)调用简短,因为SQLJ运用主机变量把参数传递到SQL语句中,当JDBC要求一个单独的语句把个个参数捆绑起来,检索每个结果。下面是一条SELECT语句的SQLJ代码片断:
 

String vName; int vSalary; String vJob; Java.sql.Timestamp vDate;
...
#sql { SELECT Ename, Sal
INTO :vName, :vSalary
FROM Emp
WHERE Job = :vJob and HireDate = :vDate };

  下面是相同的SELECT语句的JDBC代码碎片:

String vName; int vSalary; String vJob; Java.sql.Timestamp vDate;
...
PreparedStatement stmt = connection.prepareStatement(
"SELECT Ename, Sal " +
"INTO :vName, :vSalary " +
"FROM Emp " +
"WHERE Job = :vJob and HireDate = :vDate");

stmt.setString(1, vJob);
stmt.setTimestamp(2, vDate);

ResultSet rs = stmt.executeQuery();
rs.next();

vName = rs.getString(1);
vSalary = rs.getInt(2);

rs.close();

  我们可以看到,直接在一个Java程序中嵌入SQL语句能够生成比JDBC更加简明易读的代码。因此,SQLJ在Java应用程序有数据库访问需要的时候,减少了开发时间和维修代价。SQLJ程序可以在同一个源文件中很容易地与JDBC代码相互作用来做到动态的SQL语句调用,或者你也可以在SQLJ语句中使用PL/SQL语句块来完成这个目的。此外,Oracle 9i增加了在SQLJ代码直接支持动态SQL的功能。

  Java ( SQLJ和JDBC)与Oracle数据库中的PL/SQL比较:

  Oracle数据库应用程序中的使用的Java还不能够替代PL/SQL。Java和PL/SQL相辅相成,Java ( SQLJ/JDBC)有下列优于PL/SQL的地方:Java能够提供重要的性能优势,Java存储过程要快5到100倍,这主要取决于程序中使用的数学操作符和数据类型。理论上说,Java存储过程可以很容易的转化成运行在其它数据库上的存储过程。Java程序可以在一个复杂的应用程序的任何一层上配置∶在客户端上,在中间层的应用程序服务器上或者在数据库服务器本身中。Java ( SQLJ/JDBC)也同样“分享”了PL/SQL的不足之处:PL/SQL与Oracle数据库服务器紧密地结合起来,Oracle在近20年的时间中不断的改进PL/SQL,而Java只在1998年的时候才被引进Oracle 8i。PL/SQL数据类型等价于Oracle本地数据类型,所以不需要进行数据类型的换算。在另一方面,JDBC提出在Java代码和SQL语句之间插入一个普通的层,而SQLJ是又一个层。PL/SQL工作性能比Java好,因为是以数据库为中心编程:PL/SQL存储过程比Java程序快1.5倍(对于OLTP联机事务处理)到2.5倍(用于批处理)。此外,Java程序要使用比PL/SQL更多的CPU资源。CPU额外开销的增加可能是因为要进行一个比较长的编码过程以及从Oracle到Java的额外的数据类型转换。

  一个两者兼顾达到最好效果的解决方案

  Oracle提供了一个理想的环境用于利用PL/SQL和Java语言两者的优点。在一方面,PL/SQL程序可以调用SQLJ和JDBC存储过程,允许你构建基于组件的EJB和CORBA应用程序。现有的Java类库可以很容易地被利用,并通过使用PL/SQL调用规范来整合入数据库代码开发过程中。在另一方面,Java程序可以通过JDBC或者SQLJ调用PL/SQL存储过程,函数和匿名的程序块。我下面想一一详细介绍,SQLJ提供用于调用下面这些内容的语法:

  存储过程:使用CALL操作符调用UpdateSalary程序∶

#sql { CALL UpdateSalary };
函数:<0} {0>to call the GetName() function using the VALUES operator: <}0{>使用VALUES操作符调用GetName()函数∶<0}
{0>String name; <}0{>String name;<0}
#sql { name = { VALUES GetName() };
or by using the SET operator:
String name;
#sql { SET :name = GetName() };
{0>Anonymous PL/SQL blocks: <}0{>匿名的PL/SQL程序块∶<0}
#sql { [DECLARE ...] BEGIN ... END; };

  需要注意的是影响决定使用什么语言的因素,不仅由执行效果决定--说得更精确些,一个现今应用程序开发过程中的主要因素,而且由程序员劳动生产率、可获得的专家的意见和轻便性决定。幸亏对于数据库开发人员,不必在几种语言中选来选去,你可以很容易地把Java ( SQLJ和JDBC)与PL/SQL存储程序混合搭配进一个数据库来取得一个两者兼顾达到最好效果的解决方案。

SQLJ语言元素

  SQLJ是正在发展的工业标准语言,允许你使用独立于数据库代码的Oracle存储过程,可以很容易地移植到其他的可使用Java的数据库平台。知道连接上下文,迭代程序,可执行语句和主表达式的情况,那么你就可以把一些实际的SQLJ语句用到现实的应用程序中。
 

  在前文中,我描述了SQLJ是什么,比较了PL/SQL和JDBC,并且研究了SQLJ的好处。 在本文中,我将研究一下SQLJ编程语言的基础,这样你就可以在现实的应用程序中使用SQLJ了。SQLJ程序是一个使用嵌入式结构化询问语言语句的规则的Java程序,以一个#_sql标记开始并以一分号结束。有二类SQLJ语句∶声明和可执行语句。声明语句声明了连接上下文和迭代程序。连接上下文用来建立数据库连接,而迭代程序被用来存储由SQL查询返回的结果集;可执行语句执行嵌入式结构化询问语句和PL/SQL程序块。因为SQLJ程序将要被翻译然后通过JDBC运行,任何JDBC驱动程序支持的SQLJ语句可能内嵌在一个SQLJ可执行语句中。可执行语句可能同时包含主表达式,在Java程序和数据库之间通过Java变量交换信息。

  Oracle JDBC驱动程序

  Oracle提供下列JDBC驱动程序∶

  客户端瘦驱动程序是一个100%纯Java驱动程序,用于在没有安装Oracle的客户端。Oracle推荐使用小应用程序。当Java小应用程序运行的时候,它可以下载到浏览器中。

  OCI驱动程序( OCI8和OCI7)是用于安装了Oracle客户端程序的客户端。Oracle JDBC OCI驱动程序通过调用Oracle调用界面( OCI)直接从Java访问数据库,提供与不同版本的Oracle7,Oracle8和8i之间最大的兼容性。这些驱动程序要求Oracle客户程序安装Net8。服务器端瘦驱动程序提供了与客户端瘦驱动程序相同的函数,但是在一个Oracle数据库内运行并且访问一个远程数据库。这对于从一个担任中间层的Oracle服务器上访问远程Oracle服务器是很有用的,或者,更简单一些来说,从一个层内访问另一个Oracle服务器,例如从某一个Java存储过程或者EJB内访问Oracle服务器。

  服务器端内部驱动程序,称为KPRB(Kernel Program Bundled),提供对任何在执行SQL操作的目的Oracle数据库内运行的Java代码的支持。服务器端内部驱动程序允许JServer JVM直接与SQL引擎通信。这是一个默认的用于在Oracle 8i/9i服务器上运行SQLJ代码的JDBC驱动程序,这些SQLJ代码用作存储过程,存储函数,触发器,EJB或CORBA对象。KPRB JDBC驱动程序非常轻便但是效率高,并且在Oracle JServer内运行特别地能够做到尽善尽美。这就是我们要来写SQLJ存储过程所用的驱动程序。让我们来研究一下描述的SQLJ元素,逐一介绍:连接上下文,迭代程序,可执行语句以及主表达式。

  连接上下文用于单一连接,你可以使用DefaultContext类的一个实例并在构造DefaultContext对象时指定数据库URL,用户名和口令。这是使用Oracle公司提供的oracle.sqlj.runtime.Oracle类的connect()方法的最容易的方法。在本例中,我们将要使用JDBC瘦驱动程序以及用户名"scott"和口令"tiger"来通过端口1521连接MYSERVER服务器上的数据库,在这个服务器里,数据库的SID是ORCL∶
Oracle.connect("jdbc:oracle:thinMYSERVER:1521:ORCL", "scott", "tiger");它创建DefaultContext类的一个实例并且把它作为默认连接。并不一定需要直接使用DefaultContext的实例做任何事情。对于多重连接,你可以创建并通过使用Oracle.getConnection ()方法来使用DefaultContext类的辅助实例。在本例子中,你将使用Oracle OCI8驱动程序使用MYSERVER_ORCL作为Oracle服务名,在TNSNames.ora文件中创建做为一个ORCL实例∶

DefaultContext myContext1 = Oracle.getConnection
("jdbc:oracle:oci8MYSERVER_ORCL", "scott", "tiger");
DefaultContext myContext2 = Oracle.getConnection
("jdbc:oracle:oci8MYSERVER_ORCL ", "tom", "bear");

  这段代码创建二个连接上下文实例,它们两个都使用相同的Oracle OCI8驱动程序,但是不同的模式。你可以通过使用为每个语句指定连接模式的方法在两个不同的模式中执行SQL操作。

#sql [myContext1] { SQL statement };
...
#sql [myContext2] { SQL statement };

  在这个程序的结尾,我们需要关闭在FINALLY子句和TRY/CATCH语句块中的连接。

finally
{
#sql [myContext1] { commit };
myContext1.close();

#sql [myContext2] { commit };
myContext2.close();
}
...

迭代程序

  在SQLJ程序中,SQL查询返回的结果集可以作为一个迭代程序对象,用来保存数据。迭代程序对象是迭代程序类的一个实例,并且在概念上类似于PL/SQL游标。我们必须执行下面的五个步骤来使用迭代程序处理SQL查询返回的数据。

   声明迭代程序类。

   从迭代程序类声明一个迭代程序对象。
   使用SELECT语句添加迭代程序对象。

   从迭代程序对象中读取数据。

   关闭迭代程序对象。

  有二种类型的迭代程序类∶

   命名的迭代程序,在这种类型中,Java变量类型和迭代程序列名都必须被指定。

   位置迭代程序,在这个类型中,只有用于从数据库中检索得到的列的Java变量类型需要指定。

  命名迭代程序∶一个命名迭代程序声明指定了列存取器名和它们的Java类型。

  让我们来演示一下使用这个例程的五个步骤,在本例中我们想检索雇员表Emp中的Ename,Job和HireDate列,查找工资大于1500的雇员。

  声明迭代程序类∶

#sql iterator EmpIteratorClass(String Ename, String Job, Timestamp HireDate);

  我们使用Java String类来表示Ename和Job列,因为它兼容Oracle VARCHAR2数据库类型。

  java.sql.Timestamp类型用于HireDate列( Oracle的日期类型),因为java.sql.Date类型只能保存年份,日期和时间信息,而不能象java.sql.Timestamp一样保存小时,分钟和秒。

  从迭代程序类中声明迭代程序对象∶

   EmpIteratorClass empIterator;

  使用SQL SELECT语句添加迭代程序对象。

  下列SQLJ语句把Emp表的Ename,Job和HireDate列中的内容加入empIterator对象∶

int salary = 1500;
#sql empIterator = {
select Ename, Job, HireDate
from Emp
where Sal > :salary
};

  我们还要声明主变量salary,用于WHERE子句中来标识要从Emp表中返回什么数据。

  记住,通过SQL查询返回的数据库列的名称必须对应于第一步中定义迭代程序列名。从迭代程序对象中读取数据。因为迭代程序对象可能包含多个行,所以我们需要使用一个循环来访问每一行数据,就象我们从PL/SQL游标中读取数据一样。命名迭代程序实现一个next()方法,允许你在迭代程序对象的数据之间移动。此外,SQLJ还提供了检索迭代程序列的值得存取方法。下列代码在一个循环中打印出雇员姓名,职务和雇用日期。

while (empIterator.next()) {
System.out.println("Name: " + empIterator.Ename());
System.out.println("Job: " + empIterator.Job());
System.out.println("Hire Date:" +
empIterator.HireDate().toString());
}
empIterator.close();

关闭迭代程序对象∶<0}
empIterator.close();

  程序段一把从第2步到第5步联结起来演示了使用了命名迭代程序和empSalary参数的listEmployees ()方法。位置迭代程序∶与命名迭代程序相反,位置迭代程序只是指定了列的数目和种类,而不是它们的名称。列数据可以只通过位置访问,通过传统的FETCH... INTO语法。除了使用FETCH语句之外,还需要使用位置迭代程序方法endFetch ()来检测结束条件从而从循环中跳出。但是必须在访问所需要取得的数据之间检查条件。

  下面是相同的五个步骤,演示如何使用位置迭代程序:

  声明迭代程序类∶

   #sql iterator EmpIteratorClass(String, String, Timestamp);

  从迭代程序类中声明迭代程序对象。还必须声明从迭代程序对象中取得的数据所必需的主变量。

EmpIteratorClass empIterator;
String name = null;
String job = null;
Timestamp hireDate = null;

  使用SQL SELECT语句加入迭代程序对象∶

int salary = 1500;
#sql empIterator = {
select Ename, Job, HireDate
from Emp
where Sal > :salary
};

  把数据从迭代程序对象中读入主机变量∶

while (true) {
#sql { FETCH :empIterator INTO :name, :job, :hireDate };
if (empIterator.endFetch()) {
break;
}

System.out.println("Name: " + name);
System.out.println("Job: " + job);
System.out.println("Hire Date:" + hireDate().toString());
}

  关闭迭代程序对象

   empIterator.close();

  把从第2步到第5步联结起来演示了使用了位置迭代程序和empSalary参数的listEmployees ()方法。可见,位置迭代程序对象所使用的语法很像PL/SQL游标的语法。命名迭代程序和位置迭代程序两者都执行基本相同的功能∶它们都保存SQL查询的结果,有可能还是很多行数据至于到底使用哪一种迭代程序则要看情况或是根据自己的偏爱;从性能角度上来看呢,它们产生的结果都是一样的。

可执行语句

  可执行的SQLJ语句在一对大括号内包含了静态的SQL操作。有两种可能的可执行语句,由SQL是否回来一个值来决定。下面是一个不返回值的嵌入式SQL语句的例子;它在Emp表的Ename列和Sal列上创建一个复合索引∶
#sql { create index EMP_ENAME_SAL on Emp(Ename, Sal) };

  如果一个嵌入式SQL语句返回值的话,你需要使用一个主机变量来指定结果应该放在什么地方。在本例子中,调用PL/SQL函数getSalary返回雇员号Empno为7900的雇员的工资。你可以使用VALUES或者SET运算符来调用函数;也就是说,

int salary;
int empNo = 7900;
#sql salary = { VALUES getSalary(:empNo} };
or
#sql { SET :salary = getSalary(:empNo) };

  主机表达式

  在上面的例子中,我们可以看到,主机变量允许SQLJ程序在数据库和Java程序之间交换信息。它们是任何在Java程序中声明的变量。主机变量被嵌入到SQLJ语句里,称作主机表达式。主机表达式把主机变量绑定在SQLJ可执行语句上,它们也可能包括Java数组元素,对象属性或者Java函数。SQLJ负责在SQL和Java环境之间来回移动数据。所有的标准JDBC类型-象Boolean,byte,short,int,String,byte [],double,float,java.sql.Date等等。—都是SQLJ中有效的主机表达式。此外,Oracle的SQLJ翻译器支持使用Oracle数据库类型,例如ROWID,CLOB,BLOB和Object以及REF类型。在本文中,我讨论了编写实际的SQLJ代码所必需用到的SQLJ对象类型:连接上下文,命名和位置迭代程序,可执行语句和主机表达式。在以后的文章里,我想编写一个服务器端SQLJ程序,编译它,然后把它配置进Oracle JServer并把它PL/SQL对应的程序进行性能上的比较。

代码段1
public static void listEmployees(String empSalary)
throws SQLException {
EmpIteratorClass empIterator;
Integer salary = new Integer(empSalary);
try {
#sql empIterator = {
select Ename, Job, HireDate
from Emp
where Sal > :salary
};
while (empIterator.next()) {
System.out.println("Name: " + empIterator.Ename());
System.out.println("Job: " + empIterator.Job());
System.out.println("Hire Date:" +
empIterator.HireDate().toString());
}
empIterator.close();
} catch (SQLException e) {
System.err.println("SQLException" + e);
System.exit(1);
}
}
代码段2
public static void listEmployees(String empSalary)
throws SQLException {
EmpIteratorClass empIterator;
Integer salary = new Integer(empSalary);
/*主机变量 */
String name = null;
String job = null;
Timestamp hireDate = null;
try {
#sql empIterator = {
select Ename, Job, HireDate
from Emp
where Sal > :salary
};
while (true) {
#sql { FETCH :empIterator INTO :name, :job, :hireDate };
if (empIterator.endFetch()) {
break;
}

System.out.println("Name: " + name);
System.out.println("Job: " + job);
System.out.println("Hire Date:" + hireDate().toString());
}
empIterator.close();
} catch (SQLException e) {
System.err.println("SQLException" + e);
System.exit(1);
}
}




分享到
  • 微信分享
  • 新浪微博
  • QQ好友
  • QQ空间
点击: