You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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 w pamięci wewnętrznej urządzenia 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
14
15
15
## Implementacja
16
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`.
@@ -33,7 +33,7 @@ class SQLiteActivity : AppCompatActivity() {
33
33
database.close()
34
34
super.onDestroy()
35
35
}
36
-
36
+
37
37
//some database manage methods, there could be inside some manager with conversion some object type into raw data
38
38
//notice how many lines of code are needed to do simple single query operation
39
39
@@ -142,21 +142,46 @@ object DatabaseContract {
142
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
143
144
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).
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 i łączy. `@ColumnInfo` definiuje nazwę kolumny, `@Embedded` umożliwia dostęp do wewnętrznych pól klasy jako kolumn tabeli, natomiast `@Ignore` pozwala zignorowanie konstruktorów czy pól przy tworzeniu obiektu przez `RoomDatabase` (jeśli jest kilka konstruktorów).
146
146
147
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
148
+
//represents a table of database, define scheme here
//by default tableName is a class name, indices speeds up select but slows down insert or update
177
+
@Entity(tableName = "address", indices = [Index(value = ["street"])])
178
+
data class Address(@PrimaryKey(autoGenerate = true) val id : Long, var city : String, var street : String) {
179
+
180
+
@Ignore
181
+
constructor(city : String, street: String) : this(0, city, street)
182
+
}
183
+
184
+
data class Contact(var phone : String, var email : String)
160
185
{% endhighlight %}
161
186
162
187
## Dao
@@ -169,98 +194,176 @@ interface PersonDao {
169
194
170
195
//provide query command for @Query annotation
171
196
@Query("SELECT * FROM person")
172
-
fun getAll(): List<Person>
197
+
fun getAll(): List<Person>
173
198
174
199
@Query("SELECT * FROM person WHERE name LIKE :name LIMIT 1")
175
-
fun findByName(name: String): Person
200
+
fun findByName(name: String): Person
176
201
177
-
@Query("SELECT * FROM person WHERE age LIKE :age")
178
-
fun findByAge(age: Int): Person
202
+
@Query("SELECT * FROM person INNER JOIN address ON address.id = person.id WHERE address.city LIKE :city")
203
+
fun findByCity(city : String) : List<Person>
179
204
180
-
//do not have to provide any SQL with @Insert
181
-
//when conflict just replace so it works also as update
205
+
//do not have to provide any SQL with @Insert, when conflict just replace so it works also as update
206
+
//return id primary key
182
207
@Insert(onConflict = OnConflictStrategy.REPLACE)
183
-
fun insert(person : Person)
208
+
fun insert(person : Person) : Long
184
209
185
210
@Insert
186
-
fun insertAll(vararg persons: Person)
187
-
211
+
fun insertAll(vararg persons: Person)
212
+
188
213
//do not have to provide any SQL with @Update or @Delete but it can be also replace by normal @Query
189
214
@Update
190
215
fun update(person : Person)
191
216
192
217
@Delete
193
-
fun delete(person: Person)
194
-
218
+
fun delete(person: Person)
219
+
195
220
//make transaction with atomic operations
196
221
@Transaction
197
-
fun increaseAgeForAll() {
222
+
suspend fun increaseAgeForAll() {
198
223
for (person in getAll()) {
199
224
person.age = person.age + 1
200
225
update(person)
201
226
}
202
227
}
203
228
}
229
+
230
+
@Dao
231
+
interface AddressDao {
232
+
233
+
@Query("SELECT * FROM address")
234
+
fun getAll() : List<Address>
235
+
236
+
@Insert(onConflict = OnConflictStrategy.REPLACE)
237
+
fun insert(address: Address) : Long
238
+
239
+
@Delete
240
+
fun delete(address: Address)
241
+
}
204
242
{% endhighlight %}
205
243
206
244
## Database
207
245
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
246
209
247
{% highlight kotlin %}
210
248
//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() {
249
+
@Database(entities = [Person::class, Address::class], version = 1, exportSchema = false)
250
+
abstract class AppDatabase : RoomDatabase() { //must be abstract and extends RoomDatabase
214
251
215
252
//abstract no arg methods with returns of entities
216
253
abstract fun personDao() : PersonDao
254
+
abstract fun addressDao() : AddressDao
255
+
217
256
//define more methods for other entities class
218
257
}
219
258
{% endhighlight %}
220
259
221
260
## 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.
261
+
Tworzenie instancji bazy danych `RoomDatabase` 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
262
224
263
{% highlight kotlin %}
225
264
class RoomActivity : AppCompatActivity(), CoroutineScope by MainScope() {
226
265
227
-
//create or inject instance, it could be Singleton
228
266
lateinit var database : AppDatabase
267
+
lateinit var personDao : PersonDao
268
+
lateinit var addressDao : AddressDao
229
269
230
270
override fun onCreate(savedInstanceState: Bundle?) {
231
271
super.onCreate(savedInstanceState)
232
272
273
+
//create or inject instance, it could be Singleton
274
+
//can be created also by inMemoryDatabaseBuilder() which helps testing
val william = Person("William Grant", 70, polwiejskaId, Contact("333 444 555", "william@grant.com"))
308
+
personDao.insertAll(johnnie, william)
309
+
}
310
+
311
+
fun selectData() : Person {
312
+
//example of getting data usage
313
+
val persons = personDao.getAll()
314
+
val person = personDao.findByName("Jack Daniels")
315
+
return person
316
+
}
317
+
318
+
fun updateData(person : Person) {
319
+
person.age = 20
320
+
personDao.update(person)
321
+
}
322
+
323
+
fun deleteData(person : Person) {
324
+
personDao.delete(person)
325
+
}
326
+
327
+
suspend fun runTransaction() {
328
+
//run transaction by annotated method
329
+
personDao.increaseAgeForAll()
330
+
331
+
//or this can be achieve also by transaction directly on database
332
+
database.runInTransaction {
333
+
for (person in personDao.getAll()) {
334
+
person.age = person.age + 1
335
+
personDao.update(person)
336
+
}
337
+
}
338
+
}
339
+
}
340
+
{% endhighlight %}
341
+
342
+
## Migracja
343
+
W trakcie rozwijania aplikacji nierzadko występuje potrzeba zmiany struktury bazy danych. W takiej sytuacji może okazać się, że jakaś część danych z poprzedniego formatu bazy jest nadal potrzebna. W związku z tym należy zapewnić poprawną migrację danych między wersjami bazy poprzez skonstrukowanie odpowiedniego zapytania `SQL` w obiekcie `Migration` oraz dodanie go przez `addMigrations` w trakcie tworzenia bazy. Z uwagi na to, że proces migracji narażony jest na występowanie poważnych błędów przed wypuszczeniem wersji produkcyjnej aplikacji nie można zapomnieć o lokalnej kopii zapasowej danych i właściwych testach migracji wspieranych przez `MigrationTestHelper`.
344
+
345
+
{% highlight kotlin %}
346
+
class MigrationActivity : AppCompatActivity() {
347
+
348
+
//address entity is extended by new building number column and database upgraded version
349
+
350
+
lateinit var database : AppDatabase
351
+
352
+
//provide proper SQL migration
353
+
val MIGRATION_1_2 = object: Migration(1, 2) {
354
+
override fun migrate(database: SupportSQLiteDatabase) {
355
+
database.execSQL("ALTER TABLE address ADD COLUMN number TEXT")
356
+
//if new column can not be null inject some default values by execSQL
357
+
}
358
+
}
359
+
360
+
override fun onCreate(savedInstanceState: Bundle?) {
0 commit comments