Datenbanken und alle Länder dieser Welt

1. Aufgabe

Um ein Beispiel zum Thema Datenbanken zu entwerfen, betrachten wir eine Android-App, die eine Liste der offiziellen Ländernamen in Englisch mit Hilfe einer Datenbank zur Verfügung stellt. Neben den Ländernamen in englischer Notation ergänzen wir den Datenbestand auch um die Länderkürzel gemäß ISO 3166. Hierbei gibt es einige Details zu betrachten. Diese Ländercodes gibt es sowohl – gemäß ISO 3166-1-alpha-2 – als Zwei-Buchstabenkombination (beispielsweise „CH“ für die Schweiz oder „AF“ für Afghanistan) sowie nach ISO 3166-1-alpha-3 als Drei-Buchstabenkombination, zum Beispiel „DEU“ für die Bundesrepublik Deutschland.

Neben diesen beiden Kodierungen mittels lateinischer Buchstaben werden auch dreistellige Codes aus arabischen Ziffern verwendet. Diese numerischen Codes wurden bereits in den 1960er Jahren eingeführt, sind aber nicht so sprechend wie die aus Buchstaben gebildeten Codes. Im Datenbankbestand dieser Fallstudie sind alle alpha- und numerischen Codes verfügbar. Der Anfang dieser Tabelle sieht in etwa so aus:

Name              ISO3   ISO2   Numerisch
-----------------------------------------
Afghanistan       AF     AFG    004
Aland Islands     AX     ALA    248
Albania           AL     ALB    008
Algeria           DZ     DZA    012
American Samoa    AS     ASM    016
Andorra           AD     AND    020

Erstellen Sie eine Android-App, die alle Ländercodes nach ISO-3166 bereitstellt. Dabei sollte es zum Nachzuschlagen eines Landes möglich sein, eine bestimmte Teilmenge von Ländern mit demselben Anfangsbuchstaben auflisten zu können. Zur Gestaltung der Oberfläche orientieren Sie sich bitte an Abbildung 1:

Ländernamen in einer Android-Datenbanken-App.

Abbildung 1. Ländernamen in einer Android-Datenbanken-App.

2. Lösung

Die Android-Klassenbibliothek setzt für den Zugriff auf Datenbanken ein kompaktes und quelloffenes System namens SQLite ein (www.sqlite.org). Es zeichnet sich unter anderem dadurch aus, dass es ohne einen Server („standalone“) arbeitet. Pro Datenbank wird eine Datei im Dateisystem (von Android) angelegt. Trotzdem ist es möglich, dass mehrere Anwendungen eine Datenbank nutzen.

In Android stellt die Klasse SQLiteDatabase alle Methoden zur Verfügung, um mit einer SQLite-Datenbank arbeiten zu können. Ergänzt wird diese Klasse um eine zweite Klasse SQLiteOpenHelper. Sie unterstützt den Entwickler speziell beim Anlegen und bei der Versionsverwaltung einer Datenbank. In einer einfachen Anwendung kommt man aber gut und gerne auch ohne diese Klasse aus. Ich verzichte aber aus einem anderen Grund auf ihre Dienste, nämlich um die zentralen Funktionen für das Arbeiten mit einem Datenbanksystem (Öffnen der Datenbank, Anlegen und Löschen einer SQL-Tabelle) ohne Umwege demonstrieren zu können.

Bevor wir auf den Datenbestand einer Datenbank – genauer: einer Tabelle – zugreifen können, müssen wir uns Zugang zum Datenbanksystem verschaffen. Dies kann in zwei Schritten erfolgen:

  • Aufruf der (in einer Activity) geerbten Methode openOrCreateDatabase:

    SQLiteDatabase
    android.content.ContextWrapper.openOrCreateDatabase (
        String name,  // unique name of the database
        int mode,     // use 0 or MODE_PRIVATE for default operation
        CursorFactory factory  // optional factory class (use null)
    );
  • Aufruf der Methode execSQL an einem SQLiteDatabase-Objekt. Hierzu bedarf es eines klassischen SQL-Befehls zum Anlegen oder Öffnen einer SQL-Tabelle, zum Beispiel:

    CREATE TABLE IF NOT EXISTS AnotherCountries (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT,
        iso_alpha_2_code TEXT,
        iso_alpha_3_code TEXT,
        iso_num_code TEXT UNIQUE
    );

Wir kommen nun auf die Oberfläche der Hauptaktivität zu sprechen. Von den vielen Schaltflächen einmal abgesehen – mit ihrer Hilfe stoßen wir die einzelnen Datenbank-Operationen an – finden wir in der Mitte noch ein Spinner-Steuerelement vor. Mit seiner Hilfe (siehe Abbildung 2) können wir einen Anfangsbuchstaben auswählen, um die Selektion der gesuchten Länder etwas einzuschränken.

Spinner-Steuerelement der Android-Datenbanken-App.

Abbildung 2. Spinner-Steuerelement der Android-Datenbanken-App.


Die verbleibende Gestaltung der Oberfläche stellt keine besonderen Anforderungen an den Entwickler, wovon wir uns in Listing 1 überzeugen können:

01: <LinearLayout
02:     xmlns:android="http://schemas.android.com/apk/res/android"
03:     xmlns:tools="http://schemas.android.com/tools"
04:     android:orientation="vertical"
05:     android:layout_width="match_parent"
06:     android:layout_height="match_parent" >
07: 
08:    <Button
09:         android:id="@+id/button_opencreate_database"
10:         android:layout_width="match_parent"
11:         android:layout_height="wrap_content"
12:         android:layout_margin="3dip"
13:         android:text="@string/button_opencreate_database_caption" />
14:     
15:    <Button
16:         android:id="@+id/button_opencreate_table"
17:         android:layout_width="match_parent"
18:         android:layout_height="wrap_content"
19:         android:layout_margin="3dip"
20:         android:text="@string/button_opencreate_table_caption" />
21:    
22:    <Button
23:         android:id="@+id/button_delete_table"
24:         android:layout_width="match_parent"
25:         android:layout_height="wrap_content"
26:         android:layout_margin="3dip"
27:         android:text="@string/button_delete_table_caption" />
28:         
29:     <Button
30:         android:id="@+id/button_insert"
31:         android:layout_width="match_parent"
32:         android:layout_height="wrap_content"
33:         android:layout_margin="3dip"
34:         android:text="@string/button_insert_caption" />
35:     
36:     <Button
37:         android:id="@+id/button_query_all_countries"
38:         android:layout_width="match_parent"
39:         android:layout_height="wrap_content"
40:         android:layout_margin="3dip"
41:         android:text="@string/button_query_all_countries_caption" />
42:     
43:     <Button
44:         android:id="@+id/button_query_countries_with_first_letter"
45:         android:layout_width="match_parent"
46:         android:layout_height="wrap_content"
47:         android:layout_margin="3dip"
48:         android:text="@string/button_query_countries_with_first_letter_caption" />
49:     
50:     <Spinner
51:         android:id="@+id/spinner_letters"
52:         android:layout_width="fill_parent"
53:         android:layout_height="wrap_content" />
54: 
55:     <Button
56:         android:id="@+id/button_delete_entries"
57:         android:layout_width="match_parent"
58:         android:layout_height="wrap_content"
59:         android:layout_margin="3dip"
60:         android:text="@string/button_delete_entries_caption" />
61:         
62:     <Button
63:         android:id="@+id/button_close"
64:         android:layout_width="match_parent"
65:         android:layout_height="wrap_content"
66:         android:layout_margin="3dip"
67:         android:text="@string/button_close_caption" />
68:     
69:     <ScrollView 
70:         android:layout_width="match_parent"
71:         android:layout_height="match_parent"
72:         android:scrollbars="vertical"
73:         android:layout_margin="3dip" >
74:     
75:         <TextView 
76:             android:id="@+id/textview_query"
77:             android:layout_width="match_parent"
78:             android:layout_height="wrap_content"
79:             android:layout_margin="3dip" />
80: 
81:     </ScrollView>
82:         
83: </LinearLayout>

Beispiel 1. Datei activity_main.xml: Gestaltung der Oberfläche.


Etwaige Ids, die in der Datei activity_main.xml (Listing 1) für die Namensgebung der einzelnen Steuerelemente eine Rolle spielen, sind in der Datei strings.xml hinterlegt:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Database Sample Another World</string>
    <string name="action_settings">Settings</string>

    <string name="button_opencreate_database_caption">Open/Create Database</string>
    <string name="button_opencreate_table_caption">Open/Create Table</string>
    <string name="button_delete_table_caption">Delete Table</string>
    <string name="button_insert_caption">Insert Entries into Table</string>
    <string name="button_query_all_countries_caption">Query all Entries from Table</string>
    <string name="button_query_countries_with_first_letter_caption">Query Entries with first Letter</string>
    <string name="button_delete_entries_caption">Delete Entries from Table</string>
    <string name="button_close_caption">Close Database</string>
</resources>

Damit kommen wir zum Kernstück der Anwendung, der Implementierung ihrer Hauptaktivität in Listing 2:

001: package com.example.databaseapp;
002: 
003: import java.util.ArrayList;
004: import java.util.Locale;
005: import android.os.Bundle;
006: import android.view.View;
007: import android.app.Activity;
008: import android.content.ContentValues;
009: import android.content.Context;
010: import android.database.Cursor;
011: import android.database.SQLException;
012: import android.database.sqlite.SQLiteDatabase;
013: import android.view.View.OnClickListener;
014: import android.widget.AdapterView;
015: import android.widget.AdapterView.OnItemSelectedListener;
016: import android.widget.ArrayAdapter;
017: import android.widget.Button;
018: import android.widget.Spinner;
019: import android.widget.TextView;
020: 
021: public class MainActivity extends Activity implements OnClickListener {
022: 
023:     private static final String DATABASE_NAME = "AnotherWorld";
024:     private static final String SQLITE_TABLE  = "AnotherCountries";
025:     private static final String KEY_ROWID     = "id";
026:     private static final String KEY_NAME      = "name";
027:     private static final String KEY_CODE2     = "iso_alpha_2_code";
028:     private static final String KEY_CODE3     = "iso_alpha_3_code";
029:     private static final String KEY_CODENUM   = "iso_num_code";  
030:     
031:     private static final String DATATABLE_CREATE =
032:         "CREATE TABLE IF NOT EXISTS " + SQLITE_TABLE + " (" +
033:         KEY_ROWID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
034:         KEY_NAME + " TEXT," +
035:         KEY_CODE2 + " TEXT," +
036:         KEY_CODE3 + " TEXT," +
037:         KEY_CODENUM + " TEXT UNIQUE);";  
038: 
039:     private static final String DATATABLE_DELETE =
040:         "DROP TABLE IF EXISTS " + SQLITE_TABLE;
041:     
042:     private SQLiteDatabase sqlLiteDB;
043:     
044:     private TextView textviewQuery;
045:     private Spinner spinnerLetters;
046:     private String selectedLetter;
047:     
048:     @Override
049:     protected void onCreate(Bundle savedInstanceState) {
050:         
051:         super.onCreate(savedInstanceState);
052:         setContentView(R.layout.activity_main);
053: 
054:         this.textviewQuery = (TextView) this.findViewById(R.id.textview_query);
055: 
056:         // populate spinner with upper-case letters
057:         ArrayList<String> spinnerArray = new ArrayList<String>();
058:         for (char ch = 'A'; ch <= 'Z'; ch ++ )
059:             spinnerArray.add(new String (Character.toString(ch)));
060: 
061:         this.spinnerLetters = (Spinner) this.findViewById(R.id.spinner_letters);
062:         ArrayAdapter<String> spinnerAdapter =
063:             new ArrayAdapter<String> (this, android.R.layout.simple_spinner_item, spinnerArray);
064:         
065:         this.spinnerLetters.setAdapter(spinnerAdapter);
066:         this.spinnerLetters.setOnItemSelectedListener(new OnItemSelectedListener () {
067: 
068:             @Override
069:             public void onItemSelected(AdapterView<?> parent, View view,
070:                 int pos, long id) {
071:                 
072:                 selectedLetter = (String) parent.getItemAtPosition(pos);
073:             }
074: 
075:             @Override
076:             public void onNothingSelected(AdapterView<?> parent) { }
077:         });
078: 
079:         // connect event handler
080:         Button b1 = (Button) this.findViewById(R.id.button_opencreate_database);
081:         Button b2 = (Button) this.findViewById(R.id.button_opencreate_table);
082:         Button b3 = (Button) this.findViewById(R.id.button_insert);
083:         Button b4 = (Button) this.findViewById(R.id.button_query_all_countries);
084:         Button b5 = (Button) this.findViewById(R.id.button_query_countries_with_first_letter);
085:         Button b6 = (Button) this.findViewById(R.id.button_delete_entries);
086:         Button b7 = (Button) this.findViewById(R.id.button_delete_table);
087:         Button b8 = (Button) this.findViewById(R.id.button_close);
088:         
089:         b1.setOnClickListener(this);
090:         b2.setOnClickListener(this);
091:         b3.setOnClickListener(this);
092:         b4.setOnClickListener(this);
093:         b5.setOnClickListener(this);
094:         b6.setOnClickListener(this);
095:         b7.setOnClickListener(this);
096:         b8.setOnClickListener(this);
097:     }
098: 
099:     @Override
100:     public void onClick(View v) {
101:         
102:         int id = v.getId();
103:         if (id == R.id.button_opencreate_database) {
104:             // create instance of SQLiteDatabase class
105:             this.sqlLiteDB = this.openOrCreateDatabase(DATABASE_NAME, Context.MODE_PRIVATE, null);
106:         }
107:         else if (id == R.id.button_opencreate_table) {
108:             this.sqlLiteDB.execSQL(DATATABLE_CREATE);
109:         }
110:         else if (id == R.id.button_insert) {
111:             this.insertCountries();
112:         }
113:         else if (id == R.id.button_query_all_countries) {
114:             this.queryAllCountries();                 
115:         }
116:         else if (id == R.id.button_query_countries_with_first_letter) {
117:             this.queryAllCountriesUsingWhereWithFirstLetter(this.selectedLetter);      
118:         }
119:         else if (id == R.id.button_delete_entries) {
120:             this.deleteCountries();
121:         }
122:         else if (id == R.id.button_delete_table) {
123:             this.sqlLiteDB.execSQL(DATATABLE_DELETE);
124:         }
125:         else if (id == R.id.button_close) {
126:             
127:             if (this.sqlLiteDB != null) {
128:                 this.sqlLiteDB.close();
129:                 this.sqlLiteDB = null;
130:             }
131:         } 
132:     }
133:     
134:     // helper methods
135:     private void insertCountry (String code, String name, String continent, String region) {
136: 
137:         // create a row map (keys should be the column names, values the column values)
138:         ContentValues row = new ContentValues();
139:         row.put(KEY_NAME, code);
140:         row.put(KEY_CODE2, name);
141:         row.put(KEY_CODE3, continent);
142:         row.put(KEY_CODENUM, region);
143:         
144:         try {
145:             this.sqlLiteDB.insert(SQLITE_TABLE, null, row);
146:         }
147:         catch (SQLException e) {
148:             System.out.println("> insertCountry: Internal Error: " + e.getMessage());
149:         }
150:     }
151:     
152:     private void insertCountries() {
153: 
154:         for (int i = 0; i < CountriesList.getCount(); i ++) {
155:             String[] desc = CountriesList.getCountryDescription(i);
156:             this.insertCountry (desc[0], desc[1], desc[2], desc[3]);
157:         }
158:     }
159:     
160:     public void queryAllCountries() {
161:         
162:         // clear user interface
163:         this.textviewQuery.setText("");
164:         
165:         String[] cols = new String[] {KEY_ROWID, KEY_NAME, KEY_CODE2, KEY_CODE3, KEY_CODENUM};
166:         Cursor cursor = this.sqlLiteDB.query(SQLITE_TABLE, cols, null, null, null, null, null); 
167:         if (cursor != null) {
168:             cursor.moveToFirst();
169:             
170:             int n = 0;
171:             while (cursor.isAfterLast() == false) {
172:                 
173:                 String name = cursor.getString(1);
174:                 String iso_code2 = cursor.getString(2);
175:                 String iso_code3 = cursor.getString(3);
176:                 String iso_code_num = cursor.getString(4);
177:                 
178:                 n ++;
179:                 String row1 = String.format(Locale.getDefault(), "%03d: %s%n", n, name);
180:                 String row2 = String.format(Locale.getDefault(),
181:                         "ISO2: %s, ISO3: %s, ISONUM: %s%n%n", iso_code2, iso_code3, iso_code_num);
182:                 
183:                 this.textviewQuery.append(row1);
184:                 this.textviewQuery.append(row2);
185:                 
186:                 cursor.moveToNext();
187:             }
188:             cursor.close();
189:         }
190:     }
191:     
192:     public void queryAllCountriesUsingWhereWithFirstLetter(String ch) {
193:         
194:         // clear user interface
195:         this.textviewQuery.setText("");
196:         
197:         String orderByClause = KEY_CODENUM + " ASC";
198:         String whereClause = KEY_NAME + " LIKE '" + ch + "%'";
199:         String[] cols = new String[] {KEY_ROWID, KEY_NAME, KEY_CODE2, KEY_CODE3, KEY_CODENUM};
200:         
201:         Cursor cursor = this.sqlLiteDB.query(
202:                 SQLITE_TABLE, cols, whereClause, null, null, null, orderByClause); 
203:         
204:         if (cursor != null) {
205:             cursor.moveToFirst();
206:             
207:             int n = 0;
208:             while (cursor.isAfterLast() == false) {
209:                 
210:                 String name = cursor.getString(1);
211:                 String iso_code2 = cursor.getString(2);
212:                 String iso_code3 = cursor.getString(3);
213:                 String iso_code_num = cursor.getString(4);
214:                 
215:                 n ++;
216:                 String row1 = String.format(Locale.getDefault(), "%03d: %s%n", n, name);
217:                 String row2 = String.format(Locale.getDefault(),
218:                         "ISO2: %s, ISO3: %s, ISONUM: %s%n%n", iso_code2, iso_code3, iso_code_num);
219:                 
220:                 this.textviewQuery.append(row1);
221:                 this.textviewQuery.append(row2);
222:                 
223:                 cursor.moveToNext();
224:             }
225:             cursor.close();
226:         }
227:     }
228:     
229:     private void deleteCountries() {    
230:         
231:         int rows = 0;
232:         try {
233:             rows = this.sqlLiteDB.delete(SQLITE_TABLE, null , null);
234:         }
235:         catch (Exception e) {
236:             System.out.println("> deleteCountries: Internal Error: delete failed !!!");
237:         }
238:     }
239: }

Beispiel 2. Datei MainActivity.java: Implementierung der Logik.


Um die SQL-Tabelle AnotherCountries mit den länderspezifischen Informationen zu befüllen, gibt es eine Hilfsklasse CountriesList. Von dieser Klasse genügt es, einen kleinen Ausschnitt in Listing 3 zu betrachten:

01: package com.example.databaseapp;
02: 
03: public class CountriesList {
04:   
05:     private static String[][] countryInfos;
06:     
07:     static {
08:         
09:         countryInfos = new String[][] {    
10:                 
11:             new String[] { "Afghanistan", "AF", "AFG", "004" },
12:             new String[] { "Aland Islands", "AX", "ALA", "248" }, 
13:             new String[] { "Albania", "AL", "ALB", "008" },
14:             ...
15: 
16:             new String[] { "Yemen", "YE", "YEM", "887" }, 
17:             new String[] { "Zambia", "ZM", "ZMB", "894" }, 
18:             new String[] { "Zimbabwe", "ZW", "ZWE", "716" }
19:         };    
20:     }
21:     
22:     public static int getCount() {
23:         
24:         return countryInfos.length;
25:     }
26:     
27:     public static String[] getCountryDescription(int i) {
28:         
29:         return countryInfos[i];
30:     }
31: }

Beispiel 3. Datei CountriesList.java: Implementierung der Logik.


Abschließend gehen wir auf die Änderungsoperationen-Operationen ein, die wir in unserem Beispiel an die AnotherCountries-Tabelle richten können. In den Zeilen 138 bis 145 von Listing 2 finden wir das Zusammenstellen und Einfügen eines Datensatzes vor. In einem speziellen Objekt des Typs ContentValues sind – pro betroffenem Datensatz – der Spaltenname und der dazugehörige Wert einzufügen. Da es sich um ein listenartiges Objekt handelt, können beliebig viele Wertepaare eingefügt werden. Zum Einfügen des Datensatzes gibt es analog zum SQL-Statement INSERT die Methode insert an einem SQLiteDatabase-Objekt.

Die SQL-Anweisung QUERY gibt es ebenfalls am SQLiteDatabase-Objekt. Sie ist sehr allgemein gehalten und deckt im Prinzip alle Anwendungsfälle ab, die sich in SQL formulieren lassen. Dies können Sie an der Definition der query-Methode leicht erkennen:

Cursor
android.database.sqlite.SQLiteDatabase.query (
  String table,           // table to run the query against
  String[] columns,       // list of which columns to return
  String selection,       // WHERE filter declaring which rows to return
  String[] selectionArgs, // replacing all '?'s in the selection string
  String groupBy,         // GROUP BY filter declaring how to group rows
  String having,          // HAVING filter declaring which row groups to include in the result
  String orderBy          // request how to order the rows (ORDER BY clause)
);

Die beiden Methoden queryAllCountries (Zeilen 160 ff.) und queryAllCountriesUsingWhereWithFirstLetter (Zeilen 192 ff.) aus Listing 2 geben einen kleinen Einblick in die vielfältigen Aufrufmöglichkeiten der query-Methode.