|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "SQLite" |
| 4 | +date: 2019-08-19 |
| 5 | +categories: ["Przechowywanie"] |
| 6 | +image: store/sqlite |
| 7 | +github: store/tree/master/sqlite |
| 8 | +description: "Przechowywanie danych" |
| 9 | +keywords: "store, data, database, sql, sqlite, baza, dane, room, query, put, insert, select, update, delete, transaction, android, programowanie, programming" |
| 10 | +--- |
| 11 | + |
| 12 | +## Bazy danych |
| 13 | +Przechowywanie informacji w bazie danych jest jednym z najpopularniejszych sposobów zarządzania zbiorem danych o ustalonej strukturze. Doskonale spełnia swoją rolę w sytuacji gromadzenia przede wszystkim wielu obiektów zdefiniowanych typów (np. kontakty). Podobnie jak w przypadku plików w `storage` czy wartości prymitywnych w `SharedPreferences` dane pozostają dostępne do odczytu i zapisu niezależnie od cyklu życia aplikacji. Są zachowane dopóki nie zostaną usunięte programowo lub ręcznie przez użytkownika poprzez wyczyszczenie danych aplikacji. `Android` wykorzystuje bazy danych w schemacie `SQL` w implementacji `SQLite`. Pomimo możliwości bezpośredniego operowania na bazie danych za pomocą zapytań `SQLite` przez klienta `SQLiteOpenHelper` wysoce zalecane jest użycie biblioteki `Room` dostarczającej dodatkowej warstwy abstrakcji. |
| 14 | + |
| 15 | +## Implementacja |
| 16 | +Jedną z głównych zasad tworzenia baz danych `SQL` jest formalna deklaracja struktury. Definiuje ona sposób w jaki baza danych jest organizowana oraz jakie typy obiektów mogą być przechowywane i modyfikowane. Dane znajdują się w tabelach zbudowanych z kolumn i wierszy. Reprezentacją jednego wpisu (porcji informacji) jest encja. Aby zaimplementować bazę danych należy rozszerzyć klasę `SQLiteOpenHelper` oraz nadpisać metody `onCreate` i `onUpgrade` definiując jej strukturę. Modyfikacja zawartości bazy odbywa się przy pomocy zapytań `SQL` w metodach `put`, `query`, `delete`, `update`. |
| 17 | + |
| 18 | +{% highlight kotlin %} |
| 19 | +//use SQLiteDatabase in some client |
| 20 | +class SQLiteActivity : AppCompatActivity() { |
| 21 | + |
| 22 | + private lateinit var database : SQLiteDatabase |
| 23 | + |
| 24 | + override fun onCreate(savedInstanceState: Bundle?) { |
| 25 | + super.onCreate(savedInstanceState) |
| 26 | + //get instance of writable or readable database to reuse it in multiple queries |
| 27 | + database = Database(this).writableDatabase |
| 28 | + //use database by defined methods below |
| 29 | + } |
| 30 | + |
| 31 | + override fun onDestroy() { |
| 32 | + //close costly database connection |
| 33 | + database.close() |
| 34 | + super.onDestroy() |
| 35 | + } |
| 36 | + |
| 37 | + //some database manage methods, there could be inside some manager with conversion some object type into raw data |
| 38 | + //notice how many lines of code are needed to do simple single query operation |
| 39 | + |
| 40 | + fun put() { |
| 41 | + //prepare data |
| 42 | + val values = ContentValues() |
| 43 | + values.put(DatabaseContract.Person.COLUMN_NAME, "Jack") |
| 44 | + values.put(DatabaseContract.Person.COLUMN_AGE, 50) |
| 45 | + |
| 46 | + //put the data and get id of new entry, returns -1 if fails |
| 47 | + val id = database.insert(DatabaseContract.Entry.TABLE_PERSON, null, values) |
| 48 | + } |
| 49 | + |
| 50 | + fun read() { |
| 51 | + //define the query params |
| 52 | + val table = DatabaseContract.Person.TABLE_PERSON |
| 53 | + val columns : Array<String>? = null //get all columns |
| 54 | + val selection = DatabaseContract.Person.COLUMN_NAME + " = ?" //columns for WHERE |
| 55 | + val args = arrayOf("Jack") //values for WHERE |
| 56 | + val groupBy = null //ignore |
| 57 | + val filterBy = null //ignore |
| 58 | + val sortOrder = DatabaseContract.Person.COLUMN_AGE + " DESC" |
| 59 | + |
| 60 | + //make query and read data from the cursor by iterator methods |
| 61 | + val cursor : Cursor = database.query(table, columns, selection, args, groupBy, filterBy, sortOrder) |
| 62 | + while (cursor.moveToNext()) { |
| 63 | + val age = cursor.getString(cursor.getColumnIndex(DatabaseContract.Person.COLUMN_AGE)) |
| 64 | + //do something with item |
| 65 | + } |
| 66 | + cursor.close() |
| 67 | + } |
| 68 | + |
| 69 | + fun delete() { |
| 70 | + //define query params |
| 71 | + val table = DatabaseContract.Person.TABLE_PERSON |
| 72 | + val selection = DatabaseContract.Person.COLUMN_NAME + " LIKE ?" |
| 73 | + val args = arrayOf("Jack") |
| 74 | + |
| 75 | + //delete entries and get number of the removed items |
| 76 | + val count = database.delete(table, selection, args) |
| 77 | + } |
| 78 | + |
| 79 | + fun update() { |
| 80 | + //prepare data |
| 81 | + val values = ContentValues() |
| 82 | + values.put(DatabaseContract.Person.COLUMN_AGE, 51) |
| 83 | + |
| 84 | + //define query params |
| 85 | + val table = DatabaseContract.Person.TABLE_PERSON |
| 86 | + val selection = DatabaseContract.Person.COLUMN_NAME + " LIKE ?" |
| 87 | + val args = arrayOf("Jack") |
| 88 | + |
| 89 | + //update entries and updated count |
| 90 | + val count = database.update(table, values, selection, args) |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +//extend SQLiteOpenHelper |
| 95 | +class Database(context : Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { |
| 96 | + |
| 97 | + companion object { |
| 98 | + const val DATABASE_VERSION = 1 |
| 99 | + const val DATABASE_NAME = "Person.db" |
| 100 | + } |
| 101 | + |
| 102 | + //define SQL queries, use DatabaseContract |
| 103 | + private val SQL_CREATE = |
| 104 | + "CREATE TABLE ${DatabaseContract.Person.TABLE_PERSON} (" + |
| 105 | + "${BaseColumns._ID} INTEGER PRIMARY KEY," + |
| 106 | + "${DatabaseContract.Person.COLUMN_NAME} TEXT," + |
| 107 | + "${DatabaseContract.Person.COLUMN_AGE} INTEGER)" |
| 108 | + |
| 109 | + private val SQL_DELETE = |
| 110 | + "DROP TABLE IF EXISTS ${DatabaseContract.Person.TABLE_PERSON}" |
| 111 | + |
| 112 | + override fun onCreate(db: SQLiteDatabase?) { |
| 113 | + //just create database with specific structure |
| 114 | + db?.execSQL(SQL_CREATE) |
| 115 | + } |
| 116 | + |
| 117 | + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { |
| 118 | + //do something when database structure has updated, e.g. clear all old data |
| 119 | + db?.execSQL(SQL_DELETE) |
| 120 | + onCreate(db) |
| 121 | + } |
| 122 | + |
| 123 | + //implement other methods like onDowngrade, onConfigure |
| 124 | +} |
| 125 | + |
| 126 | +//good practice is to define database scheme contract to simpler manage it |
| 127 | +object DatabaseContract { |
| 128 | + |
| 129 | + //create inner class for each Table, BaseColumns has primary key field called _ID |
| 130 | + object Person : BaseColumns { |
| 131 | + const val TABLE_PERSON = "person" |
| 132 | + const val COLUMN_NAME = "name" |
| 133 | + const val COLUMN_AGE = "age" |
| 134 | + } |
| 135 | +} |
| 136 | +{% endhighlight %} |
| 137 | + |
| 138 | +## Ograniczenia |
| 139 | +`SQLiteOpenHelper` umożliwia zarządzanie zarówno strukturą jak i zawartością bazy danych. Jest narzędziem niższego poziomu, które wymaga od programisty implementacji kodu związanego ze strukturą i zapytaniami do bazy co jest podatne na błędy z uwagi na brak weryfikacji poprawności poleceń `SQL` i zgodności ze schematem bazy. Wymaga generowania nadmiarowego kodu konwersji zapytań `SQL` do obiektów i na odwrót (praktycznie jedna metoda dla każdej atomowej operacji dla danego typu). Rozwiązaniem tych problemów może być wykorzystanie biblioteki `Room`. |
| 140 | + |
| 141 | +## Room |
| 142 | +`Room` dostarcza warstwy abstrakcji dla `SQLite` dzięki czemu dostęp i zarządzanie bazą danych staje się łatwiejsze, a pisanie kodu szybsze. Weryfikuje poprawność zapytań `SQL` już w trakcie kompilacji co znacznie zmniejsza możliwość popełnienia błędu. Ponadto wspiera mechanizm transakcji i dostarcza wiele adnotacji redukując tym samym potrzebę pisania wielu zapytań. Aby umożliwić współpracę z `Room` należy dostarczyć trzy komponenty oznaczone jako `@Database`, `@Entity`, `@Dao`. |
| 143 | + |
| 144 | +## Entity |
| 145 | +Klasa oznaczona jako `@Entity` reprezentuje tabele, gdzie każde jej pole jest kolumną. Pełni ona rolę modelu w bazie danych i nie zawiera żadnej logiki. `@PrimaryKey` ustawia klucz podstawowy na wskazanym polu, a `@ForeignKey` wskazuje klucz obcy. `@ColumnInfo` definiuje nazwę kolumny, `@Embedded` umożliwia dostęp do pól wewnętrznych klasy, natomiast `@Ignore` pozwala zignorowanie konstruktorów i pól przy tworzeniu obiektu przez `Room` (jeśli jest kilka konstruktorów). |
| 146 | + |
| 147 | +{% highlight kotlin %} |
| 148 | +//represents some table of database, define scheme here |
| 149 | +@Entity(tableName = "person") //by default tableName is class name |
| 150 | +data class Person |
| 151 | +( |
| 152 | + @ColumnInfo(name = "name") |
| 153 | + var firstName: String, |
| 154 | + var age: Int //by default column name is field name |
| 155 | +) |
| 156 | +{ |
| 157 | + @PrimaryKey(autoGenerate = true) |
| 158 | + var id: Int = 0 |
| 159 | +} |
| 160 | +{% endhighlight %} |
| 161 | + |
| 162 | +## Dao |
| 163 | +Klasa oznaczona jako `@Dao` odpowiedzialna jest za dostarczenie deklaracji metod dostępowych do baz danych. Są one tworzone przy użyciu różnych adnotacji takich jak m.in. `@Insert`, `@Update`, `@Delete` które automatycznie generują kod zapytań czy też przez adnotację `@Query` wymagającej definicji zapytania. W przypadku ciągu operacji, które muszą zostać wykonane w ramach jednej transakcji należy użyć adnotacji `@Transaction`. |
| 164 | + |
| 165 | +{% highlight kotlin %} |
| 166 | +//define methods to use database |
| 167 | +@Dao |
| 168 | +interface PersonDao { |
| 169 | + |
| 170 | + //provide query command for @Query annotation |
| 171 | + @Query("SELECT * FROM person") |
| 172 | + fun getAll(): List<Person> |
| 173 | + |
| 174 | + @Query("SELECT * FROM person WHERE name LIKE :name LIMIT 1") |
| 175 | + fun findByName(name: String): Person |
| 176 | + |
| 177 | + @Query("SELECT * FROM person WHERE age LIKE :age") |
| 178 | + fun findByAge(age: Int): Person |
| 179 | + |
| 180 | + //do not have to provide any SQL with @Insert |
| 181 | + //when conflict just replace so it works also as update |
| 182 | + @Insert(onConflict = OnConflictStrategy.REPLACE) |
| 183 | + fun insert(person : Person) |
| 184 | + |
| 185 | + @Insert |
| 186 | + fun insertAll(vararg persons: Person) |
| 187 | + |
| 188 | + //do not have to provide any SQL with @Update or @Delete but it can be also replace by normal @Query |
| 189 | + @Update |
| 190 | + fun update(person : Person) |
| 191 | + |
| 192 | + @Delete |
| 193 | + fun delete(person: Person) |
| 194 | + |
| 195 | + //make transaction with atomic operations |
| 196 | + @Transaction |
| 197 | + fun increaseAgeForAll() { |
| 198 | + for (person in getAll()) { |
| 199 | + person.age = person.age + 1 |
| 200 | + update(person) |
| 201 | + } |
| 202 | + } |
| 203 | +} |
| 204 | +{% endhighlight %} |
| 205 | + |
| 206 | +## Database |
| 207 | +Klasa oznaczona jako `@Database` łączy wybrane tabele klas `@Entity` i metody dostępowe klas `@Dao` w jedną całość. Taka klasa musi być abstrakcyjna i rozszerzać `RoomDatabase` oraz deklarować metody abstrakcyjne zwracające obiekty `@Dao`. |
| 208 | + |
| 209 | +{% highlight kotlin %} |
| 210 | +//serves as the access point, define list of entities associated with database |
| 211 | +@Database(entities = arrayOf(Person::class), version = 1) |
| 212 | +//must be abstract and extends RoomDatabase and marked as Database |
| 213 | +abstract class AppDatabase : RoomDatabase() { |
| 214 | + |
| 215 | + //abstract no arg methods with returns of entities |
| 216 | + abstract fun personDao() : PersonDao |
| 217 | + //define more methods for other entities class |
| 218 | +} |
| 219 | +{% endhighlight %} |
| 220 | + |
| 221 | +## Użycie |
| 222 | +Tworzenie instancji bazy danych `Room` odbywa się przy użyciu budowniczego. Ze względu na kosztowność bazy warto rozważyć zastosowanie wzorca `Singleton` i tym samym ograniczyć instancję do jednej dla całej aplikacji. |
| 223 | + |
| 224 | +{% highlight kotlin %} |
| 225 | +class RoomActivity : AppCompatActivity(), CoroutineScope by MainScope() { |
| 226 | + |
| 227 | + //create or inject instance, it could be Singleton |
| 228 | + lateinit var database : AppDatabase |
| 229 | + |
| 230 | + override fun onCreate(savedInstanceState: Bundle?) { |
| 231 | + super.onCreate(savedInstanceState) |
| 232 | + |
| 233 | + database = Room |
| 234 | + .databaseBuilder(this, AppDatabase::class.java, "RoomDatabase") |
| 235 | + .build() |
| 236 | + |
| 237 | + doSomeOperations() |
| 238 | + } |
| 239 | + |
| 240 | + fun doSomeOperations() { |
| 241 | + //must be run on the background thread otherwise exception will be thrown |
| 242 | + launch(Dispatchers.IO) { |
| 243 | + //use it by Dao instance |
| 244 | + val personDao = database.personDao() |
| 245 | + |
| 246 | + //CREATE objects |
| 247 | + personDao.insert(Person("Jack", 50)) |
| 248 | + personDao.insertAll(Person("Johhnie", 60), Person("William", 70)) |
| 249 | + |
| 250 | + //SELECT objects |
| 251 | + var persons = personDao.getAll() |
| 252 | + val person = personDao.findByName("Jack") |
| 253 | + |
| 254 | + //UPDATE object |
| 255 | + person.age = 20 |
| 256 | + personDao.update(person) |
| 257 | + |
| 258 | + //DELETE object |
| 259 | + personDao.delete(person) |
| 260 | + |
| 261 | + //run transaction method |
| 262 | + personDao.increaseAgeForAll() |
| 263 | + } |
| 264 | + } |
| 265 | +} |
| 266 | +{% endhighlight %} |
0 commit comments