Skip to content

Commit ab42358

Browse files
committed
room description updated
1 parent dfe749c commit ab42358

File tree

2 files changed

+156
-53
lines changed

2 files changed

+156
-53
lines changed

_drafts/2019-07-29-coroutines.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: "Coroutines"
44
date: 2019-07-29
55
categories: ["Kotlin"]
66
image: kotlin/coroutines
7-
github: kotlin/blob/master/coroutines
7+
github: kotlin/blob/master/coroutines.kt
88
description: "Kotlin"
99
version: Kotlin v1.3
1010
keywords: "kotlin, coroutines, coroutine, scope, context, dispatcher, suspend, suspendig, async, withcontext, coroutinescope, runBlocking, launch, channel, actor, produce, android, programowanie, programming"

_drafts/2019-08-19-sqlite.md

Lines changed: 155 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ categories: ["Przechowywanie"]
66
image: store/sqlite
77
github: store/tree/master/sqlite
88
description: "Przechowywanie danych"
9-
keywords: "store, data, database, sql, sqlite, baza, dane, room, query, put, insert, select, update, delete, transaction, android, programowanie, programming"
9+
keywords: "store, data, database, sql, sqlite, baza, dane, room, query, put, insert, select, update, delete, transaction, dao, entity, migration, android, programowanie, programming"
1010
---
1111

1212
## 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.
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.
1414

1515
## Implementacja
1616
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() {
3333
database.close()
3434
super.onDestroy()
3535
}
36-
36+
3737
//some database manage methods, there could be inside some manager with conversion some object type into raw data
3838
//notice how many lines of code are needed to do simple single query operation
3939

@@ -142,21 +142,46 @@ object DatabaseContract {
142142
`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`.
143143

144144
## 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).
146146

147147
{% 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
149+
@Entity(
150+
tableName = "person",
151+
foreignKeys = [ForeignKey(entity = Address::class, parentColumns = ["id"], childColumns = ["addressId"])])
152+
data class Person
151153
(
152-
@ColumnInfo(name = "name")
153-
var firstName: String,
154-
var age: Int //by default column name is field name
155-
)
156-
{
157154
@PrimaryKey(autoGenerate = true)
158-
var id: Int = 0
155+
val id : Long,
156+
157+
@ColumnInfo(name = "name") //set column nam for this field
158+
var fullName : String,
159+
160+
//by default column name is field name so no need to use @ColumnInfo
161+
var age : Int,
162+
163+
//some annotations can be declared as @Entity parameters e.g. foreignKeys instead of @ForeignKey
164+
var addressId : Long,
165+
166+
@Embedded //fields of this class are columns of the table
167+
var Contact : Contact
168+
)
169+
{
170+
@Ignore //tell Room to ignore this constructor
171+
constructor(fullName : String, age : Int, addressId : Long, contact : Contact) : this(0, fullName, age, addressId, contact)
172+
173+
//some class body
159174
}
175+
176+
//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)
160185
{% endhighlight %}
161186

162187
## Dao
@@ -169,98 +194,176 @@ interface PersonDao {
169194

170195
//provide query command for @Query annotation
171196
@Query("SELECT * FROM person")
172-
fun getAll(): List<Person>
197+
fun getAll() : List<Person>
173198

174199
@Query("SELECT * FROM person WHERE name LIKE :name LIMIT 1")
175-
fun findByName(name: String): Person
200+
fun findByName(name : String) : Person
176201

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>
179204

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
182207
@Insert(onConflict = OnConflictStrategy.REPLACE)
183-
fun insert(person : Person)
208+
fun insert(person : Person) : Long
184209

185210
@Insert
186-
fun insertAll(vararg persons: Person)
187-
211+
fun insertAll(vararg persons : Person)
212+
188213
//do not have to provide any SQL with @Update or @Delete but it can be also replace by normal @Query
189214
@Update
190215
fun update(person : Person)
191216

192217
@Delete
193-
fun delete(person: Person)
194-
218+
fun delete(person : Person)
219+
195220
//make transaction with atomic operations
196221
@Transaction
197-
fun increaseAgeForAll() {
222+
suspend fun increaseAgeForAll() {
198223
for (person in getAll()) {
199224
person.age = person.age + 1
200225
update(person)
201226
}
202227
}
203228
}
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+
}
204242
{% endhighlight %}
205243

206244
## Database
207245
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`.
208246

209247
{% highlight kotlin %}
210248
//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
214251

215252
//abstract no arg methods with returns of entities
216253
abstract fun personDao() : PersonDao
254+
abstract fun addressDao() : AddressDao
255+
217256
//define more methods for other entities class
218257
}
219258
{% endhighlight %}
220259

221260
## 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.
223262

224263
{% highlight kotlin %}
225264
class RoomActivity : AppCompatActivity(), CoroutineScope by MainScope() {
226265

227-
//create or inject instance, it could be Singleton
228266
lateinit var database : AppDatabase
267+
lateinit var personDao : PersonDao
268+
lateinit var addressDao : AddressDao
229269

230270
override fun onCreate(savedInstanceState: Bundle?) {
231271
super.onCreate(savedInstanceState)
232272

273+
//create or inject instance, it could be Singleton
274+
//can be created also by inMemoryDatabaseBuilder() which helps testing
233275
database = Room
234-
.databaseBuilder(this, AppDatabase::class.java, "RoomDatabase")
276+
.databaseBuilder(this, AppDatabase::class.java, "AppDatabase")
235277
.build()
278+
//there are more builders options like allowMainThreadQueries()
236279

280+
//access to database by dao
281+
personDao = database.personDao()
282+
addressDao = database.addressDao()
283+
284+
//must be run on the background thread otherwise exception will be thrown
237285
doSomeOperations()
238286
}
239287

240288
fun doSomeOperations() {
241-
//must be run on the background thread otherwise exception will be thrown
242289
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()
290+
createData()
291+
val person = selectData()
292+
updateData(person)
293+
deleteData(person)
294+
runTransaction()
263295
}
264296
}
297+
298+
fun createData() {
299+
//put some data using dao example methods
300+
val address = Address("Poznan", "sw. Marcin")
301+
val contact = Contact("111 222 333", "jack@daniels.com")
302+
val addressId = addressDao.insert(address)
303+
personDao.insert(Person("Jack Daniels", 50, addressId, contact))
304+
305+
val polwiejskaId = addressDao.insert(Address("Poznan", "Polwiejska"))
306+
val johnnie = Person("Johnnie Walker", 60, polwiejskaId, Contact("222 333 444", "johnnie@walker.com"))
307+
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?) {
361+
super.onCreate(savedInstanceState)
362+
363+
database = Room
364+
.databaseBuilder(this, AppDatabase::class.java, "RoomDatabase")
365+
.addMigrations(MIGRATION_1_2) //pass more of them if needed
366+
.build()
367+
}
265368
}
266369
{% endhighlight %}

0 commit comments

Comments
 (0)