如何通过Android SQLiteDatabasesqlite3 创建数据库SQLite数据库视图?

使用安卓读取sqlite数据库方法记录 - Android技巧 - 大学IT网
当前位置: >
> 使用安卓读取sqlite数据库方法记录
关键词:&&阅读(2212) 赞(19)
[摘要]本文是对使用安卓读取sqlite数据库方法记录的讲解,对学习Android编程技术有所帮助,与大家分享。
最近要实现android读取sqlite数据库文件,在这里先做一个英汉字典的例子。主要是输入英语到数据库中查询相应的汉语意思,将其答案输出。数据库采用sqlite3.
实现过程完全是按照参考文章中所述。其中要说明的是,程序在第一次启动的时候,会把数据库安装到内存卡上面,从而可以读却数据库。
相关的代码:
packagecom.
importjava.io.F
importjava.io.FileOutputS
importjava.io.InputS
importandroid.app.A
importandroid.app.AlertD
importandroid.database.C
importandroid.database.sqlite.SQLiteD
importandroid.os.B
importandroid.text.E
importandroid.text.TextW
importandroid.util.L
importandroid.view.V
importandroid.view.View.OnClickL
importandroid.widget.AutoCompleteTextV
importandroid.widget.B
publicclassDictionaryextendsActivityimplementsOnClickListener,TextWatcher{
privatefinalStringDATABASE_PATH=android.os.Environment
.getExternalStorageDirectory().getAbsolutePath()
+"/dictionary";
privatefinalStringDATABASE_FILENAME="dictionary.db3";
ButtonbtnSelectW
AutoCompleteTextViewactvW
publicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//打开数据库,database是在Main类中定义的一个SQLiteDatabase类型的变量
database=openDatabase();
//下面的代码装载了相关组件,并设置了相应的事件
btnSelectWord=(Button)findViewById(R.id.btnSelectWord);
actvWord=(AutoCompleteTextView)findViewById(R.id.actvWord);
btnSelectWord.setOnClickListener(this);
actvWord.addTextChangedListener(this);
publicvoidonClick(Viewview)
//查找单词的SQL语句
Stringsql="selectchinesefromt_wordswhereenglish=?";
Cursorcursor=database.rawQuery(sql,newString[]
{actvWord.getText().toString()});
Stringresult="未找到该单词.";
//如果查找单词,显示其中文信息
if(cursor.getCount()&0)
//必须使用moveToFirst方法将记录指针移动到第1条记录的位置
cursor.moveToFirst();
result=cursor.getString(cursor.getColumnIndex("chinese"));
Log.i("tran","success"+result);
//显示查询结果对话框
newAlertDialog.Builder(this).setTitle("查询结果").setMessage(result)
.setPositiveButton("关闭",null).show();
privateSQLiteDatabaseopenDatabase(){
//获得dictionary.db文件的绝对路径
StringdatabaseFilename=DATABASE_PATH+"/"+DATABASE_FILENAME;
Filedir=newFile(DATABASE_PATH);
//如果/sdcard/dictionary目录中存在,创建这个目录
if(!dir.exists())
dir.mkdir();
//如果在/sdcard/dictionary目录中不存在
//dictionary.db文件,则从res\raw目录中复制这个文件到
//SD卡的目录(/sdcard/dictionary)
if(!(newFile(databaseFilename)).exists()){
//获得封装dictionary.db文件的InputStream对象
InputStreamis=getResources().openRawResource(
R.raw.dictionary);
FileOutputStreamfos=newFileOutputStream(databaseFilename);
byte[]buffer=newbyte[8192];
intcount=0;
//开始复制dictionary.db文件
while((count=is.read(buffer))&0){
fos.write(buffer,0,count);
fos.close();
is.close();
//打开/sdcard/dictionary目录中的dictionary.db文件
SQLiteDatabasedatabase=SQLiteDatabase.openOrCreateDatabase(
databaseFilename,null);
}catch(Exceptione){
publicvoidafterTextChanged(Editables){
publicvoidbeforeTextChanged(CharSequences,intstart,intcount,
intafter){
publicvoidonTextChanged(CharSequences,intstart,intbefore,intcount){
相关Android技巧推荐Android 中的 SQLite 数据库支持
我们大多数人都至少熟悉某些 Core Data 提供给我们的直接可用的持久化特性。然而不幸的是,它们中的很多在 Android 平台上并不是自动化的。 例如,Core Data 抽象出大部分数据库的 SQL 语法和数据库标准,这些语法和标准都是数据库工程师们每天面对的问题。 因为 Android 仅提供一个简单的 SQLite 客户端,所以你扔需要写 SQL 并且确保你的数据库表被适当的标准化了。
Core Data 允许我们在对象方面考虑。 实际上,它能自动处理列集和散集对象。 得益于其提供了记录层的缓存,所以它在移动设备上的性能很好。每次从存储中请求同样的数据段时,它不用再创建另外一个的对象实例。当观察一个对象的变化时,我们甚至不需要刷新那个被观察对象就能做到。
Android 上可不是这种情况。你要完全负责将对象写入数据库或者从中将它们读取。这意味着你要自己实现对象缓存(如果需要的话),管理对象实例化,以及手动对任何已存在对象进行是否需要变更的检测。
在 Android 中,你需要注意版本特定的功能。不同版本的 Android 使用不同的 SQLite 实现。这意味着同一个数据库指令可能在其他平台版本上产生完全不同的结果。根据你执行的 SQLite 版本不同,一个查询语句也会执行得各不相同。
许多Android开发者来自企业。许多年来,库一直在服务器平台上用于减轻与数据库的交互的难度。然而,这些库因为性能问题而导致无法在移动端直接使用。意识到这一点,一些开发者组织起来制作了面向移动端的 ORM 库来解决这一问题。
在 Android 上给 SQLite 添加 ORM 支持的其中一个使用广泛的的方法是 。OrmLite 提供对持久化对象的自动列集和散集化。它不用写大量的 SQL,并且提供程序接口来查询,更新,删除对象。在 ORM 中另一个竞争者是 。它提供许多与 OrmLite 类似的功能,但是承诺具有更好的性能(根据它的上说),例如基于注解的设置。
对第三方库通常都是抱怨都是加到项目中以后造成了额外的复杂度并且使得性能臃肿。有开发者觉得这实在很蛋疼,于是他写了一个叫
的轻量级 Android SQLite 框架的封装。它声称的目标是在不使用 ContentValues 和不解析 Cursors 的情况下为 Java 对象的提供持久化存储,它将会简单轻量,并整合时不会对核心 Android 类造成任何影响。你仍将需要管理数据库的创建,但是查询对象会变得简单很多。
还有开发者决定完全废弃 SQLite 并且创建了 。它从开始到接口都是用面向对象语言设计的。它善于列集和散集对象,并且在性能跑分中表现很好。这种解决方法确实是完全替换了部分 Android 框架,但是也存在一定风险,因为你可能很难再在以后将其替换成不同方案了。
有这些甚至是更多可用的选择,为什么大家还都选择在原始的 Android 数据库框架下开发呢?这么说吧,框架和封装相较于它们所解决的问题,有时候能带来更多麻烦。例如,在一个项目中,我们同时写入数据库且实例化很多对象,这就会导致我们的 ORM 库慢得像爬一样。因为这个库不是设计用来让我们以这样的方式使用的。
在评估框架和库的时候,检查看看有没有使用 Java 的反射机制。反射机制在 Java 中代价相对较大,因此要谨慎使用。另外,如果你的项目是 Ice Cream Sandwich 前的版本,还要看看你的库是否在使用 Java 最近解决的一个 ,它会导致在运行时因为注解而导致的性能下降。
最后,还要评估添加框架会不会显著的增加项目的复杂度。如果你与其他开发者共同开发,记住,他们也必须花时间来学习该库。在你决定要不要使用一个第三方解决方案之前,弄明白 Android 到底是如何处理数据存储的这一问题,是非常重要的。
打开数据库
Android 上创建和打开数据库相对简单。你必须通过子类化
来进行实现。默认的构造方法中,你要制定数据库名字。如果该数据库已经存在,它会被打开。如果不存在,则会被创建。应用能有许多单独的数据库文件。每个数据库都必须表示为单独的 SQLiteOpenHelper 子类。
数据库文件对你的应用来说是私有的,它们存在文件系统中你的应用的子文件夹下,并且受Linux文件访问权限保护。可惜的是,数据库文件没有加密。
但是创建数据库文件是不够的,在你的 SQLiteOpenHelper 子类中,你要重写 onCreate() 方法执行SQL语句创建表,视图以及任何数据库模式(schema)中的东西。你可以重写例如 onConfigure() 之类的其他方法来启用或禁用数据库功能,比如预写日志或外键支持等。
改变数据库模式 (schema)
除了在 SQLiteOpenHelper 子类的构造方法中指定数据库名字,你还要指定数据库版本号。版本号对于任意一个给定的 release 版本必须是不变的,而且根据框架的要求,这个数需要是只增不减的。
SQLiteOpenHelper 使用数据库的版本号来决定是否需要升级或降级。在升级或降级的回调方法中,你将使用提供给你的 oldVersion 和 newVersion 参数来决定哪个
语句需要执行来更新 schema。为每个新数据库版本提供单独的语句是一种很好的方法,这样就可以处理数据库的跨版本升级了。
连接到数据库
数据库查询是由
类来管理的。 在你的 SQLiteOpenHelper 子类调用 getReadableDatabase() 或 getWritableDatabase() 时,会返回一个 SQLiteDatabase 实例。要注意这些方法通常会返回同一个对象。唯一一个例外是 getReadableDatabase(),在遇到诸如磁盘空间已满之类的问题时,它会返回一个只读的数据库,这时会阻止写入数据库。由于磁盘问题其实很少发生,许多开发者开发过程中只调用 getWritableDatabase()。
数据库创建和模式改变是在你第一次获得 SQLiteDatabase 实例后才会进行的。因此,你不能在主线程请求 SQLiteDatabase 实例。你的 SQLiteOpenHelper 子类几乎都会返回同样的 SQLiteDatabase 实例。这意味着在任何线程调用 SQLiteDatabase.close() 都会关闭你应用中所有的 SQLiteDatabase 实例。这就导致一大堆难于查找的 bug。实际上,有些开发者选择只在程序启动时打开 SQLiteDatabase ,只在程序关闭调用 close()。
SQLiteDatabase 提供了对数据库进行查询,插入,更新及删除的方法。对于简单的查询,你不用写任何 SQL。但对于更高级的查询,你还要得自己写 SQL。SQLiteDatabase 有一个 rawQuery() 和 execSQL() 方法, 这能把整个 SQL 当做参数来执行高级查询,例如使用 unions 和 joins 命令。你可以使用
来协助你完成适当的查询。
query() 和 rawQuery() 都返回
对象。保持 Cursor 对象的引用并且在应用中传来传去听起来十分诱人,但是 Cursor 对象比 单纯的 Java 对象 (Plain Old Java Object, POJO) 耗费更多的系统资源。因此,Cursor 对象需要尽快散集化到 POJO。在散集化之后你需要调用 close() 方法释放资源。
SQLite 中支持事务 (transactions)。你可以通过调用 SQLiteDatabase.beginTransaction() 开启一个事务。事务能通过调用 beginTransaction() 嵌套。当外层事务结束后所有在这个事务中完成的工作,以及所有嵌套的事务都需要提交或回滚。所有那些没有用 setTransactionSuccessful() 方法标记为完成的事务中的变更都会被回滚。
数据访问对象 (Data Access Object)
如前面提到的,Android 不提供任何列集和散集对象的方法。这意味着我们要负责管理从 Cursor 中取数据到 POJO 中的逻辑。这套逻辑应该用
(DAO) 封装好。
Java 从业者,顺带上 Android 开发者,应该都对 DAO 模式很熟悉了。它的主要目的是将应用的交互从持久化层中抽象出来,而不暴露持久化的实现细节。这可以把应用从数据模式中隔离出来。这也使得迁移到第三方的数据库实现时,对应用的核心逻辑造成的风险能更小。你的应用中的所有与数据库的交互都应该通过 DAO 实现。
异步加载数据
获取 SQLiteDatabase 的参照是一个昂贵的操作,因此永远不要在主线程执行它。扩展来说,数据库查询也不要在主线程执行。Android 提供
来帮助实现这一点。它们允许 activity 或 fragment 异步加载数据。Loaders 可以解决配置改变时数据持久的问题,也能检测数据并在内容改变的时候发送新的结果。Android 提供
来从数据库中加载数据。
与其他应用程序共享数据
虽然数据库对于创建它们的应用来说是私有的,但是 Android 也提供与其他应用程序共享数据的方法。 提供一个结构化的接口,能使其他应用读取甚至可能修改你的数据。和 SQLiteDatabase 类似, Content Providers 开放出 query(),insert(),update() 和 delete() 这些方法来操作数据。数据以 Cursor 的形式返回,而且对 Content Provider 的访问默认是同步的,这样可以使访问是线程安全的。
Android 数据库与 iOS 上类似的功能相比,实现更加复杂。但是切记,不要只是为了减少模板代码而去使用第三方库。对于 Android 数据库框架的彻底理解会让你知道该不该选择使用第三方库,以及使用什么样的第三方库。 提供了两个操作 SQLite 数据库的样例工程。你可以详细看看
这两个项目可以获得更多信息。
iOS 开发者,目前工作在北京。喜欢吉他,游戏爱好者。先上一张效果图:
每次点击“插入”,都会在下面的ListView中新增一行
本文涉及四个文件,分别是DBTest.java、main.xml、line.xml、strings.ml
DBTest.java
public class DBTest extends Activity {
Button btn =
ListView listV
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//获取路径,也算是一种调试的方法吧
Log.d("myLog", this.getFilesDir().toString());
//创建或打开数据库(此处需要使用据对路径)
db = SQLiteDatabase.openOrCreateDatabase(this.getFilesDir()
.toString() + "/my.db3", null);
listView = (ListView) findViewById(R.id.show);
btn = (Button) findViewById(R.id.ok);
btn.setOnClickListener(new OnClickListener() {
public void onClick(View source) {
//获取用户输入
String title = ((EditText)findViewById(R.id.title))
.getText().toString();
String content = ((EditText)findViewById(R.id.content))
.getText().toString();
//如果没有数据库表就执行catch语句,先创建表,再执行其他语句
insertData(db, title, content);
Cursor cursor = db.rawQuery("select * from news_inf", null);
inflateList(cursor);
}catch(SQLiteException se){
//执行DDL创建数据表
db.execSQL("create table news_inf(_id integer primary key autoincrement,"
+" news_title varchar(50),"
+" news_content varchar(255))");
//执行insert语句插入数据
insertData(db, title, content);
//执行查询
Cursor cursor = db.rawQuery("select * from news_inf", null);
inflateList(cursor);
private void insertData(SQLiteDatabase db
, String title , String content){
//执行插入语句
db.execSQL("insert into news_inf values(null , ? , ?)"
, new String[]{title , content});
private void inflateList(Cursor cursor){
//填充SimpleCursorAdapter
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
DBTest.this, R.layout.line, cursor
, new String[]{"news_title", "news_content"}
, new int[]{R.id.my_title, R.id.my_content});
//显示数据
listView.setAdapter(adapter);
protected void onDestroy() {
super.onDestroy();
//退出程序时关闭SQLiteDatabase
if(db != null && db.isOpen()){
db.close();
其中db = SQLiteDatabase.openOrCreateDatabase(this.getFilesDir().toString() + "/my.db3", null);用于创建或打开SQLite数据库。当点击按钮的时候,程序会调用insertData方法,向底层数据库表中插入一行记录。然后再执行查询语句,把底层数据表中的记录查询出来,并使用ListView将查询结果(Cursor)显示出来。
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
DBTest.this, R.layout.line, cursor
, new String[]{"news_title", "news_content"}
, new int[]{R.id.my_title, R.id.my_content});
以上代码用于将Cursor封装成SimpleCursorAdapter,这个SimpleCursorAdapter实现了Adapter接口,可以作为ListView的内容适配器。Cursor里的每一行可以当成Map处理(以数据列的列名为key,数据列的值为value)。SimpleCursorAdapter这里有5个参数DBTest.this就是当前文件,R.layout.line是line.xml布局文件,cursor是执行完SQl语句之后,获取的游标,第4个参数是from,第5个参数是to,可以理解为将数据库里的news_title、news_content的字段值赋给line.xml里面的my_title、my_content。
&LinearLayout xmlns:android="/apk/res/android"
xmlns:tools="/tools"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:context=".DBTest" &
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:lines="2"/&
android:id="@+id/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/insert"
android:id="@+id/show"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
&/LinearLayout&
&?xml version="1.0" encoding="utf-8"?&
&LinearLayout xmlns:android="/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/my_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:width="120px"
android:id="@+id/my_content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
&/LinearLayout&
strings.xml中加入下面一行 ,一般按钮、TextView的文字都写在这个文件中,这样方便维护。比如说需要修改某些字,或者提供不同的语言版本的时候,可以直接在里面改。当然有些语言开发者不熟悉的时候,可以直接把整个文件给有能力翻译的人,这样非常方便。
&string name="insert"&插入&/string&
总结使用SQLiteDatabase进行数据库操作的步骤:
1.获取SQLiteDatabase对象,它代表了与数据库的连接
2.调用SQLiteDatabase的方法来执行SQL语句
3.操作SQL语句的执行效果,比如用SimpleCursorAdapter封装Cursor
4.关闭SQLiteDatabase,回收资源
& 开源中国(OSChina.NET) |
开源中国社区(OSChina.net)是工信部
指定的官方社区SQLite介绍SQLite是一个非常流行的嵌入式数据库,它支持SQL语言,并且只利用很少的内存就有很好的性能。此外,它还是开源的,任何人都可以使用它。SQLite由以下几个组件组成:SQL编译器、内核、后端以及附件。SQLite通过利用虚拟机和虚拟数据库引擎(VDBE),使调试、修改和扩展SQLite的内核变得更加方便。SQLite支持的数据类型参考链接:http://blog.csdn.net/wzy_1988/article/details/Android在运行时(run-time)集成了SQLite,所以每个Android应用程序都可以使用SQLite数据库。对于熟悉SQL的开发人员来说,在Android开发中使用SQLite相当简单。但是,由于JDBC会消耗太多的系统资源,所以JDBC对于手机这种内存受限设备来说并不合适。因此,Android提供了一些新的API来使用SQLite数据库。数据库存储在/data/data/项目包名/databases/ 目录下。Android开发中使用SQLite数据库Activity可以使用Content Provider或者 Service访问一个数据库。创建数据库Android不自动提供数据库。在Android应用程序中使用SQLite,必须自己创建数据库,然后创建表、索引、填充数据。Android提供了一个SQLiteOpenHelper帮助你创建一个数据库,你只要继承 SQLiteOpenHelper 类,就可以轻松的创建数据库。SQLiteOpenHelper 类根据开发应用程序的需要,封装了创建和更新数据库使用的逻辑。SQLiteOpenHelper 的子类,至少需要实现三个方法:构造函数,调用父类SQLiteOpenHelper的构造函数。这个方法需要四个参数:上下文环境,数据库名字,一个可选的游标工厂(通常是NULL),一个代表你正在使用的数据库模型版本的整数。onCreate()方法,它需要一个SQLiteDatabase对象作为参数,根据需要对这个对象填充表和初始化数据。onUpgrade()方法,它需要三个参数,一个SQLiteDatabase对象,一个旧的版本号和一个新的版本号,这样你就可以清楚如何把一个数据库从旧的模型转变为新的模型。下面代码展示了如何继承SQLiteOpenHelper创建数据库:import android.content.Cimport android.database.sqlite.SQLiteDimport android.database.sqlite.SQLiteOpenHpublic class MyDBHelper extends SQLiteOpenHelper { private static final String COLUMN_ID = "_id"; public static final String TABLE_NAME = "category"; private static final String DATABASE_NAME = "category.db"; private static final int DATABASE_VERSION = 1; private static final String DATABASE_CREATE = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + "(" + COLUMN_ID
+ " INTEGER PRIMARY KEY AUTOINCREMENT, " + " fid TEXT, token TEXT, cid TEXT, cname TEXT)"; public CategoryDBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) {
db.execSQL(DATABASE_CREATE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS" + TABLE_NAME);
onCreate(db); }}增删改查数据库因为SQLite支持标准的SQL语句,因此我们可以用标准SQL语句才增删改查数据库,推荐使用占位符的sql语句,看起来更加清爽,下面是我的代码示例:public class CategoryDBManager { private MyDBHelper myDBH private static CategoryDBManager categoryDBManager = private CategoryDBManager(Context context) {
myDBHelper = new MyDBHelper(context); } /**
* 单例模式
*/ public static CategoryDBManager getInstance(Context context) {
if (categoryDBManager == null) {
synchronized (CategoryDBManager.class) {
if (categoryDBManager == null) {
categoryDBManager = new CategoryDBManager(context);
return categoryDBM } public SQLiteDatabase getDB() {
SQLiteDatabase db = myDBHelper.getWritableDatabase();
while (db.isDbLockedByCurrentThread()) {
} public void insertLists(String token, String fid, List lists) {
// 打开可写数据库
SQLiteDatabase db = getDB();
for (PlateCategoryData pd : lists) {
// 执行SQL语句,替换占位符
db.execSQL("insert into " + MyDBHelper.TABLE_NAME + "(cid, cname, fid, token) values(?, ?, ?, ?)",
new Object[] { pd.getId(), pd.getName(), fid, token });
// 释放资源
db.close(); } public ArrayList getLists(String fid, String token) {
ArrayList datas = new ArrayList();
SQLiteDatabase db = getDB();
// 执行原始查询,得到cursor
String querySql = "select cid, cname from " + MyDBHelper.TABLE_NAME + " where fid = ? and token = ?";
Cursor cursor = db.rawQuery(querySql, new String[] { fid, token });
// 移动cursor到第一个数据(无数据返回false)
if (cursor.moveToFirst()) {
// while判断是否有下一条数据
PlateCategoryData pd = new PlateCategoryData(cursor.getString(cursor.getColumnIndex("cid")),
cursor.getString(cursor.getColumnIndex("cname")));
datas.add(pd);
} while (cursor.moveToNext());
cursor.close();
db.close();
} public void updateLists(String fid, String token, ArrayList datas) {
SQLiteDatabase db = getDB();
for (PlateCategoryData pd : datas) {
String sql = "update " + MyDBHelper.TABLE_NAME
+ " set cid = ?, cname = ? where fid = ? and token = ?";
db.execSQL(sql, new Object[] { pd.getId(), pd.getName(), fid, token });
db.close(); } public void deleteLists(String fid, String token) {
SQLiteDatabase db = getDB();
db.execSQL("delete from " + MyDBHelper.TABLE_NAME + " where fid = ? and token = ?", new Object[] { fid,
db.close(); } public void closeDB() {
SQLiteDatabase db = getDB();
if (db.isOpen()) {
myDBHelper.close();
db.close();
} }}参考链接[1] /developerworks/cn/opensource/os-cn-sqlite/安卓开发教程SQLite快速入门_百度文库
两大类热门资源免费畅读
续费一年阅读会员,立省24元!
安卓开发教程SQLite快速入门
||文档简介
将教育做到极致的高端在线教育网站|
总评分0.0|
&&安​卓​开​发​教​程​S​Q​L​i​t​e​快​速​入​门
阅读已结束,如果下载本文需要使用0下载券
想免费下载更多文档?
你可能喜欢

我要回帖

更多关于 sqlite创建数据库语句 的文章

 

随机推荐