Database local
Deseori avem nevoie de a stoca mai multe date pentru un utilizator. In acest sens, in Android avem la dispozitie o serie de functionalitati pentru a realiza acest lucru. Astazi vom folosi SQLite, care vine inclusa in sistemul de operare.
Unul dintre principalele principii ale bazelor de date SQL este schema: o declarație formală a modului în care este organizată baza de date. Schema este reflectată în instrucțiunile SQL pe care le folosești pentru a crea baza de date. Poate fi util să creezi o clasă însoțitoare, cunoscută sub numele de clasă contract, care specifică explicit structura schemei tale într-un mod sistematic și auto-documentat.
O clasă contract este un container pentru constante care definesc nume pentru URI-uri, tables și columns. Clasa contract îți permite să folosești aceleași constante în toate celelalte clase din același package. Acest lucru îți permite să modifici numele unei coloane într-un singur loc și să se propage în tot codul tău.
Un mod bun de a organiza o clasă contract este să pui definițiile care sunt globale pentru întreaga bază de date la nivelul root al clasei. Apoi să creezi o inner class pentru fiecare table. Fiecare inner class enumeră columns-urile table-ului corespunzător.
public class Constants {
// Database constants
final public static String DATABASE_NAME = "calculator_operations.db";
final public static int DATABASE_VERSION = 1;
final public static String TABLE_NAME = "operations";
final public static String COLUMN_ID = "id";
final public static String COLUMN_OPERATOR1 = "operator1";
final public static String COLUMN_OPERATOR2 = "operator2";
final public static String COLUMN_OPERATION = "operation";
final public static String COLUMN_METHOD = "method";
final public static String COLUMN_RESULT = "result";
final public static String COLUMN_TIMESTAMP = "timestamp";
}
object Constants {
// Database constants
const val DATABASE_NAME = "calculator_operations.db"
const val DATABASE_VERSION = 1
const val TABLE_NAME = "operations"
const val COLUMN_ID = "id"
const val COLUMN_OPERATOR1 = "operator1"
const val COLUMN_OPERATOR2 = "operator2"
const val COLUMN_OPERATION = "operation"
const val COLUMN_METHOD = "method"
const val COLUMN_RESULT = "result"
const val COLUMN_TIMESTAMP = "timestamp"
}
Crearea unui database
După ce ai definit cum arată baza ta de date, ar trebui să implementezi metode care creează și întrețin baza de date și tabelele. Iată câteva instrucțiuni tipice care creează și șterg un tabel:
String createTableQuery = "CREATE TABLE " + Constants.TABLE_NAME + " (" +
Constants.COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
Constants.COLUMN_OPERATOR1 + " TEXT NOT NULL, " +
Constants.COLUMN_OPERATOR2 + " TEXT NOT NULL, " +
Constants.COLUMN_OPERATION + " TEXT NOT NULL, " +
Constants.COLUMN_METHOD + " TEXT NOT NULL, " +
Constants.COLUMN_RESULT + " TEXT NOT NULL, " +
Constants.COLUMN_TIMESTAMP + " INTEGER NOT NULL" +
")";
String deleteTableQuery = "DROP TABLE IF EXISTS " + Constants.TABLE_NAME;
val createTableQuery = """
CREATE TABLE ${Constants.TABLE_NAME} (
${Constants.COLUMN_ID} INTEGER PRIMARY KEY AUTOINCREMENT,
${Constants.COLUMN_OPERATOR1} TEXT NOT NULL,
${Constants.COLUMN_OPERATOR2} TEXT NOT NULL,
${Constants.COLUMN_OPERATION} TEXT NOT NULL,
${Constants.COLUMN_METHOD} TEXT NOT NULL,
${Constants.COLUMN_RESULT} TEXT NOT NULL,
${Constants.COLUMN_TIMESTAMP} INTEGER NOT NULL
)
""".trimIndent()
val deleteTableQuery = "DROP TABLE IF EXISTS ${Constants.TABLE_NAME}"
La fel ca fișierele pe care le salvezi în memoria internă a dispozitivului, Android stochează baza ta de date în folderul privat al aplicației tale. Datele tale sunt securizate, deoarece în mod implicit această zonă nu este accesibilă altor aplicații sau utilizatorului.
Clasa SQLiteOpenHelper conține un set util de API-uri pentru gestionarea bazei
tale de date. Când folosești această clasă pentru a obține referințe la baza ta
de date, sistemul efectuează operațiunile potențial de lungă durată de creare
și actualizare a bazei de date doar când este necesar și nu în timpul pornirii
aplicației. Tot ce trebuie să faci este să apelezi getWritableDatabase() sau
getReadableDatabase().
Pentru a utiliza SQLiteOpenHelper, creează o subclasă care suprascrie
metodele callback onCreate() și onUpgrade(). De asemenea, poți implementa
metodele onDowngrade() sau onOpen(), dar acestea nu sunt obligatorii.
public class OperationsDatabaseHelper extends SQLiteOpenHelper {
public OperationsDatabaseHelper(Context context) {
super(context, Constants.DATABASE_NAME, null, Constants.DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String createTableQuery = "CREATE TABLE " + Constants.TABLE_NAME + " (" +
Constants.COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
Constants.COLUMN_OPERATOR1 + " TEXT NOT NULL, " +
Constants.COLUMN_OPERATOR2 + " TEXT NOT NULL, " +
Constants.COLUMN_OPERATION + " TEXT NOT NULL, " +
Constants.COLUMN_METHOD + " TEXT NOT NULL, " +
Constants.COLUMN_RESULT + " TEXT NOT NULL, " +
Constants.COLUMN_TIMESTAMP + " INTEGER NOT NULL" +
")";
db.execSQL(createTableQuery);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + Constants.TABLE_NAME);
onCreate(db);
}
}
class OperationsDatabaseHelper(context: Context) : SQLiteOpenHelper(
context,
Constants.DATABASE_NAME,
null,
Constants.DATABASE_VERSION
) {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL("""
CREATE TABLE ${Constants.TABLE_NAME} (
${Constants.COLUMN_ID} INTEGER PRIMARY KEY AUTOINCREMENT,
${Constants.COLUMN_OPERATOR1} TEXT NOT NULL,
${Constants.COLUMN_OPERATOR2} TEXT NOT NULL,
${Constants.COLUMN_OPERATION} TEXT NOT NULL,
${Constants.COLUMN_METHOD} TEXT NOT NULL,
${Constants.COLUMN_RESULT} TEXT NOT NULL,
${Constants.COLUMN_TIMESTAMP} INTEGER NOT NULL
)
""".trimIndent())
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS ${Constants.TABLE_NAME}")
onCreate(db)
}
}
Pentru a accesa baza ta de date, instanțiază subclasa ta de SQLiteOpenHelper:
OperationsDatabaseHelper dbHelper = new OperationsDatabaseHelper(context);
val dbHelper = OperationsDatabaseHelper(context)
Clasa pentru înregistrări
Pentru a reprezenta o înregistrare din baza de date, putem crea o clasă simplă care stochează datele:
public class OperationRecord {
private long id;
private String operator1;
private String operator2;
private String operation;
private String method;
private String result;
private long timestamp;
public OperationRecord(long id, String operator1, String operator2,
String operation, String method, String result, long timestamp) {
this.id = id;
this.operator1 = operator1;
this.operator2 = operator2;
this.operation = operation;
this.method = method;
this.result = result;
this.timestamp = timestamp;
}
// Getters pentru toate câmpurile
public long getId() { return id; }
public String getOperator1() { return operator1; }
public String getOperator2() { return operator2; }
public String getOperation() { return operation; }
public String getMethod() { return method; }
public String getResult() { return result; }
public long getTimestamp() { return timestamp; }
}
data class OperationRecord(
val id: Long,
val operator1: String,
val operator2: String,
val operation: String,
val method: String,
val result: String,
val timestamp: Long
)
Introducerea de date
Inserează date în baza de date prin transmiterea unui obiect ContentValues către metoda insert():
public long insertOperation(String operator1, String operator2, String operation, String method, String result) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(Constants.COLUMN_OPERATOR1, operator1);
values.put(Constants.COLUMN_OPERATOR2, operator2);
values.put(Constants.COLUMN_OPERATION, operation);
values.put(Constants.COLUMN_METHOD, method);
values.put(Constants.COLUMN_RESULT, result);
values.put(Constants.COLUMN_TIMESTAMP, System.currentTimeMillis());
return db.insert(Constants.TABLE_NAME, null, values);
}
fun insertOperation(
operator1: String,
operator2: String,
operation: String,
method: String,
result: String
): Long {
val db = writableDatabase
val values = android.content.ContentValues().apply {
put(Constants.COLUMN_OPERATOR1, operator1)
put(Constants.COLUMN_OPERATOR2, operator2)
put(Constants.COLUMN_OPERATION, operation)
put(Constants.COLUMN_METHOD, method)
put(Constants.COLUMN_RESULT, result)
put(Constants.COLUMN_TIMESTAMP, System.currentTimeMillis())
}
return db.insert(Constants.TABLE_NAME, null, values)
}
Citirea de date
Pentru a citi dintr-o bază de date, folosește metoda query(), transmițându-i
criteriile tale de selecție și coloanele dorite. Metoda combină elemente din
insert() și update(), cu excepția faptului că lista de coloane definește
datele pe care vrei să le preiei (denumită "projection"), și nu datele de
inserat. Rezultatele interogării îți sunt returnate într-un obiect Cursor.
public List<OperationRecord> getAllOperations() {
List<OperationRecord> operations = new ArrayList<>();
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(
Constants.TABLE_NAME,
null, // The array of columns to return (pass null to get all)
null, // The columns for the WHERE clause
null, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
Constants.COLUMN_TIMESTAMP + " DESC" // The sort order
);
if (cursor != null) {
int idIndex = cursor.getColumnIndex(Constants.COLUMN_ID);
int operator1Index = cursor.getColumnIndex(Constants.COLUMN_OPERATOR1);
int operator2Index = cursor.getColumnIndex(Constants.COLUMN_OPERATOR2);
int operationIndex = cursor.getColumnIndex(Constants.COLUMN_OPERATION);
int methodIndex = cursor.getColumnIndex(Constants.COLUMN_METHOD);
int resultIndex = cursor.getColumnIndex(Constants.COLUMN_RESULT);
int timestampIndex = cursor.getColumnIndex(Constants.COLUMN_TIMESTAMP);
while (cursor.moveToNext()) {
operations.add(new OperationRecord(
cursor.getLong(idIndex),
cursor.getString(operator1Index),
cursor.getString(operator2Index),
cursor.getString(operationIndex),
cursor.getString(methodIndex),
cursor.getString(resultIndex),
cursor.getLong(timestampIndex)
));
}
cursor.close();
}
return operations;
}
fun getAllOperations(): List<OperationRecord> {
val operations = mutableListOf<OperationRecord>()
val db = readableDatabase
val cursor = db.query(
Constants.TABLE_NAME,
null, // The array of columns to return (pass null to get all)
null, // The columns for the WHERE clause
null, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
"${Constants.COLUMN_TIMESTAMP} DESC" // The sort order
)
cursor.use {
val idIndex = it.getColumnIndex(Constants.COLUMN_ID)
val operator1Index = it.getColumnIndex(Constants.COLUMN_OPERATOR1)
val operator2Index = it.getColumnIndex(Constants.COLUMN_OPERATOR2)
val operationIndex = it.getColumnIndex(Constants.COLUMN_OPERATION)
val methodIndex = it.getColumnIndex(Constants.COLUMN_METHOD)
val resultIndex = it.getColumnIndex(Constants.COLUMN_RESULT)
val timestampIndex = it.getColumnIndex(Constants.COLUMN_TIMESTAMP)
while (it.moveToNext()) {
operations.add(
OperationRecord(
id = it.getLong(idIndex),
operator1 = it.getString(operator1Index),
operator2 = it.getString(operator2Index),
operation = it.getString(operationIndex),
method = it.getString(methodIndex),
result = it.getString(resultIndex),
timestamp = it.getLong(timestampIndex)
)
)
}
}
return operations
}
Utilizarea bazei de date în aplicație
Pentru a salva operațiile calculatorului în baza de date, putem apela metoda insertOperation() după ce am primit rezultatul de la serviciul web:
// În CalculatorWebServiceAsyncTask, după ce am primit rezultatul
OperationsDatabaseHelper dbHelper = new OperationsDatabaseHelper(context);
dbHelper.insertOperation(operator1, operator2, operation, methodName, result);
// În CalculatorWebServiceAsyncTask, după ce am primit rezultatul
val dbHelper = OperationsDatabaseHelper(context)
dbHelper.insertOperation(operator1, operator2, operation, methodName, result)
Pentru a afișa istoricul operațiilor în Logcat, putem folosi metoda getAllOperations():
OperationsDatabaseHelper dbHelper = new OperationsDatabaseHelper(this);
List<OperationRecord> operations = dbHelper.getAllOperations();
Log.d(Constants.TAG, "========== OPERATIONS HISTORY ==========");
if (operations.isEmpty()) {
Log.d(Constants.TAG, "No operations found in database.");
} else {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
for (int i = 0; i < operations.size(); i++) {
OperationRecord operation = operations.get(i);
String timestamp = dateFormat.format(new Date(operation.getTimestamp()));
Log.d(Constants.TAG, "Operation #" + (i + 1) + ":");
Log.d(Constants.TAG, " Operator 1: " + operation.getOperator1());
Log.d(Constants.TAG, " Operator 2: " + operation.getOperator2());
Log.d(Constants.TAG, " Operation: " + operation.getOperation());
Log.d(Constants.TAG, " Method: " + operation.getMethod());
Log.d(Constants.TAG, " Result: " + operation.getResult());
Log.d(Constants.TAG, " Timestamp: " + timestamp);
Log.d(Constants.TAG, " ---");
}
}
Log.d(Constants.TAG, "========================================");
val dbHelper = OperationsDatabaseHelper(this)
val operations = dbHelper.getAllOperations()
Log.d(Constants.TAG, "========== OPERATIONS HISTORY ==========")
if (operations.isEmpty()) {
Log.d(Constants.TAG, "No operations found in database.")
} else {
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
operations.forEachIndexed { index, operation ->
val timestamp = dateFormat.format(Date(operation.timestamp))
Log.d(Constants.TAG, "Operation #${index + 1}:")
Log.d(Constants.TAG, " Operator 1: ${operation.operator1}")
Log.d(Constants.TAG, " Operator 2: ${operation.operator2}")
Log.d(Constants.TAG, " Operation: ${operation.operation}")
Log.d(Constants.TAG, " Method: ${operation.method}")
Log.d(Constants.TAG, " Result: ${operation.result}")
Log.d(Constants.TAG, " Timestamp: $timestamp")
Log.d(Constants.TAG, " ---")
}
}
Log.d(Constants.TAG, "========================================")