DDie Entwicklung einer komplexen Anwendung ist ohne den Einsatz von Datenbanken, die leistungsstarke Funktionen zum Speichern, Sortieren und Abrufen von Informationen bieten, unmöglich. Ihre Anwendung in der Android-Entwicklung hat ihre eigenen Besonderheiten, die durch die Eigenschaften mobiler Geräte bedingt sind: weniger Hardwareressourcen, Batterieschonung, mobile Anwendungsarchitektur. Daher ist die Verwendung von Datenbanken in der Android-Entwicklung ein separates Thema, das eine genaue Untersuchung erfordert.
Im Jahr 2020 bleiben SQLite, Realm und ObjectBox die beliebtesten Datenbankmanagementsysteme (DBMS) für Android-Anwendungen. Jedes von ihnen besetzt seine eigene Nische auf dem Markt für mobile Anwendungen und bietet Entwicklern verschiedene Ansätze für die Speicherung und Nutzung strukturierter Daten.
SQLite – der Spitzenreiter in Sachen Popularität
SQLite wurde erstmals im Jahr 2000 vorgestellt und hat sich seitdem zu einem der beliebtesten DBMS entwickelt. Es basiert auf einem relationalen Ansatz zur Datenspeicherung und verwendet die Abfragesprache SQL, um Informationen zu verwalten. Im Gegensatz zu anderen bekannten DBMS wie MySQL und PostgreSQL benötigt SQLite keinen separaten Datenbankserver, speichert alle Informationen in einer Datei und benötigt nur wenig Speicherplatz. Diese Eigenschaften machen es zum optimalsten unter den relationalen DBMS für den Einsatz in mobilen Geräten. Zurzeit ist SQLite das am häufigsten verwendete DBMS bei der Entwicklung von Android-Anwendungen. Reines SQLite wird derzeit nur selten bei der Entwicklung von Android-Anwendungen eingesetzt. Um die Arbeit zu vereinfachen, wird häufig der ORM Room von Google verwendet, der das Schreiben von Boilerplate-Code vermeidet. Datentabellen werden mithilfe von Annotationen in der Modellklasse erstellt, die die Umwandlung von Klassenattributen in Namen und Eigenschaften von Tabellenspalten festlegen.
Realm – objektorientierte Alternative
Eine Alternative zu SQLite ist Realm. Seine erste stabile Version für Android erschien im Jahr 2014. Die Macher haben Realm als eine Datenbank konzipiert, die das Schreiben von umständlichem Boilerplate-Code in SQL vermeidet, die Arbeit mit Daten als Objekte ermöglicht und die Geschwindigkeit von CRUD-Operationen erhöht. Im Jahr 2019 wurde Realm von MongoDB Inc. übernommen und fügte der ursprünglichen Funktionalität die Option der serverlosen Plattform hinzu. Realm gehört zur Gruppe der noSQL-DBMS und verwendet Modellklassen, um die Datenstruktur zu beschreiben, nicht Tabellen und die Beziehungen zwischen ihnen. Damit entfällt das für relationale Datenbanken relevante Problem des objektrelationalen Mappings und die Kosten für die Datenkonvertierung werden reduziert.
ObjectBox – führend in der Leistung
ObjectBox – das neueste der betrachteten DBMS. Es wurde von Green Robot entwickelt, einem bekannten Android-Entwickler für seine Produkte GreenDao, EventBus und Essentials. ObjectBox wurde ursprünglich als DBMS für Mobil- und IoT-Geräte entwickelt; daher ist es im Vergleich zu seinen Konkurrenten in Bezug auf die Betriebsgeschwindigkeit und die Bequemlichkeit der Integration in mobile Anwendungen sehr günstig. Wie Realm implementiert sie einen noSQL-Ansatz, bei dem die Attribute von Modellklassen und die Beziehungen zwischen ihnen direkt in die Datenbank geschrieben werden.
Die Verwendung der Datenbank in Ihrer Android-Anwendung bedeutet in den meisten Fällen, dass Sie mit strukturierten und verknüpften Informationen umgehen müssen. Die einfache Speicherung von primitiven Daten ist unkompliziert. Komplexere Manipulationen mit Informationen können jedoch eine echte Herausforderung für einen Entwickler darstellen und erfordern gute Kenntnisse der Werkzeuge des von ihm gewählten DBMS. Versuchen wir einmal zu betrachten, wie die beliebtesten mobilen Datenbanken – SQLite, Realm und ObjectBox – komplexe Probleme lösen:
- Speicherung komplexer Objekte
- Empfang von Daten mit vielen Bedingungen
- Abrufen verwandter Objekte (eins-zu-eins, eins-zu-viele, viele-zu-viele).
Nehmen wir als Beispiel eine Anwendung für eine Bibliothek, die mit Daten über Bücher arbeiten soll. Jedes Buch hat einen oder mehrere Autoren, Titel, Erscheinungsort und -jahr, Verlag, aktuellen Status (vorhanden, verfügbar, zur Restaurierung) usw. Überlegen Sie, welche Optionen für die Organisation der Daten im Falle der Verwendung jedes der oben genannten DBMS genutzt werden können.
Speichern von komplexen Objekten
SQLite und Raum
Eines der Merkmale relationaler Datenbanken ist die Unterstützung streng definierter Datentypen, die den Tabellenspalten zugewiesen werden können. Im Falle von SQLite sind dies INTEGER, REAL, TEXT, BLOB und NULL. Um Objekte zu speichern, müssen Sie sie in eine Reihe von Attributen der entsprechenden Typen umwandeln. Room ermöglicht Ihnen dies auf verschiedene Weise:
- Hinzufügen der Annotation @Entity zu einer Klasse, die ein Datenmodell beschreibt. In diesem Fall erstellt Room eine separate Tabelle in der SQLite-Datenbank und speichert Ihre Objekte als Zeilen in dieser Tabelle. Mit Hilfe von Annotationen können Sie Spaltennamen, notwendige Felder zum Speichern, Dateneigenschaften usw. angeben.
import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "books") data class Book( @PrimaryKey val id: Int, val title: String?, val city: String?, val year: Int?, @ColumnInfo(name = "publishing_house") val publishingHouse: String? )
- Verwendung von TypeConverters, die die Logik für die Umwandlung eines Objekts in einen einfachen Datentyp beschreiben, der für die Speicherung in einer Zelle geeignet ist. Ein klassisches Beispiel ist, dass der Datentyp „Datum“ als Zeitstempel in der Datenbank gespeichert werden kann.
class Converters { @TypeConverter fun fromUnix(unixTime: Long?): Date? { return if (unixTime == null) null else Date(unixTime) } @TypeConverter fun toUnix(date: Date?): Long? { return date?.time } }
In primitive Werte umgewandelt werden können komplexere Objekte, wie Arrays und Listen.
@TypeConverter public static ArrayList
fromString(String value) { Type listType = new TypeToken >() {}.getType(); return new Gson().fromJson(value, listType); } @TypeConverter public static String fromArrayList(ArrayList list) { Gson gson = new Gson(); String json = gson.toJson(list); return json; } - Verwendung der @Embedded-Anmerkung. Wenn Sie diese verwenden, erstellt Room automatisch zusätzliche Felder für die Attribute des verschachtelten Objekts in der übergeordneten Tabelle. Bei aller Einfachheit ist die größte Einschränkung dieses Ansatzes offensichtlich – er eignet sich nur zum Speichern verschachtelter Objekte.
Realm
Da Realm ursprünglich als objektorientierte Datenbank entwickelt wurde, erfordert die Konvertierung von Anwendungsmodellen, um sie in der Datenbank zu speichern, nur minimalen Aufwand für den Entwickler. Zunächst einmal muss die Modellklasse für die Vererbung zugänglich sein (in Java muss nichts geändert werden, in Kotlin muss der Open Modifier hinzugefügt werden). Außerdem sollte diese Klasse ein Abkömmling der abstrakten Klasse RealmObject sein, die die Logik der Interaktion mit der Datenbank kapselt.
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class Book(
@PrimaryKey var id: Int,
var title: String?,
var city: String?,
var year: Int?,
var publishingHouse: String?
) : RealmObject()
Das Speichern von verschachtelten Objekten ist ebenfalls kein Problem. Die Anforderungen für verschachtelte Objekte sind die gleichen wie für die Modellklasse.
open class Author(
@PrimaryKey var id: Int,
var firstName: String?,
var lastName: String?,
var dateOfBirth: Date?,
var dateOfDeath: Date?,
var books: RealmList<Book>
) : RealmObject()
ObjectBox
Das Funktionsprinzip der ObjectBox ähnelt dem von Realm, daher sind auch hier nur minimale Manipulationen an den Modellklassen erforderlich, um sie in der Datenbank zu speichern: Hinzufügen von @Entity- und @Id-Annotationen, öffentlichen Attributen oder deren Getter und Setter.
@Entity
data class Book(
@Id var id: Long = 0,
var title: String?,
var city: String?,
var year: Int?,
var publishingHouse: String?
)
Um Verknüpfungen zu anderen Objekten zu speichern, muss die Art der Verbindung angegeben werden – ToOne, wenn es nur ein verbundenes Objekt gibt, oder ToMany, wenn es mehrere gibt. In unserem Fall können wir unsere Klasse Book um einen Link zu den Autoren ergänzen.
lateinit var authors: ToMany<Author>
Abrufen von Daten mit vielen Bedingungen
SQLite und Room
Sie bieten leistungsstarke Funktionen zur Erstellung komplexer Abfragen auf der Grundlage von SQL-Ausdrücken. Um sie nutzen zu können, müssen Sie über gute SQL-Kenntnisse verfügen und die Funktionen ihrer Implementierung in SQLite kennen.
Eine Abfrage sieht zum Beispiel so aus, dass alle Bücher nach einem bestimmten Jahr, alphabetisch sortiert, zurückgegeben werden.
@Query("SELECT * FROM books WHERE year > :minYear ORDER BY title")
fun loadAllBooksNewerThan(minYear: Int): List
Eine Abfrage, die alle Bücher mit einem bestimmten Titel zurückgibt, die in den angegebenen Jahren veröffentlicht wurden.
@Query("SELECT * FROM books WHERE title LIKE :search " +
"AND year BETWEEN :dateFrom AND :dateTo")
fun findBooksByTitleForPeriod(search: String, dateFrom: Int, dateTo: Int): List<Book>
Eine Abfrage, die eine Liste der in einer Datenbank gespeicherten Verlage für eine bestimmte Stadt zurückgibt.
@Query("SELECT DISTINCT publishing_house from books WHERE city = :city")
fun getPublishingHousesForCity(city: String): List
Realm
Dieses DBMS bietet einen umfangreichen Satz von Operatoren zum Abrufen und Filtern von Daten. Durch die Kombination dieser Operatoren können Sie eine beliebig komplexe Abfrage erstellen. Lassen Sie uns die SQLite-Beispiele für Realm umschreiben.
fun loadAllBooksNewerThan(minYear: Int): List<Book> {
val books = realm
.where(Book::class.java)
.greaterThan("year", minYear)
.sort("title")
.findAll()
return realm.copyFromRealm(books)
}
fun findBooksByTitleForPeriod(search: String, dateFrom: Int, dateTo: Int): List<Book>? {
val books = realm
.where(Book::class.java)
.like("title", search)
.and()
.between("year", dateFrom, dateTo)
.findAll()
return realm.copyFromRealm(books)
}
fun getPublishingHousesForCity(city: String): List<String?> {
val books = realm
.where(Book::class.java)
.equalTo("city", city)
.distinct("publishingHouse")
.findAll()
return realm.copyFromRealm(books).map { it.publishingHouse }
}
ObjectBox
In diesem DBMS wird zur Erstellung von Abfragen der QueryBuilder verwendet, der über ein umfangreiches Arsenal an Funktionen zur Erstellung der gewünschten Datenauswahl verfügt. Das Prinzip der Abfrageerstellung ist dem von Realm sehr ähnlich. Der Hauptunterschied besteht darin, dass in Realm die Abfrage Daten zurückgibt, die in der Klasse RealmResults verpackt sind. Deren Hauptaufgabe ist es, die Links zu den Daten auf dem neuesten Stand zu halten und Funktionen für den Zugriff auf sie bereitzustellen. Um die Daten in Rohform zu erhalten, müssen wir sie mit der Methode copyFromRealm () aus der Datenbank kopieren. ObjectBox gibt die Daten sofort zurück, wenn sie angefordert werden. Das zweite Merkmal ist die Fähigkeit, Anfragen zwischenzuspeichern und wiederzuverwenden. Beispiele, mit denen wir bereits vertraut sind, wenn wir eine ObjectBox verwenden, sehen wie folgt aus.
fun loadAllBooksNewerThan(minYear: Long): List {
return bookBox.query()
.greater(Book_.year, minYear)
.order(Book_.title)
.build()
.find()
}
fun findBooksByTitleForPeriod(search: String, dateFrom: Long, dateTo: Long): List<Book> {
return bookBox.query()
.contains(Book_.title, search)
.and()
.between(Book_.year, dateFrom, dateTo)
.build()
.find()
}
fun getPublishingHousesForCity(city: String): List<String> {
return bookBox.query()
.equal(Book_.city, city)
.build()
.property(Book_.publishingHouse)
.distinct()
.findStrings()
.toList()
}
Abrufen verwandter Entitäten (eins-zu-eins, eins-zu-viele, viele-zu-viele)
SQLite und Room
Obwohl die Implementierung von Beziehungen zwischen Entitäten eine Stärke von relationalen Datenbanken ist, erfordert ihre Umsetzung in SQLite und Room eine erhebliche Menge an Code und Entwickleraufwand. Der Grund dafür ist das Verbot der Verwendung von Objektreferenzen. Wie die Macher von Room erklären, führt der Zugriff auf verschachtelte Objekte in der Android-Anwendung zu einem verzögerten Laden durch den Haupt-Thread mit allen sich daraus ergebenden Konsequenzen: Verzögerung beim Rendern der Benutzeroberfläche oder übermäßiger Ressourcenverbrauch. Daher wird die naheliegendste Möglichkeit, Beziehungen zwischen Entitäten zu implementieren, nicht unterstützt.
Um Beziehungen in Room zu implementieren, müssen Sie eine zusätzliche Klasse erstellen, die zurückgegeben wird, wenn die entsprechenden Daten angefordert werden.
Ein Beispiel für eine eins-zu-eins und eine-zu-viele Beziehung. Erstellen wir eine Meta-Klasse, die Service-Informationen über jedes Buch enthält: Code und Status. Jedes Buch kann nur eine Metadatei haben und jede Metadatei bezieht sich nur auf ein Buch.
@Entity(tableName = "meta")
data class Meta(
@PrimaryKey val code: Long,
val status: Int
)
data class BookWithMeta(
@Embedded val book: Book,
@Relation(
parentColumn = "id",
entityColumn = "bookId"
)
val meta: Meta
)
Bücher mit Metadaten werden in Form von mehreren Aufträgen empfangen, daher benötigen sie die @Transaction-Annotation.
@Transaction
@Query("SELECT * FROM books")
fun getBooksWithMeta(): List<BookAndMeta>
Die Eins-zu-Viel-Beziehung in Room ist identisch implementiert, aber anstelle eines Verweises auf ein einzelnes Objekt wird eine Listenverknüpfung angegeben.
Ein Beispiel für eine Many-to-many-Beziehung. Erstellen wir nun eine Version der Klasse Author, die wir bereits aus dem Beispiel von Realm kennen.
class Author(
@PrimaryKey var authorId: Int,
var firstName: String?,
var lastName: String?,
var dateOfBirth: Date?,
var dateOfDeath: Date?,
var books: List<Book>
)
Um die Beziehung zu implementieren, müssen Sie drei zusätzliche Klassen erstellen: einen Link und zwei Abfrageergebnisse.
@Entity(primaryKeys = ["bookId", "authorId"])
data class AuthorBookCrossRef(
val bookId: Int,
val authorId: Int
)
data class AuthorWithBooks(
@Embedded val author: Author,
@Relation(
parentColumn = "authorId",
entityColumn = "bookId",
associateBy = @Junction(AuthorBookCrossRef::class)
)
val books: List<Book>
)
data class BookWithAuthors(
@Embedded val book: Book,
@Relation(
parentColumn = "bookId",
entityColumn = "authorId",
associateBy = @Junction(AuthorBookCrossRef::class)
)
val authors: List<Author>
)
Die Daten werden wie folgt angefordert.
@Transaction
@Query("SELECT * FROM books")
fun getBooksWithAuthors(): List
@Transaction
@Query("SELECT * FROM authors")
fun getAuthorsWithBooks(): List
Realm
Der Unterschied beim Abrufen von Bezugsdaten zwischen Room und Realm ist enorm. Der Abruf erfolgt auf dieselbe Weise wie bei einem einfachen Zugriff auf die Eigenschaft des Objekts. Ein wichtiger Punkt – bei der Implementierung einer Many-to-many-Kommunikation ist die Verbindung zwischen Objekten standardmäßig unidirektional. Für eine bidirektionale Abhängigkeit zwischen Objekten müssen Sie die @LinkingObjects-Annotation verwenden.
val book = realm
.where(Book::class.java)
.equalTo("id", bookId)
.findFirst()
val authors = book?.authors
ObjectBox
Das Abrufen verwandter Entitäten ist Realm sehr ähnlich und erfolgt in einer Zeile.
Eins-zu-eins-Beispiel
val meta = bookBox[bookId].meta.target
Many-to-many-Beispiel
val authors = bookBox[bookId].authors
Schlussfolgerung
Kriterium | |||
---|---|---|---|
Typ | Relational mit ORM | Objektorientiert | Objektorientiert |
Eintrittsschwelle | Niedrig | Mitte | Niedrig |
Speichern von komplexen Objekten | Es ist praktisch, wenn man mit einfachen Typen arbeitet, aber es erfordert zusätzliche Zeit, um Beziehungen zwischen Objekten zu implementieren. | Die Speicherung komplexer Objekte erfordert nur minimalen Aufwand. | Die Speicherung komplexer Objekte erfordert nur minimalen Aufwand. |
Abrufen von Daten mit vielen Bedingungen | Großartiges Toolkit für die Erstellung komplexer mehrstufiger Abfragen. Sie müssen SQL kennen. | Eine große Anzahl integrierter Funktionen zur Auswahl und Sortierung von Daten. | Eine große Anzahl integrierter Funktionen zur Auswahl und Sortierung von Daten. |
Abrufen von verwandten Entitäten | Erfordert einen erheblichen Zeitaufwand für die Umsetzung. | Kommt in einigen wenigen Zeilen vor. | Tritt in einer Zeile auf. |
Typ | Relational mit ORM |
Eintrittsschwelle | Niedrig |
Speichern von komplexen Objekten | Es ist praktisch, wenn man mit einfachen Typen arbeitet, aber es erfordert zusätzliche Zeit, um Beziehungen zwischen Objekten zu implementieren. |
Abrufen von Daten mit vielen Bedingungen | Großartiges Toolkit für die Erstellung komplexer mehrstufiger Abfragen. Sie müssen SQL kennen. |
Abrufen von verwandten Entitäten | Erfordert einen erheblichen Zeitaufwand für die Umsetzung. |
Typ | Objektorientiert |
Eintrittsschwelle | Mitte |
Speichern von komplexen Objekten | Die Speicherung komplexer Objekte erfordert nur minimalen Aufwand. |
Abrufen von Daten mit vielen Bedingungen | Eine große Anzahl integrierter Funktionen zur Auswahl und Sortierung von Daten. |
Abrufen von verwandten Entitäten | Kommt in einigen wenigen Zeilen vor. |
Typ | Objektorientiert |
Eintrittsschwelle | Niedrig |
Speichern von komplexen Objekten | Die Speicherung komplexer Objekte erfordert nur minimalen Aufwand. |
Abrufen von Daten mit vielen Bedingungen | Eine große Anzahl integrierter Funktionen zur Auswahl und Sortierung von Daten. |
Abrufen von verwandten Entitäten | Tritt in einer Zeile auf. |
Daher haben die komplexen Vorgänge beim Speichern und Abrufen von Daten in den beliebtesten Datenbanken für Android-Anwendungen ihre eigenen Besonderheiten. Die Kombination aus SQLite und Room ist zwar die beliebteste Lösung für die Speicherung strukturierter Daten, erfordert aber erhebliche Manipulationen, um zusammenhängende Objekte zu speichern und abzurufen, sowie gute Kenntnisse der SQL-Grundlagen für eine effiziente Arbeit. Realm und ObjectBox, die auf einem objektorientierten Ansatz für die Datenorganisation beruhen, sind ihren Konkurrenten sowohl in Bezug auf die Geschwindigkeit als auch die Benutzerfreundlichkeit deutlich überlegen.