1. 程式人生 > >Android五種資料儲存方式之SQLite資料庫儲存 載入SD卡資料庫 sql操作 事務 防止SQL注入

Android五種資料儲存方式之SQLite資料庫儲存 載入SD卡資料庫 sql操作 事務 防止SQL注入

資料庫

前言

眾所周知,資料儲存在每個應用中都會用到,那所用到的技術應該怎麼選呢,這裡Android給開發者提供了幾種方法去儲存常用應用資料,至於你想選擇哪一種方式,取決於你的特定需求;例如這個資料是本應用私有的還是跟其它應用共享以及資料儲存所需的記憶體空間等

  • Shared Preferences:這種方式通常用來儲存私有原始資料,以鍵值對形式儲存;這也就意味著這些資料只能由本應用訪問
  • Internal Storage:這種方式是將私有資料儲存在內部儲存(裝置記憶體)中,實際上是使用檔案流進行讀寫
  • External Storage:這種方式是將公共資料儲存在共享外部儲存上;也就是將資料儲存在SD卡上,儲存在這上面說明資料是開放的,其它應用可以直接訪問;這跟上面一種都是平常所說的檔案儲存
  • SQLite Databases:這種方式是將結構化資料儲存在私有資料庫中;這也就是常說的資料庫儲存,使用SQLite儲存資料,這些資料是私有的
  • Network Connection:這種方式是將資料儲存在Web伺服器上,也就是通常所說的網路儲存

筆者上一篇文章講述了第二種和第三種方式的使用封裝,這篇文章來掰掰第四種方式,也就是資料庫儲存

資料庫儲存

本文所含程式碼隨時更新,可從這裡下載最新程式碼
傳送門Mango

做Android開發的絕大多數應該都用過Sqlite了,開發者可以使用它來儲存應用私有資料,這裡簡單介紹下Sqlite

  • SQLite是一個程序內庫:它實現了一個獨立的,無伺服器,零配置的事務SQL資料庫引擎,且支援SQL語句
  • SQLite是一個嵌入式SQL資料庫引擎:與大多數其他SQL資料庫不同,SQLite沒有單獨的伺服器程序; SQLite直接讀寫普通磁碟檔案;資料庫檔案格式是跨平臺的,可以在32位和64位系統之間自由複製資料庫
  • SQLite是一個緊湊的庫:啟用所有功能後,庫大小可能小於600KiB,具體取決於目標平臺和編譯器優化設定;通常情況下給與的記憶體越多,執行越快;即使在低記憶體環境中,效能通常也非常好,SQLite可以比檔案系統I / O更快
  • Sqlite最重要的一點是開源的

現在一些主流的移動裝置,比如Android,iPhone,pad等都在使用Sqlite儲存資料,這也就意味著我們必須要掌握SQLite的開發

其實SQLite還有一個最大的特點是你可以把各種型別的資料儲存到任何欄位中,而不用關心欄位宣告的資料型別是什麼;一般其它資料庫採用的是靜態資料型別,即你定義的欄位型別是什麼,那存入的資料就得是相應的資料型別;但是SQLite採用的是動態資料型別,會根據存入值自動判斷;比如可以在Integer型別的欄位中存放字串,或者在布林型欄位中存放浮點數,或者在字元型欄位中存放日期型值;但有一種情況例外:定義為INTEGER PRIMARY KEY的欄位只能儲存64位整數, 當向這種欄位儲存除整數以外的資料時,將會產生錯誤

資料庫建立

內建儲存資料庫

建立新SQLite資料庫的推薦方法是建立SQLiteOpenHelper的子類並覆蓋onCreate()方法,在該方法中可以執行SQLite命令以在資料庫中建立表

/**
 * @Description TODO(資料庫及表建立)
 * @author cxy
 * @Date 2018/11/5 11:07
 */
public class SQLiteDBHelper extends SQLiteOpenHelper{

    //類還沒有例項化,只能是static修飾才能用來做引數
    private static final String DATABASE_NAME = "mango";
    private static final String TABLE_USER = "user";
    private static final int DATABASE_VERSION = 1;

    /**
     * 過載構造方法
     * @param context
     */
    public SQLiteDBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    /**
     * 構造方法
     * @param context 上下文
     * @param name 資料庫名
     * @param factory 遊標工廠,預設為null,即使用預設工廠
     * @param version 資料庫版本號
     */
    public SQLiteDBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    /**
     * 資料庫第一次建立時被呼叫
     * 用來建立表
     * 或者一些資料初始化操作
     * @param db
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        String dataBaseSql = "create table if not exists " + TABLE_USER+ "(uid integer primary key autoincrement,name varchar(20),sex varchar,role varchar(10))";
        db.execSQL(dataBaseSql);
    }

    /**
     * 升級軟體時更新資料庫表結構
     * 通過比對版本號
     * 一般做刪除資料表,並建立新的資料表操作
     * @param db
     * @param oldVersion 上一次版本號
     * @param newVersion 最新版本號
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }

    /**
     * 開啟一個只讀資料庫
     * 如果資料庫不存在,Android系統會自動生成一個數據庫,接著呼叫onCreate()方法
     * @return 獲取一個用於操作資料庫的SQLiteDatabase例項
     */
    @Override
    public SQLiteDatabase getReadableDatabase() {
        return super.getReadableDatabase();
    }

    /**
     * 建立或開啟一個讀寫資料庫
     * 如果資料庫不存在,Android系統會自動生成一個數據庫,接著呼叫onCreate()方法
     * @return 獲取一個用於操作資料庫的SQLiteDatabase例項
     */
    @Override
    public SQLiteDatabase getWritableDatabase() {
        return super.getWritableDatabase();
    }
}
  • SQLiteOpenHelper是Android提供的一個供開發者使用SQLiteDatabase的輔助類,用來管理資料庫及表的建立和版管理
  • 重寫構造方法,傳遞必要的引數,在這裡資料庫還沒有建立,只是構建一個用於建立,開啟或者管理資料庫的輔助類例項
  • 重寫onCreate方法,當資料庫第一次建立時,這個方法被呼叫(不需要使用者手動呼叫),用來建立表和初始化資料
  • 重寫onUpgrade方法,當資料庫版本變化時被呼叫;比如淘寶APP要升級,同時一些表結構變了,像表User多了一個欄位,但是使用者手機上還是原來的資料庫,表User沒有新的欄位,那這時候APP要想正常工作就需要修改版本號,在這個方法中去對錶User做一些修改
  • getReadableDatabase和getWritableDatabase方法都是用來獲取一個用於操作資料庫的SQLiteDatabase例項,但getWritableDatabase() 方法以讀寫方式開啟資料庫,一旦資料庫的磁碟空間滿了,資料庫就只能讀而不能寫,倘若使用getWritableDatabase()開啟資料庫就會出錯;getReadableDatabase()方法先以只讀方式開啟資料庫,如果資料庫的磁碟空間滿了,就會開啟失敗,當開啟失敗後會繼續嘗試以只讀方式開啟資料庫
  • 資料庫真正的建立是在呼叫getReadableDatabase和getWritableDatabase兩個方法的時候,建立完後才會回撥onCreate方法建立表;並且這兩個方法在資料庫upgrade時可能會花費較長時間,不要在主執行緒呼叫

注意:在這裡有一個版本號的規範問題,在API24中,在開啟或者建立資料庫時,如果之前的資料庫版本號與最新的版本號不一致,同時之前的版本號是0,那隻會回撥onCreate方法(這也是首次使用預設歷史版本號為0);如果經過了N個版本更新後,新的版本號比歷史版本號大,那就回調onUpgrade方法;如果新的版本號小於歷史版本號,那就回調onDowngrade方法,這個方法有一個預設實現是丟擲異常;所以編寫版本號的時候一定要注意

public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        throw new SQLiteException("Can't downgrade database from version " +
                oldVersion + " to " + newVersion);
}

外接儲存資料庫

上面方法建立資料庫,db檔案是存放在內建儲存中,路徑在data/data/包名/databases 目錄下,如果某些情況下出現問題需要獲取資料庫檔案進行分析,那顯然操作內建儲存是很不方便的,並且內建儲存記憶體空間有限,對資料儲存限制比較大;這時候我們就可以在外接儲存中建立資料庫檔案,通常做法是提前使用工具建立好資料庫檔案,並建立好相關表,然後放到工程assets目錄或者raw目錄下,在程式執行時將db檔案拷貝到SD卡,再通過SQLiteOpenHelper訪問這個資料庫

這裡推薦大家一款SQLite視覺化工具SQLiteManager,可用它來建立一個db檔案,並且建立你需要的表
我這裡建立了一個mango.db資料庫檔案,並且建立了一個user表,欄位與上面一樣

那接下來怎麼讓程式使用到這個SD卡上的db檔案呢?如果無從下手,那就看看預設的getReadableDatabase和getWritableDatabase方法是怎麼建立資料庫的

不管是getReadableDatabase還是getWritableDatabase方法,都沒做啥處理,將邏輯全部轉到了getDatabaseLocked方法,那就來看看這個方法(API24)

private SQLiteDatabase getDatabaseLocked(boolean writable) {

        ......

        SQLiteDatabase db = mDatabase;
        try {
            mIsInitializing = true;

            if (db != null) {
                if (writable && db.isReadOnly()) {
                    db.reopenReadWrite();
                }
            } else if (mName == null) {
            	//如果傳入的資料庫名是null,那就使用預設路徑建立資料庫
                db = SQLiteDatabase.create(null);
            } else {
                try {
                    if (DEBUG_STRICT_READONLY && !writable) {
                        final String path = mContext.getDatabasePath(mName).getPath();
                        db = SQLiteDatabase.openDatabase(path, mFactory,
                                SQLiteDatabase.OPEN_READONLY, mErrorHandler);
                    } else {
                    	//如果名稱不為null,那就構建data/data/包名/databases/mName路徑
                        db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ?
                                Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0,
                                mFactory, mErrorHandler);
                    }
                } catch (SQLiteException ex) {
                    if (writable) {
                        throw ex;
                    }
                    final String path = mContext.getDatabasePath(mName).getPath();
                    db = SQLiteDatabase.openDatabase(path, mFactory,
                            SQLiteDatabase.OPEN_READONLY, mErrorHandler);
                }
            }

            onConfigure(db);
						
			//這裡根據版本號判斷是否需要資料庫升級還是降級
            final int version = db.getVersion();
            if (version != mNewVersion) {
                if (db.isReadOnly()) {
                    throw new SQLiteException("Can't upgrade read-only database from version " +
                            db.getVersion() + " to " + mNewVersion + ": " + mName);
                }

                db.beginTransaction();
                try {
                    if (version == 0) {
                        onCreate(db);
                    } else {
                        if (version > mNewVersion) {
                            onDowngrade(db, version, mNewVersion);
                        } else {
                            onUpgrade(db, version, mNewVersion);
                        }
                    }
                    db.setVersion(mNewVersion);
                    db.setTransactionSuccessful();
                } finally {
                    db.endTransaction();
                }
            }

            onOpen(db);

            if (db.isReadOnly()) {
                Log.w(TAG, "Opened " + mName + " in read-only mode");
            }

            mDatabase = db;
            return db;
        } finally {
            mIsInitializing = false;
            if (db != null && db != mDatabase) {
                db.close();
            }
        }
    }

這裡可以看到一句很重要的話就是當mName不為null的時候,呼叫

mContext.openOrCreateDatabase

這裡的mContext是我們在構造SQLiteOpenHelper傳進來的,它是Activity的上下文,指的是ContextWrapper,具體實現是ContextImpl(具體可以參考博主的從Activity載入原始碼深入理解ActivityThrad的工作邏輯),進到這個類看看

@Override
    public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory) {
        return openOrCreateDatabase(name, mode, factory, null);
    }

    @Override
    public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory,
            DatabaseErrorHandler errorHandler) {
        checkMode(mode);
        File f = getDatabasePath(name);//構建資料庫檔案
        int flags = SQLiteDatabase.CREATE_IF_NECESSARY;
        if ((mode & MODE_ENABLE_WRITE_AHEAD_LOGGING) != 0) {
            flags |= SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING;
        }
        if ((mode & MODE_NO_LOCALIZED_COLLATORS) != 0) {
            flags |= SQLiteDatabase.NO_LOCALIZED_COLLATORS;
        }
        SQLiteDatabase db = SQLiteDatabase.openDatabase(f.getPath(), factory, flags, errorHandler);
        setFilePermissionsFromMode(f.getPath(), mode, 0);
        return db;
    }

這裡表明預設情況下資料庫檔案是通過ContextWrapper的實現類建立的,並且是存放在內建儲存中,路徑data/data/包名/databases/mName

那解決方法也就出來了,我們只要定義一個類,繼承ContextWrapper,重寫openOrCreateDatabase方法,獲取SD卡上的db檔案路徑,然後再呼叫SQLiteDatabase類的openOrCreateDatabase方法即可獲取SQLiteDatabase例項,然後返回;
這樣操作以後,getDatabaseLocked方法中的mContext.openOrCreateDatabase具體實現就是我們編寫的這個方法了

/**
 * @Description TODO(載入SD卡上的db)
 * @author cxy
 * @Date 2018/11/5 17:04
 */
public class DataBaseContext extends ContextWrapper{

    public DataBaseContext(Context base) {
        super(base);
    }

    /**
     * 重寫這個方法,載入SD卡的資料庫
     * @param name
     * @param mode
     * @param factory
     * @param errorHandler
     * @return
     */
    @Override
    public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler) {
        String dbPath = FileStorageTools.getInstance(this).makeFilePath("/Android/data/"+getPackageName()+"/database/"+SQLiteDBHelper.DATABASE_NAME+".db");
        File file = new File(dbPath);
        if (!file.exists()) throw new NullPointerException("DB File is not exists");
        SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(dbPath,null);
        return database;
    }
}

其中的FileStorageTools類可檢視博主上一遍文章

接下來只需要在例項化SQLiteDBHelper的時候傳遞這個自定義的Context物件就可以了

DataBaseContext baseContext = new DataBaseContext(context);
SQLiteDBHelper mHelper = new SQLiteDBHelper(baseContext);

通過SQLiteDatabase 物件的getPath()方法,你就知道你使用的資料庫存放在哪了

編寫DAO

我們先寫個父類


/**
 * @Description TODO()
 * @author cxy
 * @Date 2018/11/5 18:12
 */
public class BaseDao {

    protected WeakReference<DataBaseContext> mContext;
    protected SQLiteDBHelper mHelper;

    public BaseDao(Context context) {
        mContext = new WeakReference<>(new DataBaseContext(context));
        mHelper = new SQLiteDBHelper(baseContext);
    }
}

我們前面建立了一個user表,然後再編寫對應的dao

/**
 * @Description TODO(提供對user的操作)
 * @author cxy
 * @Date 2018/11/5 14:50
 */
public class UserDao extends BaseDao{

    private String TAG = UserDao.class.getSimpleName();

    public UserDao(Context context) {
        super(context);
    }

    public void addUser(User user){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        String sql = "insert into " + SQLiteDBHelper.TABLE_USER + "(uid,name,sex,role) values(?,?,?,?)";
        database.execSQL(sql,new String[]{user.getUid(),user.getName(),user.getSex(),user.getRole()});
        database.close();
    }

    /**
     *
     * @param user
     * @return 插入的新紀錄的行號,即是第多少行
     */
    public long addUserValues(User user){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put("uid",user.getUid());
        values.put("name",user.getName());
        values.put("sex",user.getSex());
        values.put("role",user.getRole());
        long rowNum = database.insert(SQLiteDBHelper.TABLE_USER,null,values);
        database.close();
        return rowNum;
    }

    public void updateUser(String uid,String name,String sex){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        String sql = "update " + SQLiteDBHelper.TABLE_USER + " set name = ? where uid = ? and sex = ?";
        database.execSQL(sql,new String[]{name,uid,sex});
        database.close();
    }

    public void updateMoreUser(String uid,String uid2,String uid3,String name){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        String sql = "update " + SQLiteDBHelper.TABLE_USER + " set name = ? where uid in (?,?,?)";
        database.execSQL(sql,new String[]{name,uid,uid2,uid3});
        database.close();
    }

    /**
     *
     * @param sex
     * @param name
     * @return 受影響的行數總和
     */
    public int updateUserValues(String sex,String name){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put("name",name);
        int rowNum = database.update(SQLiteDBHelper.TABLE_USER,values,"sex = ?",new String[]{sex});
        database.close();
        return rowNum;
    }

    public void delUser(String uid){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        String sql = "delete from " + SQLiteDBHelper.TABLE_USER + " where uid = ?";
        database.execSQL(sql,new String[]{uid});
        database.close();
    }

    public void delAllUser(){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        String sql = "delete from " + SQLiteDBHelper.TABLE_USER ;
        database.execSQL(sql);
        database.close();
    }

    /**
     *
     * @param sex
     * @return  受影響的行數總和
     */
    public int delUserValues(String sex){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        int rowNum = database.delete(SQLiteDBHelper.TABLE_USER,"sex = ?",new String[]{sex});
        database.close();
        return rowNum;
    }

    /**
     *
     * @return 受影響的行數總和
     */
    public int delALLUserValues(){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        int rowNum = database.delete(SQLiteDBHelper.TABLE_USER,null,null);
        database.close();
        return rowNum;
    }

    public User getUser(String uid){
        User user = new User();
        SQLiteDatabase database = mHelper.getWritableDatabase();
        String sql = "select * from "+ SQLiteDBHelper.TABLE_USER + " where uid = ?";
        Cursor cursor = database.rawQuery(sql,new String[]{uid});
        while (cursor.moveToNext()) {
            user.setUid(cursor.getInt(cursor.getColumnIndex("uid"))+"");
            user.setName(cursor.getString(cursor.getColumnIndex("name")));
            user.setSex(cursor.getString(cursor.getColumnIndex("sex")));
            user.setRole(cursor.getString(cursor.getColumnIndex("role")));
        }
        cursor.close();
        database.close();
        return user;
    }


}
/**
 * @Description TODO()
 * @author cxy
 * @Date 2018/11/5 14:53
 */
public class User {

    private String uid;
    private String name;
    private String sex;
    private String role;

    public String getUid() {
        return uid;
    }

    public void setUid(String uid) {
        this.uid = uid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    @Override
    public String toString() {
        return "User{" +
                "uid='" + uid + '\'' +
                ", name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", role='" + role + '\'' +
                '}';
    }
}

在應用啟動的時候要記得將db檔案拷貝到SD卡

private void moveDBFile(){
        boolean isDbFileExist = true;
        File dbFile;
        String path = "/Android/data/"+getPackageName()+"/database/";
        File parent = new File(FileStorageTools.getInstance(this).makeFilePath(path));
        if (!parent.exists()) {
            parent.mkdirs();
            isDbFileExist = false;
            dbFile = FileStorageTools.getInstance(this).makeFile(parent, SQLiteDBHelper.DATABASE_NAME+".db");
        } else {
            dbFile = FileStorageTools.getInstance(this).makeFile(parent, SQLiteDBHelper.DATABASE_NAME+".db");
            if (!dbFile.exists()) {
                isDbFileExist = false;
            }
        }
        if (!isDbFileExist) {
            FileStorageTools.getInstance(this).moveResourceFileToExternalStorage(dbFile,FileStorageTools.getInstance(this).getStreamFromRaw(R.raw.mango));
        }
    }

我這裡簡單寫了幾個資料庫操作的方法,資料庫操作無外乎增刪改查,這些操作SQLiteDatabase 都提供了api供我們使用,我們重點要掌握它的execSQL()和rawQuery()方法; execSQL()方法可以執行insert、delete、update和CREATE TABLE等有更改行為的SQL語句; rawQuery()方法用於執行select語句

假如你不會編寫Sql語句,不用擔心,Android很貼心的, SQLiteDatabase專門提供了對應於新增、刪除、更新、查詢的操作方法: insert()、delete()、update()和query();你只需要傳遞相應的引數就能完成資料庫操作,有沒有很激動;對於熟悉SQL語法的程式設計師而言,直接使用execSQL()和rawQuery()方法執行SQL語句其實更方便

執行SQL語句的方法有了,那就要學會編寫相應的SQL語句了

插入操作

  • 它的SQL語句格式:insert into 表名(欄位列表) values(值列表)

舉例:

insert into user(uid,name,sex,role) values("1","tom","1","2")

這種寫法就是一個欄位對應一個值,但是實際上這種寫法不利於開發效率,當欄位多的時候,你需要對語句進行拼接,很容易出現錯誤,而且使得語句變得非常長,不利於維護;同時,這些值有可能是使用者輸入的,裡面可能會包含特殊符號,比如單引號,&類似的符號,為了保證語句正確執行,你必須對每個欄位進行轉義,對每條語句都這樣操作很麻煩

針對這種問題,SQLiteDatabase類提供了一個過載後的execSQL(String sql, Object[] bindArgs)方法,使用這個方法可以解決前面提到的問題,因為這個方法支援使用佔位符引數(?)

我們就可以這樣寫了

insert into user(uid,name,sex,role) values(?,?,?,?)
SQLiteDatabase.execSQL(sql,new String[]{user.getUid(),user.getName(),user.getSex(),user.getRole()});
  • 另一種非SQL語句寫法
ContentValues values = new ContentValues();
values.put("uid",user.getUid());
values.put("name",user.getName());
values.put("sex",user.getSex());
values.put("role",user.getRole());
database.insert(SQLiteDBHelper.TABLE_USER,null,values);

insert方法需要三個引數

  • 第一個引數:要插入資料的表的名稱
  • 第二個引數:指定空值欄位的名稱;當第三個引數為空或者裡面沒有內容的時候,我們insert是會失敗的(底層資料庫不允許插入一個空行),構建成的sql語句:insert into user() values(),顯然這不滿足標準SQL的語法;為了防止這種情況,我們要在這裡指定一個列名,到時候如果發現第三個引數為null或者其元素個數為0,就會將你指定的這個列名的值設為null,然後再向資料庫中插入,這時候構建成的語句:insert into user(name) values(NULL)
  • 第三個引數:一個ContentValues物件,類似一個map,通過鍵值對的形式儲存值,key為列名,values為值

第三個引數如果不為null且元素個數不為0,也就是通過putXXX方法儲存了值,那第二個引數就可以為null

更新操作

  • 它的SQL語句格式:update 表名 set 欄位名=值 where 條件子句

舉例:

update user set name = ? where uid = ?
database.execSQL(sql,new String[]{name,uid});//執行

這就是將指定uid的一行記錄中的name欄位更新成指定值

這裡條件表示式可以用and或者or來表示

//這個表示兩個條件必須都滿足才更新行記錄
String sql = "update user  set name = ? where uid = ? and sex = ?";
database.execSQL(sql,new String[]{name,uid,sex});
//只要有一個條件滿足就更新行記錄
String sql = "update user  set name = ? where uid = ? or sex = ?";
database.execSQL(sql,new String[]{name,uid,sex});

有時候條件表示式中一個欄位有多個值範圍的,比如你要更新uid等於1,2,3,4的多個行記錄,這時候要用到 in

String sql = "update user  set name = ? where uid in (?,?,?)";
database.execSQL(sql,new String[]{name,uid,uid2,uid3});
  • 非SQL語句寫法
public int updateUserValues(String uid,String name){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put("name",name);
        int rowNum = database.update(SQLiteDBHelper.TABLE_USER,values,"sex = ?",new String[]{sex});
        database.close();
        return rowNum;
}

這裡通過呼叫SQLiteDatabase 的update方法

  • 第一個引數是表名
  • 第二個引數是要修改的列名對應的值(也就是每行記錄中欄位對應的值)
  • 第三個引數就是判斷條件(把SQL語句中的where後面的拿過來就可以了)
  • 第四個引數是條件對應的值

刪除操作

  • 它的SQL語句格式:delete from 表名 where 條件子句

舉例:

String sql = "delete from user where uid = ?";
database.execSQL(sql,new String[]{uid});

條件表示式同更新操作中一樣

如果要刪除所有資料,去掉條件表示式即可

String sql = "delete from user  " ;
database.execSQL(sql);
  • 非SQL語句寫法
public int delUserValues(String uid){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        int rowNum = database.delete(SQLiteDBHelper.TABLE_USER,"uid = ?",new String[]{uid});
        database.close();
        return rowNum;
}

delete方法引數與更新操作中一樣

如果要刪除全部資料,第二個引數和第三個引數傳入null

public int delALLUser(){
        SQLiteDatabase database = mHelper.getWritableDatabase();
        int rowNum = database.delete(SQLiteDBHelper.TABLE_USER,null,null);
        database.close();
        return rowNum;
}

注意:update和delete方法的返回值表明影響的行數總和,insert方法返回值是新插入行的行ID,如果發生錯誤,則為-1

查詢操作

毫無疑問,查詢操作應該是用的最頻繁的也是最複雜多變的一個操作了,SQLiteDatabase給我們提供了query和rawQuery兩類查詢方法,它們都會返回一個Cursor物件,Cursor是一個遊標介面,代表資料集的遊標,提供了遍歷查詢結果的方法,它的API如下

Cursor API

  • int getCount() :獲得查詢後的資料總條數
  • boolean isFirst():游標是否指向第一個條目
  • boolean isLast():游標是否指向最後一個條目
  • boolean isBeforeFirst():游標是否指向第一行之前的位置
  • boolean isAfterLast():游標是否指向最後一行之後的位置
  • boolean isNull(int columnIndex):指定列資料是否為null
  • boolean move(int offset):將游標從當前位置向前或向後移動相對量。 正偏移向前移動,負偏移向後移動。 如果最終位置超出結果集的邊界,則結果位置將固定為-1或count();返回值是是否移動成功
  • boolean moveToPosition(int position):將游標移動到絕對位置。 有效值範圍是-1 <= position <= count;返回值是是否移動成功
  • boolean moveToFirst():將游標移動到第一行,返回值是是否移動成功
  • boolean moveToLast():將游標移動到最後一行,返回值是是否移動成功
  • boolean moveToNext():將游標移動到下一行,返回值是是否移動成功
  • boolean moveToPrevious():將游標移動到上一行,返回值是是否移動成功
  • int getColumnIndex(String columnName):返回給定列名稱的從零開始的索引,如果該列不存在,則返回-1
  • int getInt(int columnIndex):返回當前行指定列的值
  • long getLong(int columnIndex):同上
  • float getFloat(int columnIndex):同上
  • String getString(int columnIndex):同上

查詢方法

再看幾個查詢方法

  • public Cursor rawQuery(String sql, String[] selectionArgs)

這第一個引數是執行查詢的SQL語句,第二個是語句中對應占位符的值

  • public Cursor query(String table, String[] columns, String selection,String[] selectionArgs, String groupBy, String having, String orderBy)

    • table:表名
    • columns:需要返回的列,傳入null將返回所有列(儘量不要傳入null,將會從消耗多餘的效能)
    • selection:查詢條件子句,相當於select語句where關鍵字後面的部分,在條件子句允許使用佔位符“?”
    • selectionArgs:對應於selection語句中佔位符的值,值在陣列中的位置與佔位符在語句中的位置必須一致,否則就會有異常
    • groupBy:用於設定返回行的分組方式,傳遞null表示返回的行不會被分組;相當於select語句group by關鍵字後面的部分
    • having:決定哪一行被放到Cursor中的過濾器,相當於select語句having關鍵字後面的部分;傳遞null會導致所有的行都包含在內,前提是groupBy屬性也設定為null
    • orderBy:用於設定行的排列方式(依據列值),相當於select語句order by關鍵字後面的部分;傳遞null表示使用預設的排序方式,可能是無序排列
  • public Cursor query(String table, String[] columns, String selection,String[] selectionArgs, String groupBy, String having,
    String orderBy, String limit)

    這裡比上面方法多了一個引數limit,設定query語句返回行的數量,執行分頁功能,相當於select語句limit關鍵字後面的部分;傳遞null表示沒有設定limit語句;注意格式為String,傳遞的時候需要傳遞數字字串,例如“10”

  • public Cursor query(boolean distinct, String table, String[] columns,String selection, String[] selectionArgs, String groupBy,
    String having, String orderBy, String limit)

    這個方法比上面的又多了一個引數distinct,是用來去重的,設定為true,每一行的資料必須唯一

查詢樣例

它的SQL語句格式:select column1, column2,…columnN from 表名 where 條件子句 group by 分組字句 having … order by 排序子句 (順序一定不能錯)

舉例:

  • where
//查詢user表所有記錄
select * from user 

//查詢uid=2的記錄
select * from user where uid = 2

//查詢uid大於2的記錄
select * from user where uid > 2

//查詢uid等於2或者3的記錄
select * from user where uid in (2,3)

//查詢uid既不是 2也不是 3的記錄
select * from user where uid not in (2,3)

//查詢uid在2和5之間的記錄
select * from user where uid between 2 and 5

//查詢uid等於2且sex=1的記錄
select * from user where uid =2 and sex = 1

//查詢uid等於2或者且sex=1的記錄
select * from user where uid =2 or sex = 1

//查詢name以jack開頭的記錄
select * from user where name like 'jack%'

//查詢name以jack結尾的記錄
select * from user where name like '%jack'

//查詢name包含jack的記錄
select * from user where name like '%jack%'

  • limit
//查詢user表前10條記錄
select * from user limit 10

//查詢user表從第10條開始的後10條記錄
select * from user limit 10 offset 10

  • group by

假如表裡很多條記錄,記錄著每個人每個月的收入,這裡就會存在多條記錄是同一個人的
然後我要查出來每個人總共收入是多少,這時候可以用到group by了,將同一個人的多條記錄合併成一條記錄,會讓其中的收入欄位值累加,從而的得到每個人的總收入,表裡現有記錄如下

uid name income
1 jack 1000
2 jack 1000
3 tom 500
4 aliex 500
5 tom 1000
6 jack 2000
select name,income from user group by name 

執行這個語句後,查出來的記錄如下

name income
jack 4000
tom 1500
aliex 500
  • order by
select name,income from user group by name order by income asc

這個語句執行的結果就是在上面的基礎上將記錄按照income欄位升序排列,得到如下結果

name income
aliex 500
tom 1500
jack 4000

還可以用 order by income desc,這個就是降序排列

  • having
    having 子句在由 group by 子句建立的分組上設定條件,在一個查詢中,having 子句必須放在 group by 子句之後,必須放在 order by 子句之前

比如現在有這樣一個例子:
有三個人,他們中了很多次獎,中獎記錄存放在user表中,現在老闆想看下每個人總共中了多少獎,也就是拿了多少錢,同時對中獎人的條件是中獎次數要大於1次,也就是說只中獎一次的人就不要給我看了

先看看原始資料

uid name income
1 jack 1000
2 jack 1000
3 tom 500
4 aliex 500
5 tom 1000
6 jack 2000

執行下面這個語句

select name,income from user group by name having count(name) > 1

這裡的count是計數功能,count(name) > 1表示每個分組中的分組標誌name要出現一次以上,得到如下結果

name income
jack 4000
tom 1500

因為name=aliex的分組中name只出現了一次,所以被排除了

事務(Transaction)

事務是指作為單個邏輯工作單元執行的一系列操作,要麼完全地執行,要麼完全地不執行;使用事務的好處是可以保證資料的一致性和完整性(避免異常和錯誤等導致的資料資訊異常) ;比如我要批量的插入一批資料,要麼全插入成功,要麼一條也別插入

事務(Transaction)具有以下四個標準屬性,通常根據首字母縮寫為 ACID

  • 原子性(Atomicity):確保工作單位內的所有操作都成功完成,否則,事務會在出現故障時終止,之前的操作也會回滾到以前的狀態
  • 一致性(Consistency):資料庫從一個一致性狀態變到另一個一致性狀態(一系列操作後,所有的操作和更新全部提交成功,資料庫只包含全部成功後的資料就是資料的一致性;相反,由於系統異常或資料庫出現故障導致只有部分資料更新成功,這不是我們需要的最終資料,這就是資料的不一致)
  • 隔離性(Isolation):使事務操作相互獨立和透明(事務內部操作的資料對其它事務是隔離的,在一個事務執行完之前不會被其他事務影響和操作)
  • 永續性(Durability):確保已提交事務的結果或效果在系統發生故障的情況下仍然存在

事務的使用:

主要四步:beginTransaction -> sql操作 -> setTransactionSuccessful -> endTransaction

//往資料庫批量插入記錄
public  boolean batchInsertBySql(List<String> list) {
        SQLiteDatabase db = null;
        try {
            db = sqlConnection.getWritableDatabase();
            
            db.beginTransaction();//開始事務
            for (String str : list) {
                StringBuffer sql = new StringBuffer(200);
                sql.append("insert into terminal_deviceinfo  values(")
                .append(str)
                .append(");");
                db.execSQL(sql.toString());
            }
            db.setTransactionSuccessful();//設定事務的標誌為成功,這樣執行到endTransaction() 時提交當前事務,如果不呼叫此方法會回滾事務
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            try {
                if (null != db) {
                    db.endTransaction();//由事務的標誌決定是提交事務,還是回滾事務
                    db.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return true;
    }

SQL注入

SQL注入是比較常見的網路攻擊方式,注入通常在請求使用者輸入時發生,比如需要使用者輸入姓名,但使用者卻輸入了一個 SQL 語句,這個語句能做的事太多了,非常嚴重的是刪除資料庫了

案列

  • 案例一

比如使用者登入,需要輸入使用者名稱,然後使用者輸入了

tom'; delete from user;

此時一個疏忽的程式猿去判斷表裡有沒有這個人,通過如下語句

select * from user where name='{$name}'

那這個語句最後就是

select * from user where name='tom'; delete from user

像這種堆疊查詢語句很明顯會對資料庫造成非常大的損失

  • 案例2

在登入介面,需要使用者輸入使用者名稱和密碼,然後使用者輸入的使用者名稱如下

‘or 1 = 1 --

這時候又一個疏忽的程式猿這樣去資料庫判斷使用者存不存在

String sql = "select * from user where name=' "+userName+" ' and password=' "+password+" ' "

這個語句接上上面使用者的輸入內容

select * from user where name= '‘ or 1 = 1 -- and password = ' '

where後面接了兩個條件,name= '‘ 或者 1 = 1,那這必然成立的,且後面接了 – 表示後面判斷password 的語句就變成了註釋,就沒有作用了;那這樣使用者就隨便登入了

防止SQL注入

編寫SQL語句的時候切記不要直接使用字串拼接的方式,使用引數化SQL,可以使用 ? 這個佔位符填充引數值的位置;Google在Sqlite里弄了很多過載的函式,像上面提到的幾種查詢方法,就是引數化SQL的效果,引數化處理就是為了防止SQL注入

對使用者輸入的內容進行字串過濾,判斷輸入內容是否包含一些特殊符號和SQL關鍵字

總的說來,防範一般的SQL注入只要在程式碼規範上下點功夫就可以了