Журнал LinuxFormat - перейти на главную

LXF159:Android:Ба­зы дан­ных

Материал из Linuxformat
Перейти к: навигация, поиск


Android.

Содержание

Android:Ба­зы дан­ных

Android под­дер­жи­ва­ет ба­зы дан­ных SQLite. Уз­най­те от Джуль­­ет­­ты Кемп, как соз­да­вать ба­зы дан­ных и по­стро­ить с ними спи­сок дел, раз­би­тый на ка­те­го­рии.


По­ка в на­ших ста­тьях не за­тра­ги­ва­лось хранение дан­ных. Од­на­ко во мно­гих про­ек­тах оно по­на­до­бит­ся. В Android есть для это­го не один спо­соб, но для струк­ту­ри­ро­ван­ных или взаи­мо­свя­зан­ных дан­ных луч­ше по­дой­дет ба­за дан­ных.

Ес­ли дан­ные хра­нят­ся локаль­но, это оз­на­ча­ет, что ис­поль­зу­ет­ся встро­ен­ная под­держ­ка SQLite (в сле­дую­щей ста­тье мы по­го­во­рим о досту­пе к дан­ным на уда­лен­ных сер­ве­рах и о том, как встро­ить его в свое при­ло­жение).

Что­бы оп­ро­бо­вать это на прак­ти­ке, мы соз­да­дим спи­сок за­дач с дву­мя таб­ли­ца­ми (од­ной для за­дач и од­ной для ка­те­го­рий) и постараемся по­лу­чить пред­став­ление об управ­лении мно­же­ст­­вом таб­лиц и свя­зей.

На­строй­ка про­ек­та и ба­зы дан­ных

Вос­поль­зу­ем­ся API уров­ня 10 (2.3.3):

android create project --target android-10 --name todo \\

--path ~/android/todo --activity ToDo --package com.example.todo

Пре­ж­де чем брать­ся за на­пи­сание ко­да, важ­но тща­тель­но про­ду­мать струк­ту­ру сво­ей ба­зы дан­ных, что­бы по­том не при­шлось тра­тить вре­мя на ис­прав­ления.

В на­шем про­ек­те понадобятся две таб­ли­цы (хо­тя сна­ча­ла мы бу­дем поль­зо­вать­ся толь­ко од­ной из них) – Tasks (за­да­чи) и Categories (ка­те­го­рии):

За­да­чи

  • ID (с ав­то­ин­кре­мен­том)
  • ID Опи­сание за­да­чи
  • ID Срок вы­полнения
  • ID Ссыл­ка на ка­те­го­рию

Ка­те­го­рии

  • ID (с ав­то­ин­кре­мен­том)
  • На­звание ка­те­го­рии

Для на­ше­го ин­тер­фей­са требует­ся пер­вич­ное За­ня­тие, ко­то­рое ото­бра­жа­ет спи­сок за­дач со сро­ка­ми вы­полнения и ка­те­го­рия­ми. Нам так­же по­на­до­бит­ся вто­рое За­ня­тие, ко­то­рое занима­ет­ся до­бав­лением и из­менением за­дач.

На­строй­ка ба­зы дан­ных

Когда план на­шей ба­зы дан­ных го­тов, для его реа­ли­за­ции нам по­на­до­бит­ся класс TodoDatabaseProvider, ко­то­рый яв­ля­ет­ся на­следником ContentProvider и занима­ет­ся взаи­мо­дей­ст­ви­ем с ба­зой дан­ных. Стро­го го­во­ря, ContentProvider ну­жен толь­ко тогда, когда вы хо­ти­те де­лить­ся дан­ны­ми с дру­ги­ми при­ло­жения­ми, а не поль­зо­вать­ся ими толь­ко в од­ном. Од­на­ко с его по­мо­щью удоб­но де­лить­ся дан­ны­ми и внут­ри За­ня­тия; кро­ме то­го, он пре­достав­ля­ет вам раз­лич­ные вспо­мо­га­тель­ные ме­то­ды и ас­пек­ты API в ка­че­­ст­ве ин­тер­фей­са к ба­зе дан­ных SQLite.

Что­бы восполь­зо­вать­ся ContentProvider, ука­жи­те его уникаль­ный иден­ти­фи­ка­тор ре­сур­са (URI) в ви­де пуб­лич­ной кон­стан­ты в верхней час­ти клас­са:

public static final String AUTHORITY = “com.example.todo.tododatabaseprovider”; public static final String TODO_BASE_PATH = “todo”;

public static final Uri CONTENT_URI = Uri.parse(“content://” + AUTHORITY + “/” + TODO_BASE_PATH);

Так­же по­тре­бу­ет­ся за­ре­ги­ст­ри­ро­вать его в AndroidManifest.xml с пол­но­стью оп­ре­де­лен­ным име­нем клас­са ContentProvider:

<provider android:authorities=“com.example.todo.tododatabaseprovider”

android:multiprocess=“true”

android:name=”

com.example.todo.TodoDatabaseProvider”>

</provider>

В ат­ри­бу­те name нуж­но ука­зать пол­но­стью оп­ре­де­лен­ное имя клас­са ContentProvider. Ат­ри­бут authority дол­жен со­от­вет­ст­во­вать зна­чению, за­дан­но­му в ко­де, ко­то­рое оп­ре­де­ля­ет про­вай­де­ра, без ука­зания пу­ти. Что­бы оно бы­ло уникаль­ным, оно долж­но точ­но сов­па­дать с именем клас­са (но за­пи­сы­вать­ся толь­ко в нижнем ре­ги­ст­ре).

Для об­ра­бот­ки соз­дания ба­зы дан­ных мы восполь­зу­ем­ся внут­ренним вспо­мо­га­тель­ным клас­сом inner helper. Соз­дай­те при­ват­ный под­класс SQLiteOpenHelper и пе­ре­гру­зи­те ме­тод onCreate():

private static class TodoDBOpenHelper extends SQLiteOpenHelper {

TodoDBOpenHelper(Context c) {

super(c, DB_NAME, null, DB_VERSION);

}

@Override

public void onCreate(SQLiteDatabase db) {

db.execSQL(“CREATE TABLE” + TASK_TABLE_NAME + “ (“

+ ID + “ INTEGER PRIMARY KEY,”

+ COL_TASK + “ TEXT,”

+ COL_DUEDATE + “ DATE,”

+ COL_CREATEDATE + “ INTEGER,”

COL_CATEGORY_LINK + “ INTEGER REFERENCES ”

+ CATEGORY_TABLE + “(”

+ COL_CATEGORY_ID + “)”

+ “);”);

db.execSQL(“CREATE TABLE” + CATEGORY_TABLE_NAME + “ (“

+ ID + “ INTEGER PRIMARY KEY,”

+ COL_CATEGORY + “ TEXT”

+ “);”);

db.execSQL(“INSERT INTO “ + CATEGORY_TABLE + “VALUES(1, ‘work’);”);

db.execSQL(“INSERT INTO “ + CATEGORY_TABLE + “VALUES(2, ‘personal’);”);

}

} 

Данный за­прос так­же соз­да­ет две за­пи­си в таб­ли­це Category. По­скольку это ContentProvider, ID дол­жен иметь суф­фикс “_id” в обе­их таб­ли­цах; в про­тив­ном слу­чае SQL вы­даст ошиб­ку. Такое происходит в свя­зи с осо­бен­но­стя­ми ContentProvider, а не ча­ст­ных баз дан­ных в Android. После этого реа­ли­зуй­те ме­то­ды onCreate() и query() клас­са ContentProvider в на­шем клас­се TodoDatabaseProvider:

private static final int TODOS = 100;

private static final int TODO_ID = 101;

private static final UriMatcher matcher =

new UriMatcher(UriMatcher.NO_MATCH);

static {

matcher.addURI(AUTHORITY, TODO_BASE_PATH, TODOS);

matcher.addURI(AUTHORITY, TODO_BASE_PATH + “/#”, TODO_ID);

}

@Override

public boolean onCreate() {

db = new TodoDBOpenHelper(getContext());

return true;

}

@Override

public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {

SQLiteQueryBuilder builder = new SQLiteQueryBuilder();

builder.setTables(TASK_TABLE);

int uriType = matcher.match(uri);

switch (uriType) {

case TODO_ID:

builder.appendWhere(ID + “=” + uri.getLastPathSegment());

break;

case TODOS:

break;

default:

throw new IllegalArgumentException(“Unknown URI” + uri);

}

Cursor cursor = builder.query(db.getReadableDatabase(),

projection, selection, selectionArgs, null, null, sortOrder);

cursor.setNotificationUri(getContext().getContentResolver(), uri);

return cursor;

}

UriMatcher об­ра­ба­ты­ва­ет уникаль­ные иден­ти­фи­ка­то­ры ре­сур­сов и фор­ми­ру­ет це­лое чис­ло, в соответствии с кон­крет­ны­м ти­пом URI. Он по­ме­ча­ет URI из Authority и путь за­дан­ным це­лым чис­лом.

В этом слу­чае мы со­постав­ля­ем URI ви­да “com.example.todo.TodoDatabaseProvider/todo” (ко­то­рые, сле­до­ва­тель­но, ссы­ла­ют­ся на весь спи­сок) с це­лым чис­лом 100, а URI ви­да “com.example.todo.TodoDatabaseProvider/todo/1” (ко­то­рые, сле­до­ва­тель­но, ссы­ла­ют­ся на от­дель­ный эле­мент todo) с це­лым чис­лом 101. Это оз­на­ча­ет, что за­тем мы смо­жем об­ра­бо­тать их долж­ным об­ра­зом с по­мо­щью опе­ра­то­ров switch в ме­то­де query().

Ес­ли URI со­от­вет­ст­ву­ет от­дель­но­му за­данию, мы до­бав­ля­ем к SQL-за­про­су вы­ра­жение where, ко­то­рое осу­ще­ст­в­ля­ет по­иск по усло­вию ‘id=#’; в про­тив­ном слу­чае вы­ра­жения where нет, и мы за­пра­ши­ва­ем весь спи­сок за­дач.

SQLiteQueryBuilder, как сле­ду­ет из на­звания, по­мо­га­ет стро­ить за­про­сы для SQLite. Мы ве­лим ему ис­поль­зо­вать таб­ли­цу Task, до­бав­ля­ем где необ­хо­ди­мо вы­ра­жение where, а за­тем генери­ру­ем Cursor из ре­зуль­та­тов SQL-за­про­са. setNotificationUri() ре­ги­ст­ри­ру­ет Cursor для от­сле­жи­вания лю­бых из­менений в со­дер­жи­мом за­дан­но­го URI.

Нам так­же нуж­ны ме­то­ды insert() и update():

@Override

public Uri insert(Uri uri, ContentValues values) {

int uriType = matcher.match(uri);

if (uriType != TODOS) {

throw new IllegalArgumentException(“Invalid URI for insert”);

}

SQLiteDatabase sqlDB = db.getWritableDatabase();

long newID = sqlDB.insert(TASK_TABLE, COL_TASK, values);

if (newID > 0) {

Uri newUri = ContentUris.withAppendedId(uri, newID);

getContext().getContentResolver().notifyChange(uri, null);

return newUri;

} else {

throw new SQLException(“Failed to insert row into “ + uri + “result “ + newID);

}

}

@Override

public int update(Uri uri, ContentValues values, String selection,

String[] selectionArgs) {

int uriType = matcher.match(uri);

if (uriType != TODO_ID) {

throw new IllegalArgumentException(“Invalid URI for update”);

}

long id = ContentUris.parseId(uri);

String where = ID + “=’” + id + “’”;

SQLiteDatabase sqlDB = db.getWritableDatabase();

int result = sqlDB.update(TASK_TABLE, values, where, null);

return result;

}

Ме­тод insert() про­ве­ря­ет URI, иден­ти­фи­ци­рую­щий всю таб­ли­цу (по­сколь­ку мы встав­ля­ем но­вую стро­ку, а не вы­би­ра­ем из су­ще­ст­вую­щих), по­лу­ча­ет доступ­ную для за­пи­си ба­зу дан­ных из при­ват­но­го эк­зем­п­ля­ра TodoDbOpenHelper (db) и за­тем про­бу­ет до­ба­вить дан­ные – Insert.

Пе­ре­мен­ная COL_TASK требуется нам по­то­му, что аб­со­лют­но пустую стро­ку SQL в таб­ли­цу до­ба­влять не станет. Ес­ли зна­чения values пусты (как обыч­но и происходит в на­шем слу­чае), в COL_TASK по­па­дет яв­ный NULL. Ес­ли воз­вра­щае­мое зна­чение нену­ле­вое, это иден­ти­фи­ка­тор вновь до­бав­лен­ной стро­ки; за­тем ме­тод notifyChange() опо­ве­ща­ет все за­ре­ги­ст­ри­ро­ван­ные ме­то­ды-на­блю­да­те­ли об об­нов­лении стро­ки. В про­тив­ном слу­чае ме­тод за­вер­ша­ет­ся неудач­но, и мы по­лу­ча­ем ис­клю­чение.

update() про­ве­ря­ет URI, иден­ти­фи­ци­рую­щий от­дель­ную за­да­чу, и стро­ит вы­ра­жение where с иден­ти­фи­ка­то­ром за­да­чи. Мы опять же по­лу­ча­ем доступ­ную для за­пи­си ба­зу дан­ных и об­нов­ля­ем ее необ­хо­ди­мы­ми зна­чения­ми. Ре­зуль­та­том бу­дет ко­ли­че­­ст­во об­нов­лен­ных строк.

Поль­зо­ва­тель­ский ин­тер­фейс к ба­зе дан­ных

(thumbnail)
Спи­сок с пер­вым до­бав­лен­ным эле­мен­том.

Мы соз­да­ли ба­зу дан­ных и ме­то­ды для досту­па к ней; те­перь нуж­но соз­дать ин­тер­фейс поль­зо­ва­те­ля для досту­па к ба­зе дан­ных. Как ука­за­но вы­ше, наш пер­во­на­чаль­ный ин­тер­фейс – спи­сок за­дач и пункт ме­ню для до­бав­ления но­во­го эле­мен­та спи­ска.

Мы уже в основ­ном с этим спра­ви­лись (ес­ли вам нуж­на по­мощь, оз­на­комь­тесь с ко­дом на DVD). Для по­лу­чения дан­ных из ба­зы и соз­дания спи­ска мы вы­зо­вем ме­тод populateList() из ме­то­да onCreate(). (Дан­ные для ка­те­го­рий мы по­ка не по­лу­ча­ем; зай­мем­ся этим поз­же).

private static final String[] TODO_PROJECTION = newString[] {

TodoDatabaseProvider.ID,

TodoDatabaseProvider.COL_TASK,

TodoDatabaseProvider.COL_DUEDATE

};

private void populateList() {

Cursor cursor = managedQuery(TodoDatabaseProvider.CONTENT_URI,

TODO_PROJECTION, null, null, null);

String[] colNames = { TodoDatabaseProvider.COL_TASK,

TodoDatabaseProvider.COL_DUEDATE };

int[] viewIDs = { R.id.taskname, R.id.duedate };

SimpleCursorAdapter adapter = new SimpleCursorAdapter(

this,

R.layout.todo_item,

cursor,

colNames,

viewIDs

); setListAdapter(adapter);

}

Всю черную ра­бо­ту вы­полнили за вас клас­сы Cursor и SimpleCursorAdapter (уч­ти­те: что­бы это ра­бо­та­ло ав­то­ма­ти­че­­ски, ва­ше За­ня­тие долж­но на­сле­до­вать ListActivity).


Про­ек­ция оп­ре­де­ля­ет, ка­кие столб­цы за­про­са нуж­но вер­нуть. SimpleCursorAdapter по­лу­ча­ет cursor, мас­сив строк столб­цов, ко­то­рые нуж­но ото­бра­зить, и иден­ти­фи­ка­то­ры пред­став­лений viewed, ко­то­рым долж­ны со­от­вет­ст­во­вать дан­ные этих столб­цов. За­тем спи­сок чу­дес­ным об­ра­зом за­пол­ня­ет­ся.

Про­ек­ция оп­ре­де­ля­ет, ка­кие столб­цы за­про­са нуж­но вер­нуть. SimpleCursorAdapter по­лу­ча­ет cursor, мас­сив строк столб­цов, ко­то­рые нуж­но ото­бра­зить, и иден­ти­фи­ка­то­ры пред­став­лений viewed, ко­то­рым долж­ны со­от­вет­ст­во­вать дан­ные этих столб­цов. За­тем спи­сок чу­дес­ным об­ра­зом за­пол­ня­ет­ся.


До­бав­ление эле­мен­та

Те­перь мы мо­жем ото­бра­зить спи­сок за­даний, но в нем ниче­го нет. Для до­бав­ления за­дания восполь­зу­ем­ся но­вым За­ня­ти­ем – TodoEditor:

@Override public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

final Intent intent = getIntent();

final String action = intent.getAction();

if (Intent.ACTION_INSERT.equals(action)) {

state = STATE_INSERT;

uri = getContentResolver().insert(TodoDatabaseProvider.

CONTENT_URI, null);

if (uri == null) {

Log.e(TAG, “Failed to insert new todo into”

+ TodoDatabaseProvider.CONTENT_URI);

finish();

return;

}

setResult(RESULT_OK, (new Intent()).setAction(uri.toString()));

} else {

Log.e(TAG, “Unrecognised action “ + action + “, exiting”);

finish();

return;

}

cursor = managedQuery(uri, TODO_PROJECTION, null, null, null);

setContentView(R.layout.todo_editor);

text = (EditText) findViewById(R.id.todo);

date = (EditText) findViewById(R.id.duedate);

if (savedInstanceState != null) {

initialTodo = savedInstanceState.getString(INITIAL_TODO);

}

}

Здесь про­из­во­дит­ся вы­зов про­вай­де­ра ба­зы дан­ных и пред­принима­ют­ся дей­ст­вия в со­от­вет­ст­вии с его ре­зуль­та­том. Об­ратите внимание, что в на­ча­ле мы встав­ля­ем стро­ку, а за­тем об­нов­ля­ем ее, когда ре­ша­ем со­хранить эле­мент спи­ска.

Вы­зо­ви­те этот код из Todo.java, до­ба­вив пункт ме­ню New Item [Но­вый эле­мент]. Мы так­же сно­ва до­бав­ля­ем пунк­ты ме­ню с по­мо­щью MenuInflater (см. код на DVD) для реа­ли­за­ции со­хранения, уда­ления и от­ме­ны (Save, Delete и Cancel). При со­хранении вы­зы­ва­ет­ся ме­тод updateTodo():

private final void updateTodo(String task, String duedate) {

ContentValues values = new ContentValues();

if (state == STATE_INSERT) {

values.put(TodoDatabaseProvider.COL_CREATEDATE, System.currentTimeMillis());

values.put(TodoDatabaseProvider.COL_TASK, task);

values.put(TodoDatabaseProvider.COL_DUEDATE, duedate);

}

getContentResolver().update(uri, values, null, null);

}

ContentValues по­лу­ча­ет зна­чения, ко­то­рые мы хо­тим до­ба­вить, а за­тем с по­мо­щью getContentResolver() мы по­лу­ча­ем про­вай­дера ба­зы дан­ных и вы­зы­ва­ем его ме­тод update(). По­сле это­го ак­тив­ным за­ня­ти­ем сно­ва станет ToDo, и мы уви­дим в спи­ске но­вый эле­мент.

У нас поч­ти за­кон­чи­лось ме­сто для статьи. По­ка мы ра­бо­та­ли толь­ко с таб­ли­цей за­дач без ссыл­ки на ка­те­го­рии; что­бы ото­бра­зить ка­те­го­рию для ка­ж­дой за­да­чи, нам нуж­но из­менить ме­тод populateList(), что­бы он по­лу­чал эту ин­фор­ма­цию.

Для это­го сна­ча­ла нуж­но на­пи­сать за­прос join, ко­то­рый ссы­ла­ет­ся на две таб­ли­цы. managedQuery() не под­дер­жи­ва­ет ра­бо­ту с несколь­ки­ми таб­ли­ца­ми, и нам при­дет­ся на­пи­сать спе­ци­аль­ный SQL-за­прос. Все это, а так­же код, ко­то­рый по­на­до­бит­ся вам для уда­ления и из­менения за­пи­сей в ба­зе дан­ных, мож­но най­ти на LXFDVD (или на www.linuxformat.com).

До­машнее за­дание

Это при­ло­жение ра­бо­та­ет пре­крас­но, но оно до­воль­но про­стое. Вот несколь­ко ве­щей, ко­то­рые вы мо­же­те за­хо­теть по­про­бо­вать, что­бы луч­ше осво­ить ра­бо­ту API:

  • Ка­те­го­рия не мо­жет быть пустой, ее нуж­но за­да­вать.
  • Поль­зо­ва­тель не мо­жет ре­дак­ти­ро­вать доступ­ные ка­те­го­рии. Для их об­ра­бот­ки и для кор­рект­ной ра­бо­ты с На­ме­рения­ми для вы­зо­ва кор­рект­ных За­ня­тий вы ско­рее все­го соз­да­ди­те но­вое За­ня­тие (ре­дак­ти­ро­вать спи­сок/ре­дак­ти­ро­вать ка­те­го­рию).
  • По­сле за­вер­шения за­да­чи ее мож­но толь­ко уда­лить. Как на­счет про­став­ления га­лоч­ки?
  • Что­бы да­ты со­хра­ня­лись кор­рект­но, их нуж­но вво­дить в спе­ци­аль­ном фор­ма­те. Вме­сто это­го хо­ро­шо бы по­до­шел ка­лен­дарь или дру­гая фор­ма вво­да.

Взгляните на вид­жет да­ты в Android.

В сле­дую­щей ста­тье этой се­рии мы по­смот­рим, как по­лу­чать ин­фор­ма­цию из уда­лен­ной ба­зы дан­ных и за­пи­сы­вать в нее дан­ные. |

Персональные инструменты
купить
подписаться
Яндекс.Метрика