ContentProvider和SQLite数据库

源地址: http://www.vogella.com/tutorials/AndroidSQLite/article.html

1.SQLite 和 Android

1.1 什么是SQLite?

SQLite是开源数据库。SQLite支持标准的关系数据库的特性,例如SQL语法,事务操作,prepared statement. 只需要很少的运行内存(大概250k),很适合嵌入到一些应用程序中。
SQLite支持的数据类型有:TEXT(类似java中的String)、INTEGER(类似java中的long)、和REAL(类似java中的double)。所有的类型在存入数据中前必须先转换成这几种类型。数据库自己不会去检查写入的数据是否是对应的数据类型,例如,你可以讲一个integer类型数据写入到string条目中,相反也可以。
更多关于SQLite的信息,可以访问SQLite网站:http://www.sqlite.org

1.2 Android中的SQLite

SQLite被嵌入在每一个Android设备中。在Android中使用SQLite数据库,不需要安装也不需要管理员权限。
你只需要定义创建和更新数据库的语句。然后Android系统会自动帮你维护这个数据库。
对SQLite数据库的访问需要访问文件系统。这可能比较慢。因此推荐异步执行数据库操作。
如果你的应用创建了数据库,那么数据库被默认保存在这个目录下:DATA/data/APP_NAME/databases/FILENAME
上面的数据库路径会符合这么几个规则:DATAEnvironment.getDataDirectory()方法返回的路径。APP_NAME是应用名字。FILENAME是你在代码中为数据库指定的名字。

2.看这篇教程的必要基础

掌握Android开发的基础知识。

3.SQLite知识结构

3.1 有关的包

android.database包 包括所有和数据库操作相关的类。android.database.sqlite包括和SQLite相关的类。

3.2 使用SQLiteOpenHelper来创建和更新数据库

可以通过创建一个继承SQLiteOpenHelper类的子类来创建和升级数据库。在类的构造函数中调用SQLiteOpenHelpersuper()方法,指定数据库的名字和当前版本。
在这个新创建的类中,你需要覆盖这么几个方法来创建和更新数据库:

  • onCreate() - 如果数据库还没有创建,会被调用。
  • onUpgrade() - 如果数据库版本升级会被调用。这个方法允许你更新当前数据库的机构或者删掉当前的数据库使用onCreate()重新创建数据库。

两个方法都会收到SQLiteDatabase对象作为参数,是数据库的java封装。
SQLiteOpenHelper提供两个方法getReadableDatabase()getWriteableDatabase()来获取SQLiteDatabase对象,用于从数据库中读取信息或者写入数据库。
数据库表中,应该用_id来作为键值。几个Android功能依赖这一设定。

窍门:每个表建立一个类是个很好的编程习惯。这个类定义静态的onCreate()方法和onUpgrade()方法。在SQLiteOpenHelper对应的方法中调用这些方法。这样即使你有多个表,也可以保持SQLiteOpenHelper实现类的可读性。

3.3 SQLiteDatabase

SQLiteDatabase是Android中操作SQLite数据库的基础类,它提供了方法去打开、查询、更新和关闭数据库。
特定的,SQLiteDatabase提供了insert()update()delete()等方法。
另外还提供了execSQL()方法,来直接执行一个SQL语句。
ContentValues对象可以用来定义键值对。键代表数据库表中某一条目的标识符,值代表数据库某一行纪录中这个条目对应的内容。ContentValues可以用来插入或者更新数据库。
查询可以通过rawQuery()或者query()来完成。或者通过SQLiteQueryBuilder类来完成。
rawQuery()直接接受select查询语句。
query()提供一个指定SQL查询的接口。
SQLiteQueryBuilder是一个很方便构建SQL查询的类。

3.4 rawQuery()示例

下面是一个rawQuery()调用例子:

Cursor cursor = getReadableDatabase().
    rawQuery("select * from todo where _id = ?", new String[] { id });

3.5 query()示例

下面是一个query()例子:

return database.query(DATABASE_TABLE, 
    new String[] { KEY_ROWID, KEY_CATEGORY, KEY_SUMMARY, KEY_DESCRIPTION },
     null, null, null, null, null);

方法query()有下面这几个参数:表 1. 方法query()的参数

参数 解释
String dbName 需要查询的数据库表
String[] columnNames 查询需要返回的列名集合,null表示所有列
String whereClause Where-clause,也就是过滤出需要返回的条目,null会选择所有条目
String[] selectionArgs whereClause中可能包括?占位符,这些占位符会被selectionArgs数组中的值代替
String[] groupBy 表示如何将结果分组,null表示不分组
String[] having 用来过滤分组后的结果,null表示不过滤
String[] orderBy 被用来排序的条目,null表示不排序

如果那个参数不需要,传null就可以了,例如,group by语句。
“whereClause”语句中不包括“where”字符,例如一个“where”语句可能像这样“_id=19 and summary=?”。
如果你通过?在where语句中指定占位符,那么需要在selectionArgs参数中传递对应的参数。

3.6 Cursor

一个查询操作会返回一个Cursor对象。一个Cursor代表查询结果,基本上指向查询结果的一行。这样Android可以有效地缓存查询结果,因为不需要一次性把所有的数据装载到内存。
可以通过getCount()来返回查询结果的数目。
可以通过方法moveToFirst()moveToNext()从行与行之间切换。方法isAfterLast()可以用来检查,是不是所有的查询结果已经被访问了。
Cursor提供get*()方法,例如getLong(columnIndex)getString(columnIndex)来访问当前行中的特定列的内容。“columnIndex”表示你要访问的列的下标。
Cursor用方法getColumnIndexOrThrow(String)根据列名字返回列的下标。
一个Cursor需要用close()方法来关闭。

3.7 ListView, ListActivities 和 SimpleCursorAdapter

ListViews是可以显示一系列元素的Views
ListAcitivties用来更方便的使用ListViews
为了将数据库和ListViews连接在一起,你可以使用SimpleCursorAdapterSimpleCursorAdapter允许为ListViews的每一个元素创建layout。
你需要定义一个包含数据库列名的数组,和另外一个包含Views中需要填充数据的元素的ID。
SimpleCursorAdapter会把Cursor代表的数据和ListView中的每个条目中需要填充数据的元素映射起来。
为了获得Cursor对象,你需要使用Loader类。

4.教程:使用SQLite

4.1 下面这些内容演示如何使用SQLite 数据库。我们将使用一个DAO对象来管理数据。这个DAO负责和数据库的连接,还有查询和修改数据。它也负责将数据库数据转换为java数据类型,所以用户界面的代码不需要处理和数据库的连接层。
app最终会想下面这样:

使用DAO并不总是正确的选择。DAO创建java的model对象;直接使用数据库或者通过ContentProvider可能在有效利用资源方面是更好的选择,因为你可以不用创建model对象。
我仍然会展示如何使用DAO,作为一个相对简单的使用数据库的例子。使用的是Android4.0的系统。API等级是15.另外我更愿意介绍Loader类,用来展示在Android3.0用来维护数据库Cursor。而且这个类有额外的复杂性。

4.2 创建项目

de.vogella.android.sqlite.first来创建Android project,并同时创建一个名叫TestDatabaseActivity的activity。

4.3 数据库和数据模型

创建一个MySQLiteHelper类。这个类负责创建数据库。onUpgrade()负责删掉现在的数据库并重新创建一个表。它也定义了几个常量表示表名和表里面的列名。

package de.vogella.android.sqlite.first;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class MySQLiteHelper extends SQLiteOpenHelper {

  public static final String TABLE_COMMENTS = "comments";
  public static final String COLUMN_ID = "_id";
  public static final String COLUMN_COMMENT = "comment";

  private static final String DATABASE_NAME = "commments.db";
  private static final int DATABASE_VERSION = 1;

  // Database creation sql statement
  private static final String DATABASE_CREATE = "create table "
      + TABLE_COMMENTS + "(" + COLUMN_ID
      + " integer primary key autoincrement, " + COLUMN_COMMENT
      + " text not null);";

  public MySQLiteHelper(Context context) {
    super(context, DATABASE_NAME, null, DATABASE_VERSION);
  }

  @Override
  public void onCreate(SQLiteDatabase database) {
    database.execSQL(DATABASE_CREATE);
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    Log.w(MySQLiteHelper.class.getName(),
        "Upgrading database from version " + oldVersion + " to "
            + newVersion + ", which will destroy all old data");
    db.execSQL("DROP TABLE IF EXISTS " + TABLE_COMMENTS);
    onCreate(db);
  }

} 

创建Comment类。这个类是我们的数据模型,包含着我们要保存到数据库中的用户界面数据。

package de.vogella.android.sqlite.first;

public class Comment {
  private long id;
  private String comment;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getComment() {
    return comment;
  }

  public void setComment(String comment) {
    this.comment = comment;
  }

  // Will be used by the ArrayAdapter in the ListView
  @Override
  public String toString() {
    return comment;
  }
} 

创建CommentsDataSource类。这个类是我们的DAO。它维护着和数据库的连接,并且支持向数据库中添加数据和获取数据。

package de.vogella.android.sqlite.first;

import java.util.ArrayList;
import java.util.List;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;

public class CommentsDataSource {

  // Database fields
  private SQLiteDatabase database;
  private MySQLiteHelper dbHelper;
  private String[] allColumns = { MySQLiteHelper.COLUMN_ID,
      MySQLiteHelper.COLUMN_COMMENT };

  public CommentsDataSource(Context context) {
    dbHelper = new MySQLiteHelper(context);
  }

  public void open() throws SQLException {
    database = dbHelper.getWritableDatabase();
  }

  public void close() {
    dbHelper.close();
  }

  public Comment createComment(String comment) {
    ContentValues values = new ContentValues();
    values.put(MySQLiteHelper.COLUMN_COMMENT, comment);
    long insertId = database.insert(MySQLiteHelper.TABLE_COMMENTS, null,
        values);
    Cursor cursor = database.query(MySQLiteHelper.TABLE_COMMENTS,
        allColumns, MySQLiteHelper.COLUMN_ID + " = " + insertId, null,
        null, null, null);
    cursor.moveToFirst();
    Comment newComment = cursorToComment(cursor);
    cursor.close();
    return newComment;
  }

  public void deleteComment(Comment comment) {
    long id = comment.getId();
    System.out.println("Comment deleted with id: " + id);
    database.delete(MySQLiteHelper.TABLE_COMMENTS, MySQLiteHelper.COLUMN_ID
        + " = " + id, null);
  }

  public List getAllComments() {
    List comments = new ArrayList();

    Cursor cursor = database.query(MySQLiteHelper.TABLE_COMMENTS,
        allColumns, null, null, null, null, null);

    cursor.moveToFirst();
    while (!cursor.isAfterLast()) {
      Comment comment = cursorToComment(cursor);
      comments.add(comment);
      cursor.moveToNext();
    }
    // make sure to close the cursor
    cursor.close();
    return comments;
  }

  private Comment cursorToComment(Cursor cursor) {
    Comment comment = new Comment();
    comment.setId(cursor.getLong(0));
    comment.setComment(cursor.getString(1));
    return comment;
  }
} 

4.4 用户界面

res/layout文件夹下面的main.xml文件改成下面这样。这个布局中包含两个按钮分别用于添加和删除评论,和一个用来显示当前所有评论的ListView。评论内容一会在activity中随机生成。




    

        

TestDatabaseActivity类改成下面这样:

package de.vogella.android.sqlite.first;

import java.util.List;
import java.util.Random;

import android.app.ListActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;

public class TestDatabaseActivity extends ListActivity {
  private CommentsDataSource datasource;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    datasource = new CommentsDataSource(this);
    datasource.open();

    List values = datasource.getAllComments();

    // use the SimpleCursorAdapter to show the
    // elements in a ListView
    ArrayAdapter adapter = new ArrayAdapter(this,
        android.R.layout.simple_list_item_1, values);
    setListAdapter(adapter);
  }

  // Will be called via the onClick attribute
  // of the buttons in main.xml
  public void onClick(View view) {
    @SuppressWarnings("unchecked")
    ArrayAdapter adapter = (ArrayAdapter) getListAdapter();
    Comment comment = null;
    switch (view.getId()) {
    case R.id.add:
      String[] comments = new String[] { "Cool", "Very nice", "Hate it" };
      int nextInt = new Random().nextInt(3);
      // save the new comment to the database
      comment = datasource.createComment(comments[nextInt]);
      adapter.add(comment);
      break;
    case R.id.delete:
      if (getListAdapter().getCount() > 0) {
        comment = (Comment) getListAdapter().getItem(0);
        datasource.deleteComment(comment);
        adapter.remove(comment);
      }
      break;
    }
    adapter.notifyDataSetChanged();
  }

  @Override
  protected void onResume() {
    datasource.open();
    super.onResume();
  }

  @Override
  protected void onPause() {
    datasource.close();
    super.onPause();
  }

} 

4.5 运行这个应用

安装这个app,并使用AddDelete按钮。重启你的应用,验证数据是一直存在的。

5.Content Provider 和数据共享

5.1 什么是content provider

如果你想和别的应用共享数据,你可以使用content provider(简称provider)。Provider提供基于URI封装的数据。任何以content://开头指向资源的URI都以通过provider来访问。通过content provider一个资源URI允许你执行数据基本的CRUD操作(Create, Read, Update, Delete)。
provider允许应用访问数据。这个数据可以存储在数据库中,文件系统中,或者远程服务器上的一个文件。
一般content provider被用在应用中是为了和别的应用分享数据。应用数据通常是默认应用私有的,一个content provider是一个方便的和别的应用分享数据的接口。
一个content provider必须在应用的manifest文件中声明。

5.2 content provider的基本URI

访问content provider的基本URI被定义为content://和provider命名空间的组合。这个命名空间通过android:authorities属性定义在manifest文件中。例如content://test/
基本URI代表一个数据集合。如果基本URI后跟一个实例标识,例如content://test/2,则表示单一实例。

5.3 访问content provider

因为访问一个provider需要知道它的URI,所以把provider的URI作为公共常量提供给别的开发者是一个很好的开发习惯。
许多Android数据, 例如联系人, 都是通过content provider访问的。

5.4 自定义content provider

创建自定义content provider,你必须创建继承android.content.ContentProvider的类。然后将这个类在应用manifest文件中声明。相应的必须声明android:authorities属性,用来标识这个content provider。这个属性是访问数据的基本URI,所以必须是独一无二的。


你的content provider必须实现几个方法, 例如query(), insert(), delete(), getType(), onCreate()。对于不支持的方法最好抛一个异常UnsupportedOperationException()
query()方法必须返回Cursor对象。

5.5 content provider和安全

在Android 4.2之前,content provider都是默认对别的应用可用的。Android 4.2以后必须明确地声明为导出的。
在manifest文件中的content provider的声明中,可以使用android:exported=false|true参数来制定content provider的透明性。

技巧:明确的设置android:exported参数来确保在不同的版本间的一致性。

5.6 线程安全

如果直接访问数据库,并且在不同的线程都有写操作,那么你将会陷入并发问题。
一个content provider可以在同一时间被多个程序访问,所以必须实现线程安全性。最简单的方法是在content provider每个方法前加上synchoronized关键字, 这样同一时间只有一个线程可以访问。
如果你不需要Android同步对provider的访问,在manifest文件中provider的生命中设置参数android:multiprocess=true。这样就允许在每个客户端进程中创建一个content provider实例,省去了IPC(interprocess communication)。

6.教程:使用ContentProvider

6.1 介绍

下面的内容将会使用Contact(联系人)应用中的ContentProvider

6.2 在你的手机上创建联系人

对于这个例子来说,我们需要几个已经存在的联系人内容。选择菜单按钮然后是People(联系人)按钮来创建联系人。

应用会问你是否想登陆,选择登陆或者暂不登陆。选择“创建新联系人”。你可以创建本地联系人。


创建完一个新的联系人,然后可以点击 + 按钮添加更多的联系人。这样app中应该有一些可以使用的联系人数据。

6.3 使用Contact中的ContentProvider

创建一个新的Android项目,名字是de.vogella.android.contentprovider和一个名字是ContactsActivity的activity。
更改res/layout中对应的layout文件。把当中的TextViewid改为contactview。删除默认的text内容。
布局文件最后回想下面这样:




    

 

访问联系人中的ContentProvider需要一定的权限,因为不是所有应用都应该有访问联系人信息的权限。打开应用的AndroidManifest.xml文件,选择Permission标签。点击添加按钮,选择使用权限。从下拉菜单中选择android.permission.READ_CONTACTS
将activity改成下面这样:

package de.vogella.android.contentprovider;

import android.app.Activity;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.TextView;

public class ContactsActivity extends Activity {
  
/** Called when the activity is first created. */

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_contacts);
    TextView contactView = (TextView) findViewById(R.id.contactview);

    Cursor cursor = getContacts();

    while (cursor.moveToNext()) {

      String displayName = cursor.getString(cursor
          .getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
      contactView.append("Name: ");
      contactView.append(displayName);
      contactView.append("\n");
    }
  }

  private Cursor getContacts() {
    // Run query
    Uri uri = ContactsContract.Contacts.CONTENT_URI;
    String[] projection = new String[] { ContactsContract.Contacts._ID,
        ContactsContract.Contacts.DISPLAY_NAME };
    String selection = ContactsContract.Contacts.IN_VISIBLE_GROUP + " = '"
        + ("1") + "'";
    String[] selectionArgs = null;
    String sortOrder = ContactsContract.Contacts.DISPLAY_NAME
        + " COLLATE LOCALIZED ASC";

    
    return managedQuery(uri, projection, selection, selectionArgs,
        sortOrder);
  }

} 

如果你运行这个应用,那么它将会从联系人应用的ContentProvider读取数据,并且显示在TextView中。通常这样的数据应该显示在ListView中。

7.Loader

7.1 Loader类的作用

Loader类允许你在activity或者fragment下异步的加载数据。它们可以在数据源发生变化时,将新的数据更新过来。也可以维护配置变化前后数据的一致性。
如果数据是在和activity或者fragment断开后获取的,则缓存这些数据。
Loader类在Android3.0被引入进来,在android1.6有兼容包支持。

7.2 实现一个Loader

你可以将AsyncTaskLoader作为基类来实现你自己的Loader类。
Activity或者Fragment的LoaderManager可以管理一或多个Loader实例Loader的创建是通过下面的调用来实现的:

#start a new loader or re-connect to existing one
getLoaderManager().initLoader(0, null, this)

第一个参数是用来识别的ID,回调类用来标识Loader类。第二个参数用来给回调类额外信息。
initLoader()的第三个参数表示初始化一旦开始就回调用的类(回调类)。这个类必须实现LoaderManager.LoaderCallbacks接口。推荐用activity或者fragment使用Loader并且实现LoaderManager.LoaderCallbacks接口。
Loader不是通过LoaderManager.initLoader()直接创建的,但是必须在 onCreateLoader()中由回调类来创建。
Loader异步获取完数据,回调类的onLoadFinished()方法会被调用。在这里,你可以更新你的用户接口。

7.3 SQLite 数据库和CursorLoader

Android提供了一个默认Loader实现CursorLoader来管理SQLite数据库连接。
对于基于SQLite数据库的ContentProvider,一般需要使用CursorLoader类。这个类在后台线程执行数据库查询,这样就不会阻塞应用程序。
CursorLoader用来替换已经废弃的使用acivity自己维护cursors的方法。
如果Cursor失效,回调类的onLoaderReset()会被调用。

8.Cursor 和 Loaders

访问数据库的问题之一就是访问速度太慢。另一个问题就是应用得正确考虑组件的生命周期,例如当配置改变的时候应该关闭和打开数据库。
为了维护组件的生命周期,在Android3.0之前可以使用managedQuery()
但是这个方法在Android3.0之后被弃用了,你应该使用Loader框架来访问ContentProvider
SimpleCursorAdapter可以和ListViews一起使用,它由swapCursor()方法。你的Loader可以使用这个方法在onLoadFinished()方法里面更新CursorCursorLoader在配置变化之后会重新连接Cursor

你可能感兴趣的:(contentProvider,android,sqlite)