SQLite DB mitliefern, bei Update nur bestimme Werte überschreiben

  • Antworten:21
Alexander Brummer
  • Forum-Beiträge: 32

14.08.2012, 14:24:36 via Website

Hallo liebe Developer,

ich arbeite zurzeit an meiner ersten App. Es handelt sich um eine Art Quiz mit speziellem Modus (also nicht einfach Frage und 4 Antwort-Möglichkeiten :)).
Die Fragen speichere ich in einer SQLite-Datenbank und liefere sie im assets-Ordner mit. Diese wird dann in das App-Verzeichnis kopiert, sofern noch nicht geschehen, wobei ich mich an diesem Tutorial orientiert habe.
Da ich jetzt so gut wie alles fertig habe, muss ich mich nun zum Schluss erneut um die DB kümmern. Mein Problem sieht folgendermaßen aus:
Bisher habe ich es einfach so gemacht, dass in einem Spiel verhindert wird, dass eine Frage mehrfach vorkommt. Beim nächsten Spiel kann es dann aber durchaus vorkommen, dass eine Frage nochmal drankommt, denn ich wähle sie einfach random aus. Ich glaube aber nun, dass es besser ist, wenn eine Frage nie mehr dran kommt, falls sie schon einmal gestellt wurde (solange bis alle Fragen durch sind :D).
Um dies zu schaffen habe ich drei Ansätze. Entweder ich füge noch eine Spalte ein, in der vermerkt wird, ob die Frage schon dran war. Oder ich mache eine neue Tabelle, wo nur diese Information gespeichert wird... oder eine ganze neue Datenbank.
Vor allem letzteres mag sich absurd anhören, aber das Problem ist ja, dass ich die Fragen bei Updates auch erweitern oder Rechtschreibfehler usw. korrigieren möchte. Das wäre kein Problem, wenn ich die Zusatzinformation nicht benötigen würde, denn dann würde ich einfach im Falle einer neueren DB-Version die Datenbank überschreiben.
Aber jetzt sind da ja auch noch Informationen drinnen, die von User zu User unterschiedlich sind und die dürfen eben nicht überschrieben würden.
Für mich am leichtesten wäre deshalb die dritte Variante mit der eigenen Datenbank, die aber ziemlich unschön und ineffizient ist wie ich vermute.

Was würdet ihr vorschlagen?
Ich hoffe ihr versteht mein Problem. :D

Danke schonmal.
Gruß ;)
Alex

Antworten
Gelöschter Account
  • Forum-Beiträge: 694

14.08.2012, 14:45:02 via Website

Hat alles seine Vor- und Nachteile - über welche Größenordnungen reden wir hier? Anzahl Fragen, Anzahl Antworten, Anzahl mögliche (und somit ein wenig geschätzt) Upgrades, Anzahl mögliche Änderungen pro Upgrade.

Bei einem kleineren Volumen könntest Du zum Beispiel die Antworten in den Preferencen hinterlegen und immer die komplette Datenbank austauschen ...

Antworten
Alexander Brummer
  • Forum-Beiträge: 32

14.08.2012, 15:18:56 via Website

Etwa 15 Spalten. Und die Anzahl der Datensätze (also der Fragen-/Antworteneinheiten) wird anfangs 200 sein, ich möchte es aber schon auf 1000 steigern, sofern meine kreative Ader mitspielt, d.h. nach und nach immer ca. 200 weitere einfügen.
Preferences mit 200 - 1000 Werten, läuft das noch einigermaßen effizient?

— geändert am 14.08.2012, 15:20:41

Antworten
Gelöschter Account
  • Forum-Beiträge: 694

14.08.2012, 15:34:22 via Website

Datenbanktechnisch sind 200-1000 Rows lachhaft. Eine Transaktion, Prepared Statements mit Parameter Markern - das geht so schnell da braucht man noch nicht mal eine Eieruhr.

Komplette Datenbank austauschen - ich bleibe dabei. Das entlastet Dich auch von der Versionierung (nicht alle Kunden spielen alle Updates ein und überspringen dann in einem Rutsch 10 Release oder so).

Was die Preferences angeht: Hmm, ich gebe die Frage mal weiter. Ich nutze die Preferences bisher wirklich nur für native Daten. Ein Byte-Stream würde mir als erstes Einfallen - kann man das in die Preferences packen? Es gibt ein putSetString() im Preferences-Editor. Das erscheint mir ein wenig Overkilll - aber was solls:

PK: J/N
-------------
000001: J
000002: N
000003: J
...

Was sagen die anderen?

Antworten
Ansgar M
  • Forum-Beiträge: 1.544

14.08.2012, 15:41:57 via App

Hey,
ich würde die Datenbank einfach read-only aus dem Assets-Ordner nutzen und eine zweite Datenbank (anstelle von SharedPrefs) für die Information zu bereits gehabt / nicht. Verknüpfen über eine ID ist klar - evtl. noch ein Feld um eine Frage als gelöscht / invalid zu erklären (damit man später beim Pflegen der DB nicht mit den IDs durcheinander kommt).

Lg Ansgar

Antworten
Alexander Brummer
  • Forum-Beiträge: 32

14.08.2012, 15:50:13 via Website

@Ansgar: Ja, genau, das hatte ich auch in der engeren Auswahl.
Fand es halt nur etwas doof, extra für die kleine Tabelle dann noch eine Datenbank anzulegen.
Oder doch die Preferences-Variante von Harald?
Also wenn ihr meint, dass eine zweite Datenbank doch kein allzu großes Manko ist, würde ich es wohl so machen?

Antworten
Rafael K.
  • Forum-Beiträge: 2.359

14.08.2012, 15:50:36 via Website

Ansgar M
eine zweite Datenbank (anstelle von SharedPrefs) für die Information zu bereits gehabt / nicht. Verknüpfen über eine ID ist klar - evtl. noch ein Feld um eine Frage als gelöscht / invalid zu erklären (damit man später beim Pflegen der DB nicht mit den IDs durcheinander kommt).
Zweite DB (bzw. Tabelle) wäre auch mein Tipp.
Dann kann man auch recht schnell SELECTs bauen, die alle noch nicht genutzten Fragen liefern.
Wenn man statt einem binären Flag zur Nutzung einer Frage den timestamp der Nutzung speichert, könnte man sogar dafür sorgen, dass sobald alle Fragen einmal genutzt wurden und man wieder von vorne anfängt, man zuerst aus den Fragen randomisiert, deren Nutzun am längsten her ist.
So entsteht IMHO die beste User-Experience.

Antworten
Alexander Brummer
  • Forum-Beiträge: 32

14.08.2012, 21:27:37 via App

Ok danke, dann probiere ich das mal so.

Antworten
Alexander Brummer
  • Forum-Beiträge: 32

04.09.2012, 16:15:26 via Website

Nur wens interessiert, habe es jetzt nach dem Urlaub so gemacht wie beschrieben und es klappt wie gewünscht. Danke! ;)

Antworten
Alexander Brummer
  • Forum-Beiträge: 32

29.10.2012, 19:08:07 via Website

Hallo,

ich habe ein Problem mit meiner Datenbank.
Ich habe vor zwei Wochen die erste Version meiner App veröffentlicht. Die Datenbank wurde wie oben besprochen eingebaut. Kurz: In einer Datenbank, die in assets mitgeliefert wird, stehen die Fragen, in einer weiteren ob die jeweilige Frage schon dran war oder nicht. Beim ersten Mal wird die Fragendatenbank aus assets rauskopiert und die andere Datenbank auch angelegt. Es funktioniert alles wie es soll.
Beim Upgrade sollte einfach die neue Version erkannt werden (durch Erhöhung der db-Versionen in den Databasehelpern) und die neue db über die alte kopiert werden.
Nun habe ich die Datenbank erweitert und wollte ein Update veröffentlichen. Doch beim Testen ergaben sich verschiedene Probleme.
Ich habe zunächst bei mir (SGSII, 4.0.4) die neue signierte APK installiert. Wenn ich nun die eigentliche GameActivity starte, aus der die DatabaseHelpers instanziiert und die Verbindungen zu den Datenbanken erstellt werden, wird die onUpgrade gar nicht aufgerufen. Ich habe im Konstruktor korrekt 2 als Version übergeben, die alte Version ist 1. Per Log habe ich mir die Datenbankversion ausgeben lassen... und sie ist nach dem Update 2 - in die onUpgrade ging er aber wie gesagt nie rein. Ergebnis ist also, dass bei mir einfach die alte Datenbank bestehen bleibt (wenn man die Daten löscht und dann Clues startet, sind die neuen Fragen also mit dabei, da die Datenbank dann ja erst neu erstellt wird).
Okay, als nächstes habe ich die signierte APK auf dem Handy meines Bruders (SG Ace, 2.3.6) ausgeführt. Wenn ich hier das gerade Beschriebene tue, wird "die Anwendung unerwartet beendet". Der Log sagt mir, dass hierbei die onUpgrade ausgeführt wird, dabei aber die Datenbank wohl unbrauchbar wird:
110-29 18:37:22.421: I/CluesGameActivity(15420): New database version: 1
210-29 18:37:22.453: E/Database(15420): Removing corrupt database: /data/data/de.abrdev.clues/databases/cluesDB
310-29 18:37:22.484: D/AndroidRuntime(15420): Shutting down VM
410-29 18:37:22.492: W/dalvikvm(15420): threadid=1: thread exiting with uncaught exception (group=0x40018578)
510-29 18:37:22.500: E/AndroidRuntime(15420): FATAL EXCEPTION: main
610-29 18:37:22.500: E/AndroidRuntime(15420): java.lang.RuntimeException: Unable to start activity ComponentInfo{de.abrdev.clues/de.abrdev.clues.GameActivity}: android.database.sqlite.SQLiteDatabaseCorruptException: database disk image is malformed
Komisch ist auch, dass New database version auf 1 steht. Ich lasse das am Ende der onUpgrade ausgeben und es sollte demnach doch eigentlich 2 sein.

Außerdem ist mir noch aufgefallen, dass bei mir bei der aktuellen funktionsfähigen Version im Log erscheint:
110-29 18:59:40.998: I/SqliteDatabaseCpp(4249): sqlite returned: error code = 1, msg = no such table: Clues, db=/data/data/de.abrdev.clues/databases/cluesDB
Allerdings kann das irgendwie nicht sein, denn die Fragen werden ja sofort danach problemlos ausgelesen und angezeigt und beim Ace meines Bruders ist diese Meldung auch nicht aufgetreten. Also keine Ahnung, ob das vielleicht was mit dem Upgrade-Problem zu tun hat.

Ich weiß echt nicht, woran es liegt, habe schon überall nach Lösungen gesucht, aber nichts gefunden. Deshalb hoffe ich, dass ihr mir helfen könnt.
Bei Bedarf kann ich auch den betreffenden Code posten.

MfG ;)
Alex

Antworten
Alexander Brummer
  • Forum-Beiträge: 32

31.10.2012, 09:07:09 via Website

Hab das Problem jetzt anders beseitigt. Und zwar nutze ich die onUpgrade einfach gar nicht mehr, sondern überprüfe selbst, ob die mitgelieferte Datenbank neuer ist. Falls ja, wird sie wie beim ersten Mal kopiert. So funktioniert es jetzt endlich richtig.
Warum der "normale" Weg solche Probleme bereitet, habe ich aber immer noch nicht herausgefunden. Naja, Hauptsache, es geht jetzt.

Antworten
Gelöschter Account
  • Forum-Beiträge: 694

31.10.2012, 11:29:36 via Website

Wofür benötigst Du den onUpgrade() wenn Du die Datenbank ohnehin komplett austauschst? Diese Methode ist üblicherweise nur für den Fall vorgesehen das Du die Datenbank programmtechnisch auf einen neuen Stand bringen willst.

Die Version der Datenbank (nicht verwechseln mit der SQLite Version) wird mit einem PRAGMA user_version in die Datenbank geschrieben. Wenn onUpgrade() nicht ausgeführt wird dann wird das wohl schon in der von Dir ausgelieferten Datenbank gesetzt sein. Letztendlich macht Android nur die folgende if() Abfrage um zwischen onCreate() oder onUpgrade() zu entscheiden. getVersion() liest das PRAGMA, setVersion() setzt das PRAGMA und mNewVersion ist der Wert den Du beim Aufruf mitgegeben hast (also die 2):

1int version = db.getVersion();
2if (version != mNewVersion) {
3 db.beginTransaction();
4 try {
5 if (version == 0) {
6 onCreate(db);
7 } else {
8 onUpgrade(db, version, mNewVersion);
9 }
10 db.setVersion(mNewVersion);
11 db.setTransactionSuccessful();
12 } finally {
13 db.endTransaction();
14 }
15}

Antworten
Alexander Brummer
  • Forum-Beiträge: 32

31.10.2012, 18:58:47 via Website

Danke für Deine Antwort! Also die user_version der db selbst stand auf 0. Bei meinem Bruder wurde sie ja auch aufgerufen. Allerdings war die db danach kaputt, wahrscheinlich weil - wie du sagst - die Methode nicht zum Austauschen der ganzen db gedacht ist.
Die zweite hingegen erzeuge und erweitere ich ja programmatisch und hier funktioniert es.
Das einzige Komische ist das unterschiedliche Verhalten meines Handys und das meines Bruders. Aber so ein bisschen Mystery ist ja heute ganz passend. :D

Antworten
Alexander Brummer
  • Forum-Beiträge: 32

31.10.2012, 20:42:14 via Website

Beide hatten 0, ich habe daran nichts verändert. Die onCreate() ist leer. Das Kopieren wird in einer eigenen Methode durchgeführt (createDB()), die ich selbst aufrufe.
Die user_version 0 in meiner vorgefertigten Datenbank dürfte aber doch eigentlich gar keine Auswirkungen haben, denn sie befindet sich ja zunächst erst in assets. Die ausschlaggebende user_version übergebe ich doch über den Konstruktor des DBHelpers?

Antworten
Gelöschter Account
  • Forum-Beiträge: 694

31.10.2012, 21:35:17 via Website

Die Entscheidung über onCreate vs. onUpgrade wird im getWritableDatabase getroffen. Zu dem Zeitpunkt sollte die neue Datenbank bereits am Platz sein.

Ich hatte mal vor Jahren im Web eine populäre Methode zum Kopieren einer eigenen Datenbank gesehen - die war aber falsch. Der kopierte im SQLiteOpenHelper statt davor und bekam eine Race-Condition. Der erste Start nach dem Kopieren ging oft schief. Da die Datenbanken gecacht werden war es Zufall wie sein Ablauf war. Erst nach dem zweiten App-Start war die neue Datenbank garantiert geöffnet.

Guck Dir mal genau an zu welchem Zeitpunkt und in welchem Kontext Du kopierst.

Antworten
Alexander Brummer
  • Forum-Beiträge: 32

31.10.2012, 22:19:24 via Website

Ich habe mich im Wesentlichen an dieses Tutorial gehalten. Nur openDatabase habe ich nicht, dafür nutze ich einfach getReadableDatabase, nachdem kopiert wurde.
Glücklicherweise läuft es jetzt ja auch wieder. :)

Antworten
Gelöschter Account
  • Forum-Beiträge: 694

01.11.2012, 00:05:30 via Website

Jau, das war das Tutorial das ich meinte. Hat mir nur Ärger eingehandelt.

Du musst wissen: getReadableDatabase ruft getWritableDatabase auf. Beide rufen somit onCreate/onUpgrade auf. Und nun schau genau hin. Es wird getReadableDatabase aufgerufen zum Anlegen der DB. Es wird danach aber nur noch mit openDatabase gearbeitet. Das erzwingt kein onCreate/onUpgrade. Damit erklärt sich warum diese Methode bei Dir nicht aufgerufen wird.

Antworten
Alexander Brummer
  • Forum-Beiträge: 32

01.11.2012, 00:36:28 via Website

Ja, aber wie gesagt, ich verwende nicht das openDatabase() wie er, sondern ganz normal getReadableDatabase().
Sry dass ich dich damit so nerve. xD

Antworten
Nachbar1990
  • Forum-Beiträge: 33

01.11.2012, 01:08:04 via App

Oh Gott, viel Spaß beim fragen erstellen und unterschätz das nicht :D ich hab auch mal sowas programmiert für das Nokia n900 und hatte mir auch zum Vorsatz gemacht, über 1000 fragen einzubauen. Nach 200 oder so hatte ich keinen Bock mehr.

Ich hab auch eine Funktion eingebaut, durch die die Nutzer Fragen direkt aus dem Spiel an mich senden konnten, das haben aber leider die wenigsten gemacht.

Wenn ich die source noch finde, kann ich dir meinen fragenkatalog schicken, vlt hilft es dir ja

— geändert am 01.11.2012, 01:08:26

Antworten
Alexander Brummer
  • Forum-Beiträge: 32

01.11.2012, 12:36:29 via Website

@Robokopp: Jo, es ist schon ziemlich anstrengend, sich die Fragen auszudenken. Habe nun aber schon über 200, siehe dieser Thread. Falls Du echt noch was hast, schreibe am besten dort, ich glaube, das hier ist der falsche Thread dafür. :)
@Harald:
Wie gesagt läuft das ganze kopieren jetzt, egal ob beim ersten Mal oder beim Überschreiben in einer Methode ab, die ich selbst aufrufe. Die onCreate und onUpgrade sind jetzt beide leer. Also wird da ja eigentlich auch nichts mehr gemacht, wenn ich bzw. meine create-Methode dann getReadableDatabase() aufrufen. So funktioniert es nun auch.
Du meinst wahrscheinlich das Problem, das ich hatte, als ich die Kopierarbeit in der onUpgrade verrichtet hatte?
Ja, da war wohl wirklich das, was Du erläutert hast, der Grund. Vielleicht erklärt das auch den Unterschied vom Handy meines Bruders zu meinem. Denn er hat ein Ace, ich ein S2. Und so wirkte sich die Race-Condition auf beiden Modellen unterschiedlich aus, da mein S2 halt einfach schneller ist. Das könnte sein, oder?

Antworten