Skip to content

Commit dfe749c

Browse files
committed
sqlite post added
1 parent 7c81f2f commit dfe749c

File tree

5 files changed

+266
-0
lines changed

5 files changed

+266
-0
lines changed
File renamed without changes.

_drafts/2019-08-19-sqlite.md

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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 %}

assets/img/posts/store/sqlite.jpg

721 KB
Loading
21.5 KB
Loading
65.8 KB
Loading

0 commit comments

Comments
 (0)