Android :数据存储方案学习笔记之 SQLite数据库存储

SQLite数据库存储

特点:

  • 适用于存储大量复杂的数据

1、创建数据库

Android专门提供了一个 SQLiteOpenHelper帮助类对数据库进行创建和升级

  • SQLiteOpenHelper需要创建一个自己的帮助类去继承它并且重写它的两个抽象方法,即 onCreate()
    onUpgrade()
  • SQLiteOpenHelper 中有两个重要的实例方法:getReadableDatabase()getWritableDatabase()

创建一个名为 BookStore.db 的数据库

  • 在这个数据库中新建一张 book表,表中有 id(主键)、作者、价格、页数和书名等列

新建 MyDatabaseHelper类继承自 SQLiteOpenHelper,代码如下:

public class MyDatabaseHelper extends SQLiteOpenHelper {
    
    
 
    // Book表的建表语句
    private static final String CREATE_BOOK = "create table book("
            +"id integer primary key autoincrement,"
            +"author text,"
            +"price real,"
            +"pages integer,"
            +"name text)";
 
    private Context mContext;
 
    /**
     * 构造方法
     * @param context
     * @param name 数据库名
     * @param factory 允许我们在查询数据的时候返回一个自定义的 Cursor,一般都是传入 null
     * @param version 当前数据库的版本号,可用于对数据库进行升级操作
     */
    public MyDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
    
    
        super(context, name, factory, version);
        mContext = context;
    }
 
    /**
     * 创建数据库
     * @param db
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
    
    
        // 执行建表语句
        db.execSQL(CREATE_BOOK);
        ToastUtils.showShort("创建成功");
    }
 
    /**
     * 升级数据库
     * @param db
     * @param oldVersion
     * @param newVersion
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
    
 
    }
}

修改布局 activity_sqlite.xml 中代码,添加个按钮来创建数据库:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="15dp"
    android:orientation="vertical" >
 
    <Button
        android:id="@+id/btn_create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="创建数据库"/>
 
</LinearLayout>

修改 activity 中的代码:

public class SQLiteActivity extends AppCompatActivity {
    
    
 
    private MyDatabaseHelper dbHelper;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sqlite);
 
        // 构建MyDatabaseHelper对象,指定数据库名为"BookStore.db、版本号为1
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 1);
 
        Button btn_create_database = (Button) findViewById(R.id.btn_create_database);
        btn_create_database.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View view) {
    
    
                // 创建或打开一个现有的数据库(已存在则打开,否则创建一个新的)
                dbHelper.getWritableDatabase();
            }
        });
    }
}


第一次点击按钮时,会检测到当前程序中并没有 BookStore.db 这个数据库,于是会创建该数据库并调用 MyDatabaseHelper中的 onCreate()方法,创建 book 表,

然后弹出一个 Toast 提示创建成功。再次点击按钮时,会发现此时已存在 BookStore.db 数据库了,因此不会再创建一次。

2、升级数据库

MyDatabaseHelper 中的onUpgrade() 方法是用于对数据库进行升级

项目中已经有一张 book 表用于存放书的各种详细数据,若我们想再添加一张 category 表用于记录书籍的分类该怎么做?

修改 MyDatabaseHelper 中的代码,如下:

public class MyDatabaseHelper extends SQLiteOpenHelper {
    
    
 
    // Book表的建表语句
    private static final String CREATE_BOOK = "create table book("
            +"id integer primary key autoincrement,"
            +"author text,"
            +"price real,"
            +"pages integer,"
            +"name text)";
 
    // Category表的建表语句
    private static final String CREATE_CATEGORY = "create table category("
            +"id integer primary key autoincrement,"
            +"category_name text,"
            +"category_code integer)";
    
    private Context mContext;
 
    /**
     * 构造方法
     * @param context
     * @param name 数据库名
     * @param factory 允许我们在查询数据的时候返回一个自定义的 Cursor,一般都是传入 null
     * @param version 当前数据库的版本号,可用于对数据库进行升级操作
     */
    public MyDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
    
    
        super(context, name, factory, version);
        mContext = context;
    }
 
    /**
     * 创建数据库
     * @param db
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
    
    
        // 执行建表语句
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
        ToastUtils.showShort("创建成功");
    }
 
    /**
     * 升级数据库
     * @param db
     * @param oldVersion
     * @param newVersion
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
    
        // 若发现数据库中已存在 book 表或 category 表,将这两张表删除掉
        db.execSQL("drop table if exists book");
        db.execSQL("drop table if exists category");
        // 重新创建表
        onCreate(db);
    }
}

修改 activity 中的代码,在 SQLiteOpenHelper 的构造方法里接收的第四个参数传入一个比之前传入的版本号 1 大的数

就可以让 onUpgrade()方法得到执行了。如下

public class SQLiteActivity extends AppCompatActivity {
    
    
 
    private MyDatabaseHelper dbHelper;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sqlite);
 
        // 构建MyDatabaseHelper对象,指定数据库名为"BookStore.db、版本号为1
        //dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 1);
        // 将数据库版本号指定为2
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
 
        Button btn_create_database = (Button) findViewById(R.id.btn_create_database);
        btn_create_database.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View view) {
    
    
                // 创建或打开一个现有的数据库(已存在则打开,否则创建一个新的)
                dbHelper.getWritableDatabase();
            }
        });
    }
}

3、增删查改

CRUD:添加(Create),查询(Retrieve),更新(Update),删除(Delete)

SQLiteOpenHelper 中的 getReadableDatabase() 或 getWritableDatabase() 方法都会返回一个 SQLiteDatabase对象,借助这个对象可以对数据进行 CRUD 操作

修改下布局 activity_sqlite.xml 的代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="15dp"
    android:orientation="vertical" >
 
    <Button
        android:id="@+id/btn_create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="创建数据库"/>
 
    <Button
        android:id="@+id/btn_add_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="添加数据"/>
 
    <Button
        android:id="@+id/btn_update_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="更新数据"/>
 
    <Button
        android:id="@+id/btn_delete_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="删除数据"/>
 
    <Button
        android:id="@+id/btn_query_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="查询数据"/>
 
</LinearLayout>

3.1、添加数据

SQLiteDatabase 中提供了一个 insert() 方法用于添加数据

它接收三个参数,

  • 第一个参数是表名
  • 第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值 NULL,一般直接传入 null 即可
  • 第三个参数是一个 ContentValues 对象,它提供了一系列的 put() 方法重载

用于向 ContentValues 中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可

修改 activity 中的代码,如下:

public class SQLiteActivity extends AppCompatActivity {
    
    
 
    private MyDatabaseHelper dbHelper;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sqlite);
 
        . . .
 
        // 添加数据
        Button btn_add_data = (Button) findViewById(R.id.btn_add_data);
        btn_add_data.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View view) {
    
    
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                // 开始组装第一条数据
                values.put("name","The Da Vinci Code");
                values.put("author","Dan Brown");
                values.put("pages",454);
                values.put("price",16.96);
                db.insert("book",null,values); //插入第一条数据
                values.clear();
                // 开始组装第二条数据
                values.put("name","The Lost Symbol");
                values.put("author","Dan Brown");
                values.put("pages",510);
                values.put("price",19.95);
                db.insert("book",null,values); //插入第二条数据
            }
        });
    }
}

3.2、更新数据

SQLiteDatabase 中提供了一个 update() 方法

它接收四个参数,

  • 第一个参数和 insert()方法一样,是表名
  • 第二个参数是 ContentValues 对象,要把更新数据在这里组装进去
  • 第三、第四个参数用于去约束更新某一行或某几行中的数据,不指定的话默认就是更新所有行

修改 activity 中的代码,如下:

public class SQLiteActivity extends AppCompatActivity {
    
    
 
    private MyDatabaseHelper dbHelper;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sqlite);
 
        . . . 
 
        // 更新数据
        Button btn_update_data = (Button) findViewById(R.id.btn_update_data);
        btn_update_data.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View view) {
    
    
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                // 把名字为 The Da Vinci Code 的这本书的更新成666.66
                values.put("price",666.66);
                db.update("book",values, "name = ?", new String[]{
    
    "The Da Vinci Code"});
            }
        });
    }
}

3.3、删除数据

SQLiteDatabase 中提供了一个 delete()方法用于删除数据

这个方法接收三个参数,

  • 第一 个参数仍然是表名
  • 第二、第三个参数又是用于去约束删除某一 行或某几行的数据,不指定的话默认就是删除所有行

修改 activity 中的代码,如下:

public class SQLiteActivity extends AppCompatActivity {
    
    
 
    private MyDatabaseHelper dbHelper;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sqlite);
 
        . . . 
 
        // 删除数据
        Button btn_delete_data = (Button) findViewById(R.id.btn_delete_data);
        btn_delete_data.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View view) {
    
    
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                // 删除页数超过500页的书
                db.delete("book", "pages > ?", new String[]{
    
    "500"});
            }
        });
    }
}

3.4、查询数据

SQLiteDatabase 中提供了一个 query()方法用于查询数据

这个方法的参数非常复杂,最短的一个方法重载也需要传入七个参数

  • 第一个参数是表名
  • 第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列
  • 第三、第四个参数用于去约束查询某一行或某几行的数据,不指定则默认是查询所有行的数据
  • 第五个参数用于指定需要去 group by 的列,不指定则表示不对查询结果进行 group by 操作
  • 第六个参数用于对 group by 之后的数据进行进一步的过滤,不指定则表示不进行过滤
  • 第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式

调用 query() 方法后会返回一个 Cursor 对象,查询到的所有数据都将从这个对象中取出

修改 activity 中的代码,如下:

public class SQLiteActivity extends AppCompatActivity {
    
    
 
    private MyDatabaseHelper dbHelper;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sqlite);
 
        . . . 
 
        // 查询数据
        Button btn_query_data = (Button) findViewById(R.id.btn_query_data);
        btn_query_data.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View view) {
    
    
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                // 查询 book 表中所有数据
                Cursor cursor = db.query("book", null,null,null,null,null,null,null);
                if (cursor.moveToFirst()){
    
    
                    do {
    
    
                        // 遍历 Cursor 对象,取出数据并打印
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        double price = cursor.getDouble(cursor.getColumnIndex("price"));
 
                        Log.d("SQLiteActivity_query", "book name is: " + name);
                        Log.d("SQLiteActivity_query", "book author is: " + author);
                        Log.d("SQLiteActivity_query", "book pages are: " + pages);
                        Log.d("SQLiteActivity_query", "book price is: " + price);
                    }while (cursor.moveToNext());
                }
                cursor.close();
            }
        });
    }
}

4、使用 SQL 操作数据库

添加数据:

db.execSQL("insert into book (name, author, pages, price) values(?, ?, ?, ?)", new String[] {
    
     "The Da Vinci Code", "Dan Brown", "454", "16.96" }); 

db.execSQL("insert into book (name, author, pages, price) values(?, ?, ?, ?)",  new String[] {
    
     "The Lost Symbol", "Dan Brown", "510", "19.95" });

更新数据:

db.execSQL("update Book set price = ? where name = ?", new String[] {
    
     "666.66", "The Da Vinci Code" });

删除数据:

db.execSQL("delete from Book where pages > ?", new String[] {
    
     "500" });

查询数据:

db.rawQuery("select * from Book", null);

5、使用事务

前面我们已经知道,SQLite 数据库是支持事务的,事务的特性可以保证让某一系列的操作要么全部完成,要么一个都不会完成。那么在什么情况下才需要使用事务呢?想象以下场景,比如你正在进行一次转账操作,银行会将转账金额先从你的账户中扣除,然后再向收款方的账户中添加等量的金额。看上去好像没什么问题吧?可是,如果当你账户中的金额刚刚被扣除,这时由于一些异常导致对方收款失败,这一部分钱就凭空消息了!当然银行肯定已经充分考虑到了这种情况,它会保证扣钱和收款的操作要么一起成功,要么都不会成功,而使用的技术当然是事务了。

接下来我们看一看如何在 Android 中使用事务吧,仍然是在 DatabaseTest 项目的基础上进行修改。比如 Book 表中的数据都已经很老了,现在准备全部废弃替换成新数据,可以先使用 delete() 方法将 Book 表中的数据删除,然后再使用 insert() 方法将新的数据添加到表中。我们要保证的是,删除旧数据和添加新数据的操作必须一起完成,否则就还要继续保留原来的旧数据。修改 activity_main.xml 中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
 
    ......
 
   <Button 
        android:id="@+id/replace_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
		android:text="Replace data"        
        />
 
</LinearLayout>


可以看到,这里又添加了一个按钮,用于进行数据替换操作。然后修改 MainActivity 中的代码,如下所示:

public class MainActivity extends Activity {
    
    
 
	private MyDatabaseHelper dbHelper;
 
	@Override
	protected void onCreate(Bundle savedInstanceState) {
    
    
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
		......
		Button replaceData = (Button) findViewById(R.id.replace_data);
		replaceData.setOnClickListener(new OnClickListener() {
    
    
			@Override
			public void onClick(View v) {
    
    
				SQLiteDatabase db = dbHelper.getWritableDatabase();
				db.beginTransaction(); //开启事务
				try {
    
    
					db.delete("Book", null, null);
					if (true) {
    
    
                                            // 在这里手动抛出一个异常,让事务失败
                                           throw new NullPointerException();
					}
					ContentValues values = new ContentValues();
					values.put("name", "Game of Thrones");
					values.put("author", "George Martin");
					values.put("pages", 720);
					values.put("price", 20.85);
					db.insert("Book", null, values);
					db.setTransactionSuccessful();  // 事务已经执行成功
				} catch (Exception e) {
    
    
					e.printStackTrace();
				} finally {
    
    
					db.endTransaction();  // 结束事务
				}
			}
		});
	}
 
}

上述代码就是 Android 中事务的标准用法,首先调用 SQLiteDatabase 的 beginTransaction() 方法来开启一个事务,然后在一个异常捕获的代码块中去执行具体的数据库操作,当所有的操作都完成之后,调用 setTransactionSuccessful() 表示事务已经执行成功了,最后在 finally 代码块中调用 endTransaction() 来结束事务。注意观察,我们在删除旧数据的操作完成后手动抛出了一个 NullPointerException,这样添加新数据的代码就执行不到了。不过由于事务的存在,中途出现异常会导致事务的失败,此时旧数据应该是删除不掉的。

现在可以运行一下程序并点击 Replace data 按钮,你会发现,Book 表中存在的还是之前的旧数据。然后将手动抛出异常的那行代码去除,再重新运行一下程序,此时点击一下 Replace data 按钮就会将 Book 表中的数据替换成新数据了。

6、升级数据库的最佳写法

想象以下场景,比如你编写的某个应用已经成功上线,并且还拥有了不错的下载量。现在由于添加新功能的原因,使得数据库也需要一起升级,然后用户更新了这个版本之后发现以前程序中存储的本地数据全部丢失了!那么狠遗憾,你的用户群体可能已经流失一大半了。

听起来好像挺恐怖的样子,难道说在产品发布出去之后还不能升级数据库了?当然不是,其实只需要进行一些合理的控制,就可以保证在升级数据库的时候数据并不会丢失了。

下面我们就来学习一下如何实现这样的功能,你已经知道,每一个数据库版本都会对应一个版本号,当指定的数据版本号大于当前数据库版本号的时候,就会进入到 onUpgrade() 方法中去执行更新操作。这里需要为每一个版本号赋予它格子改变的内容,然后在 onUpgrade() 方法中对当前数据库版本号进行判断,再执行相应的改变就可以了。

接着就让我们来模拟一个数据库升级的案例,还是由 MyDatabaseHelper 类来对数据库进行管理。第一版的程序要求非常简单,只需要创建一张 Book 表,MyDatabaseHelper 中的代码如下所示:

public class MyDatabaseHelper extends SQLiteOpenHelper {
    
    
 
	public static final String CREATE_BOOK = "create table Book ("
			+ "id integer primary key autoincrement, " 
			+ "author text, "
			+ "price real, " 
			+ "pages integer, " 
			+ "name text)";
 
	public MyDatabaseHelper(Context context, String name,
			CursorFactory factory, int version) {
    
    
		super(context, name, factory, version);
	}
 
	@Override
	public void onCreate(SQLiteDatabase db) {
    
    
		db.execSQL(CREATE_BOOK);
	}
 
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
    
	}
 
}

不过,几星期之后又有了新需求,这次需要向数据库中再添加一张 Category 表,于是,修改 MyDatabaseHelper 中的代码,如下所示:

public class MyDatabaseHelper extends SQLiteOpenHelper {
    
    
 
	public static final String CREATE_BOOK = "create table Book ("
			+ "id integer primary key autoincrement, " 
			+ "author text, "
			+ "price real, " 
			+ "pages integer, " 
			+ "name text)";
	
	public static final String CREATE_CATEGORY = "create table Category ("
			+ "id integer primary key autoincrement, "
			+ "category_name text, "
			+ "category_code integer)";
 
	public MyDatabaseHelper(Context context, String name,
			CursorFactory factory, int version) {
    
    
		super(context, name, factory, version);
	}
 
	@Override
	public void onCreate(SQLiteDatabase db) {
    
    
		db.execSQL(CREATE_BOOK);
		db.execSQL(CREATE_CATEGORY);
	}
 
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
    
	      switch (oldVersion) {
    
    
            case 1:
            db.execSQL(CREATE_CATEGORY);
             default:
        }
     }
 
}

可以看到,在 onCreate() 方法里面我们新增了一条建表语句,然后又在 onUpgrade() 方法中添加了一个 switch 判断,如果用户当前数据库的版本号是 1,就只会创建一张 Category 表。这样当用户是直接安装的第二版的程序时,就会将两张表一起创建。而当用户是使用第二版的程序覆盖第一版的程序时,就会进入到升级数据库的操作,此时由于 Book 表已经存在了,因此只需要创建一张 Category 表即可。

但是没过多久,新的需求又来了,这次要给 Book 表和 Category 表之间建立关联,需要在 Book 表中添加一个 category_id 的字段。再次修改 MyDatabaseHelper 中的代码,如下所示:

public class MyDatabaseHelper extends SQLiteOpenHelper {
    
    
 
	public static final String CREATE_BOOK = "create table Book ("
			+ "id integer primary key autoincrement, " 
			+ "author text, "
			+ "price real, " 
			+ "pages integer, " 
			+ "name text"
			+ "category_id integer)";
	
	public static final String CREATE_CATEGORY = "create table Category ("
			+ "id integer primary key autoincrement, "
			+ "category_name text, "
			+ "category_code integer)";
 
	public MyDatabaseHelper(Context context, String name,
			CursorFactory factory, int version) {
    
    
		super(context, name, factory, version);
	}
 
	@Override
	public void onCreate(SQLiteDatabase db) {
    
    
		db.execSQL(CREATE_BOOK);
		db.execSQL(CREATE_CATEGORY);
	}
 
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
    
		switch (oldVersion) {
    
    
			case 1:
				db.execSQL(CREATE_CATEGORY);
			case 2:
				db.execSQL("alter table Book add column category_id integer");
			default:
		}
	}
 
}


可以看到,首先我们在 Book 表建表语句中添加了一个 category_id 列,这样当用户直接安装第三版的程序时,这个新增的列就已经自动添加成功了。然而,如果用户之前已经安装了某一版的程序,现在需要覆盖安装,就会进入到升级数据库的操作中。在 onUpgrade() 方法里,我们添加了一个新的 case,如果当前数据库的版本号是 2,就会执行 alter 命令来为 Book 表新增一个 category_id 列。

这里请注意一个非常重要的细节,switch 中的每一个 case 的最后都是没有使用 break 的,为什么要这么做呢?这是为了保证在跨版本升级的时候,每一次的数据库修改都能被全部执行到。比如用户当前是从第二版程序升级到第三版程序的,那么 case 2 中的逻辑就会执行。而如果用户是直接从第一版程序升级到第三版程序的,那么 case 1 和 case 2 中的逻辑都会执行。使用这种方式来维护数据库的升级,不管版本怎样更新,都可以保证数据库的表结构是最新的,而且表中的数据也完全不会丢失了。

参考

1、第一行代码之SQLite数据库存储
2、《第一行代码》 6.4 SQLite数据库存储
3、SQLite 数据库的最佳实践

猜你喜欢

转载自blog.csdn.net/JMW1407/article/details/121284599