使用SQLite保存数据

本文基本内容是对于官方文档Save data using SQLite的瞎JB翻译,以及优达学城安卓基础最后一门数据存储学习的记录。SQL过于底层,官方推荐Room库。

这篇文章里有两条时间线,一条是官网文档里的样例代码FeedReeder,还有一条是Udacity的课程项目Pet。

Step 1. Define a schema and contract

Schema 是SQL数据库的原则之一,他定义了数据库如何组织数据。Schema也反映在创建数据库的SQL语句中。您可能会发现创建一个伴随类(称为 contract class)会很有帮助,该类系统且明确地指定了schema的层次布局。

一些常量定义了URI,表和列的名称,contract类正是这些常量的容器。您可以在同一包的所有其他类中使用contract类中定义的常量。这样一来,修改表结构时,您只需在contract类中更改常量的值即可。

组织contract类的一种好方法是将数据库的全局定义放在类的根级别。然后为每个表创建一个内部类。每个内部类枚举相应表的列。

第一步大概的意思就是你得确定数据库的架构,写一个Contract类来体现这种架构(这不是必须的,但是写了会方便)。举一个栗子,裸写一个SQL语句大概长这样

String makeTableStatement = "CREATE TABLE entry(
			_id INTEGER PRIMARY KEY,
			entryid TEXT,
			title TEXT,
			sbutitle TEXT);";

然后看文档里的案例,我们先写一个contract类,现在只需要知道这里定义了一些常量。

// FeedReaderContract.class
public final class FeedReaderContract {
    // 防止被实例化,私有化构造函数
    private FeedReaderContract() {}

    /* 内部类定义表的内容,实现BaseColumns这个接口的时候
    会继承一个叫做 _ID 的主键 */
    public static class FeedEntry implements BaseColumns {
        public static final String TABLE_NAME = "entry";
        public static final String COLUMN_NAME_TITLE = "title";
        public static final String COLUMN_NAME_SUBTITLE = "subtitle";
    }
}

我们用上面那些常量再写一个SQL语句(一般会写在帮助类里面)

// 一般存在于 步骤2 会讲到的帮助类中
String SQL_CREATE_ENTRIES = "CREATE TABLE " +
		FeedEntry.TABLE_NAME + " (" +
		FeedEntry._ID + " INTEGER PRIMARY KEY," +
		FeedEntry.COLUMN_NAME_TITLE + " TEXT," +
		FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT);";

看上去便麻烦了,但是有智能提示啊!而且当你想修改某个字段时,只需要修改常量的值就可以。如果你不用常量,就得修改整个代码中所有用到这个字段的地方。毕竟我没那个神仙水平看得懂aaabbb000111ttuuvvbtn1234567

总结一下contract类的三个用处

  • 1、帮助定义了(体现了)schema,在这里定义了常量,我们可以方便地找到这些常量
  • 2、减少在SQL语句中的拼写错误
  • 3、便于更新

接下来,阅读代码,回答以下问题 (这个是课程中的一道习题)

  • 1.这个天气应用数据库里有多少张表?
  • 2.WeatherEntry对应的表,在SQLite数据库中的表名是什么?
  • 3.问题2那张表中有多少列?(不包括_ID和_COUNT)
  • 4.有一个常量对应的字段是weather condition的short description,这个常量是什么?
  • 2张表,58行位置表,90行天气表
  • weather,100行
  • 10,103-127行
  • COLUMN_SHORT_DESC,在111行

下面是课程中Pet项目的一些操作

create a contract class
添加一个Contract类
package com.example.android.pets.data;

import android.provider.BaseColumns;

public final class PetContract {
    private PetContract() {
    }

    public static abstract class PetEntry implements BaseColumns {
        public static final String TABLE_NAME = "pets";
        public static final String _ID = BaseColumns._ID;
        public static final String COLUMN_PET_NAME = "name";
        public static final String COLUMN_PET_BREED = "breed";
        public static final String COLUMN_PET_GENDER = "gender";
        public static final String COLUMN_PET_WEIGHT = "weight";

        public static final int GENDER_UNKNOWN = 0;
        public static final int GENDER_MALE = 1;
        public static final int GENDER_FEMALE = 2;
    }
}
readability
在其他类中,使用常量,增加可读性

Step 2. Create a database using an SQL helper

SQLiteOpenHelper类包含一组有用的API,用于管理数据库。当您使用此类获取对数据库的引用时,系统仅在需要时才执行创建和更新数据库的操作(可能花费很长时间运行),而不是在应用程序启动期间执行。您需要做的就是调用getWritableDatabase()或getReadableDatabase()。

要使用SQLiteOpenHelper,请创建一个重写onCreate()和onUpgrade()回调方法的子类。您可能还想实现onDowngrade()或onOpen()方法,但不是必需的。

// 1. 创建一个类继承 SQLiteOpenHelper
public class FeedReaderDbHelper extends SQLiteOpenHelper {
    // 2. 为数据库名称和数据库版本创建常量
    // 如果你修改了数据库 schema, 你必须增加 database version 的值.
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "FeedReader.db";

    // 还记得上面我们用常量写的SQL语句嘛,就是这里的一部分
    // 记得import FeedReaderContract.FeedEntry
    private static final String SQL_CREATE_ENTRIES =
            "CREATE TABLE " + FeedEntry.TABLE_NAME + " (" +
                    FeedEntry._ID + " INTEGER PRIMARY KEY," +
                    FeedEntry.COLUMN_NAME_TITLE + " TEXT," +
                    FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT)";

    private static final String SQL_DELETE_ENTRIES =
            "DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;

    // 3. 写一个构造函数
    public FeedReaderDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    // 4. 实现 OnCreate 回调函数
    public void onCreate(SQLiteDatabase db) {
        // void execSQL(String sql) 
        //执行,无返回值,不能用于查询等
        db.execSQL(SQL_CREATE_ENTRIES);
    }

    // 5. 实现 onUpgrade 回调函数
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 这个数据库只是在线数据的缓存,
        // 升级策略就是删掉数据,从头来过
        db.execSQL(SQL_DELETE_ENTRIES);
        onCreate(db);
    }
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        onUpgrade(db, oldVersion, newVersion);
    }
}

类似的,实现PetDbHelper类

public final class PetDbHelper extends SQLiteOpenHelper {

    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "shelter.db";

    private static final String SQL_CREATE_ENTRIES =
            "CREATE TABLE " + PetEntry.TABLE_NAME + "( " +
                    PetEntry._ID + " INTEGER PRIMARY KEY," +
                    PetEntry.COLUMN_PET_NAME + " TEXT," +
                    PetEntry.COLUMN_PET_BREED + " TEXT NOT NULL," +
                    PetEntry.COLUMN_PET_GENDER + " INTEGER NOT NULL," +
                    PetEntry.COLUMN_PET_WEIGHT + " INTEGER NOT NULL DEFAULT 0);";
    private static final String SQL_DELETE_ENTRIES =
            "DROP TABLE IF EXISTS " + PetEntry.TABLE_NAME;

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

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(SQL_CREATE_ENTRIES);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        sqLiteDatabase.execSQL(SQL_DELETE_ENTRIES);
        onCreate(sqLiteDatabase);
    }
}

没有权限就先复制一个临时的类,用来展示信息,仔细阅读代码就知道怎么操作数据库了

    /**
     * Temporary helper method to display information in the onscreen TextView about the state of
     * the pets database.
     */
    private void displayDatabaseInfo() {
        // To access our database, we instantiate our subclass of SQLiteOpenHelper
        // and pass the context, which is the current activity.
        PetDbHelper mDbHelper = new PetDbHelper(this);

        // Create and/or open a database to read from it
        SQLiteDatabase db = mDbHelper.getReadableDatabase();

        // Perform this raw SQL query "SELECT * FROM pets"
        // to get a Cursor that contains all rows from the pets table.
        Cursor cursor = db.rawQuery("SELECT * FROM " + PetEntry.TABLE_NAME, null);
        try {
            // Display the number of rows in the Cursor (which reflects the number of rows in the
            // pets table in the database).
            TextView displayView = (TextView) findViewById(R.id.text_view_pet);
            displayView.setText("Number of rows in pets database table: " + cursor.getCount());
        } finally {
            // Always close the cursor when you're done reading from it. This releases all its
            // resources and makes it invalid.
            cursor.close();
        }
    }

Step 3. CRUD

3.1 Create

// Gets the data repository in write mode
// 数据库不存在时会调用dhHelper中的onCreate方法,返回一个可写的数据库对象
SQLiteDatabase db = dbHelper.getWritableDatabase();

// Create a new map of values, where column names are the key
// 创建键值对的映射
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle);

// Insert the new row, returning the primary key value of the new row
// 插入新的一行数据,返回新纪录主键的值
long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);

向数据库插入一条记录,只需向insert方法传递一个ContentValues对象。

很明显insert()方法的第一个参数是表名。第二个参数告诉框架当ContentValues内容为空时要做什么(比如你没有放任何键值对进去)。If you specify the name of a column, the framework inserts a row and sets the value of that column to null. If you specify null, like in this code sample, the framework does not insert a row when there are no values.(反正我看不懂他什么意思,框架源码的链接可能有帮助,insertWithOnConflict方法,和SQLiteStatement的父类SQLiteProgram)

insert()方法会返回新插入行的ID,或者在插入失败时返回-1.

3.2 Read

        // Create and/or open a database to read from it
        SQLiteDatabase db = mDbHelper.getReadableDatabase();
        // 写一个投影(选择列)
        String[] projection = {
                PetEntry._ID,
                PetEntry.COLUMN_PET_NAME,
                PetEntry.COLUMN_PET_BREED,
                PetEntry.COLUMN_PET_GENDER,
                PetEntry.COLUMN_PET_WEIGHT
        };
        // 用query()方法来选择,前四个参数比较重要,具体请参见官方文档
        // 三四两个参数用来选择行一般就是where,且用来访防止sql注入
        // 看到我敷衍的语气了嘛,这篇文章估计马上就要咕咕咕了
        Cursor cursor = db.query(PetEntry.TABLE_NAME,
                projection,
                null,null,null,null,null);
        try {
            // do something with cursor
        } finally {
            // Always close the cursor when you're done reading from it. This releases all its
            // resources and makes it invalid.
            cursor.close();
        }

3.3 Update

3.4 Delete

更新和删除也差不多,然而我直接奔向了Content Provider,害,啥时候能上Room啊。

JS30-Day13

Day13 – 图片随屏幕滚动而滑入滑出的效果

  • 首先获取触发动画的位置,在滚动到图片一半的位置时触发。 
    const slideAt = window.innerHeight + window.scrollY - sliderimage.height/2; 
    • window.innerHeight表示浏览器的内部视图窗口的高度值
    • window.scrollY表示浏览器当前的在Y轴上滚动的距离(未滚动时值为0),也可通过采用window.scroll(X,Y)方法,设置页面在X轴和Y轴上面的滚动值
  • 再获取图片底部到页面文档顶端的距离,采用const imageBottom = sliderimage.offsetTop + sliderimage.height; 
    • sliderimage.offsetTop表示该图片最上面的值,到页面文档顶端的距离,再加上该图片的高度,就是图片底部到页面文档顶端的距离
  • 设置两个flag,分别表示图片是否显示了一半和图片是否已经被完全滚动出去了,分别为const isHalfShown = slideAt > sliderimage.offsetTop;const isNotScrolledPast = window.scrollY < imageBottom;
  • 只有当图片已经显示了一半并且没有被图片没有被滚动出窗口是,图片才会显示出来,此处的动画处理方式如下:默认时将图片向左或向右移动30%,当图片出现在窗口中时,取消该图片的移动,显示在原位置;再加上transition: all .5s;,在图片出现的时候,就会显示出约0.5秒的过渡动画。

很明显上面这段是我复制的,前人已经做好了总结我就不再赘述了,这几天都些匆匆忙忙,成品我自己还没来得急敲,先从调整作息开始吧,今天就早些睡了。

今天了解了ST表,区间最值查询(RMQ)还是得多练习一下。