Пример создания собственного ContentProvider для работы с SQLite БД — часть 2

Часть 1
Часть 3

В предыдущей части я рассказывала что такое Contract Class и как его создать.
Сегодня мы разберем из чего состоит ContentProvider, а также один из вариантов написания данного класса.

Как вы помните, обращаться к БД мы будем с помощью Content URI, которая несет в себе информацию о конкретном ContentProvider и о необходимой нам таблице. Вот один из примеров Content URI:
content://org.nerdgrl.examples.contentproviderexample.provider.ContractClass/students
Здесь мы видим схему данных (content://), уникальный идентификатор нашего провайдера — authority (org.nerdgrl.examples.contentproviderexample.provider.ContractClass), и имя таблицы (students).

В Android SDK есть класс UriMatcher, который хранит соответствие между Uri и неким заданным значение integer. Это значение можно использовать в операторе switch, чтобы описать поведение для каждого Content URI.

Для начала зададим четыре константы, соответствующие четырем возможным типам запроса к нашей БД:

private static final int STUDENTS = 1;
private static final int STUDENTS_ID = 2;
private static final int CLASSES = 3;
private static final int CLASSES_ID = 4;

Затем объявим переменную класса UriMatcher

private static final UriMatcher sUriMatcher;

И в static блоке подготовим ее к использованию:

static {
	sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
	sUriMatcher.addURI(ContractClass.AUTHORITY, "students", STUDENTS);
	sUriMatcher.addURI(ContractClass.AUTHORITY, "students/#", STUDENTS_ID);
	sUriMatcher.addURI(ContractClass.AUTHORITY, "classes", CLASSES);
	sUriMatcher.addURI(ContractClass.AUTHORITY, "classes/#", CLASSES_ID);
}

Также нам необходимо задать проекции для выборки столбцов в запросе, они пригодятся нам в методе query():

private static HashMap sStudentsProjectionMap;
private static HashMap sClassesProjectionMap;

Для проекции по умолчанию мы возьмем весь список столбцов:

static {
	for(int i=0; i < ContractClass.Students.DEFAULT_PROJECTION.length; i++) {
		sStudentsProjectionMap.put(
			ContractClass.Students.DEFAULT_PROJECTION[i],
			ContractClass.Students.DEFAULT_PROJECTION[i]);
	}
	sClassesProjectionMap = new HashMap();
	for(int i=0; i < ContractClass.Classes.DEFAULT_PROJECTION.length; i++) {
		sClassesProjectionMap.put(
			ContractClass.Classes.DEFAULT_PROJECTION[i],
			ContractClass.Classes.DEFAULT_PROJECTION[i]);
	}
}

Теперь определим класс DBHelper, с помощью которого мы будем создавать базу данных и обращаться к ней в методах ContentProvider'a. Здесь мы зададим имя БД, имена таблиц, столбцов, а также запросы для создания таблиц и процедуры создания-обновления БД:

private static class DatabaseHelper extends SQLiteOpenHelper {
	private static final String DATABASE_NAME = "ContractClassDB";
	
	public static final String DATABASE_TABLE_STUDENTS = ContractClass.Students.TABLE_NAME;
	public static final String DATABASE_TABLE_CLASSES = ContractClass.Classes.TABLE_NAME;
	
	public static final String KEY_ROWID  = "_id";
	public static final String KEY_FIRST_NAME   = "first_name";
	public static final String KEY_SECOND_NAME   = "second_name";
	public static final String KEY_CLASS_LETTER   = "class_letter";
	public static final String KEY_FK_CLASS_ID   = "fk_class_id";
	public static final String KEY_AVERAGE_SCORE   = "average_score";
	public static final String KEY_CLASS_NUMBER   = "class_number";
	
	private static final String DATABASE_CREATE_TABLE_STUDENTS =
		"create table "+ DATABASE_TABLE_STUDENTS + " ("
			+ KEY_ROWID + " integer primary key autoincrement, "
			+ KEY_FIRST_NAME + " string default '', "
			+ KEY_SECOND_NAME + " string default '', "
			+ KEY_AVERAGE_SCORE + " real default 0, "
			+ KEY_FK_CLASS_ID + " integer, "
			+" foreign key ("+KEY_FK_CLASS_ID+") references "+DATABASE_TABLE_CLASSES+"("+KEY_ROWID+"));";
			
	private static final String DATABASE_CREATE_TABLE_CLASSES =
		"create table "+ DATABASE_TABLE_CLASSES + " ("
			+ KEY_ROWID + " integer primary key autoincrement, "
			+ KEY_CLASS_NUMBER + " string default '', "
			+ KEY_CLASS_LETTER + " string default '' );";
			
	private Context ctx;
	DatabaseHelper(Context context) {
		super(context, DATABASE_NAME, null, DATABASE_VERSION);
		ctx = context;
	}
	@Override
	public void onCreate(SQLiteDatabase db) {
		db.execSQL(DATABASE_CREATE_TABLE_STUDENTS);
		db.execSQL(DATABASE_CREATE_TABLE_CLASSES);
	}
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		
		db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE_STUDENTS);
		db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE_CLASSES);
		onCreate(db);
	}
}

Теперь подробнее о методах самого ContentProvider'a:

onCreate() - инициализирует ContentProvider. Провайдер будет создан как только вы обратитесь к нему с помощью ContentResolver'a
query() - извлекает данные из БД, и возвращает их в виде Cursor
insert() - добавляет новые данные в БД, возвращает uri новой записи
update() - обновляет строки в БД согласно заданным условиям
delete() - удаляет данные
getType() - возвращает MIME-тип для заданной content URI

Следует помнить, что все перечисленные методы кроме onCreate() могут выполняться одновременно в нескольких потоках, и поэтому должны быть потоко-безопасными (thread-safe).

В методе onCreate() создадим наш DBHelper:

@Override
public boolean onCreate() {
	dbHelper = new DatabaseHelper(getContext());
	return true;
}

В реализации метода getType() мы просто будем возвращать тип данных из нашего ContractClass'a:

@Override
public String getType(Uri uri) {
	switch (sUriMatcher.match(uri)) {
		case STUDENTS:
			return ContractClass.Students.CONTENT_TYPE;
		case STUDENTS_ID:
			return ContractClass.Students.CONTENT_ITEM_TYPE;
		case CLASSES:
			return ContractClass.Classes.CONTENT_TYPE;
		case CLASSES_ID:
			return ContractClass.Classes.CONTENT_ITEM_TYPE;
		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
	}
}

Рассмотрим метод insert().

Добавлять строки можно только в таблицы, поэтому сделаем фильтр для тех Content Uri, которые не подходят:

if (sUriMatcher.match(uri) != STUDENTS &&
			sUriMatcher.match(uri) != CLASSES) {
	throw new IllegalArgumentException("Unknown URI " + uri);
}

Далее получим экземпляр БД и создадим структуру для хранения данных новой строки таблицы:

SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values;
if (initialValues != null) {
	values = new ContentValues(initialValues);
}
else {
	values = new ContentValues();
}

И определим в какую таблицу нужно добавить новые данные:

switch (sUriMatcher.match(uri)) {
	case STUDENTS:
		rowId = db.insert(ContractClass.Students.TABLE_NAME,
			ContractClass.Students.COLUMN_NAME_FIRST_NAME,
			values);
		if (rowId > 0) {
			rowUri = ContentUris.withAppendedId(ContractClass.Students.CONTENT_ID_URI_BASE, rowId);
			getContext().getContentResolver().notifyChange(rowUri, null);
		}
	break;
	case CLASSES:
		...
	break;
}

Здесь хочу обратить внимание на строку:

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

Именно она отвечает за обновление данных в CursorAdapter(и, соответственно, в нашем ListView, где мы его будем использовать).

Полный код метода:

@Override
public Uri insert(Uri uri, ContentValues initialValues) {
	if (
		sUriMatcher.match(uri) != STUDENTS &&
		sUriMatcher.match(uri) != CLASSES
	) {
		throw new IllegalArgumentException("Unknown URI " + uri);
	}
	SQLiteDatabase db = dbHelper.getWritableDatabase();
	ContentValues values;
	if (initialValues != null) {
		values = new ContentValues(initialValues);
	}
	else {
		values = new ContentValues();
	}
	long rowId = -1;
	Uri rowUri = Uri.EMPTY;
	switch (sUriMatcher.match(uri)) {
		case STUDENTS:
			rowId = db.insert(ContractClass.Students.TABLE_NAME,
				ContractClass.Students.COLUMN_NAME_FIRST_NAME,
				values);
			if (rowId > 0) {
				rowUri = ContentUris.withAppendedId(ContractClass.Students.CONTENT_ID_URI_BASE, rowId);
				getContext().getContentResolver().notifyChange(rowUri, null);
			}
		break;
		case CLASSES:
			rowId = db.insert(ContractClass.Classes.TABLE_NAME,
				ContractClass.Classes.COLUMN_NAME_CLASS_NUMBER,
				values);
			if (rowId > 0) {
				rowUri = ContentUris.withAppendedId(ContractClass.Classes.CONTENT_ID_URI_BASE, rowId);
				getContext().getContentResolver().notifyChange(rowUri, null);
			}
		break;
	}
	return rowUri;
}

Теперь разберем метод query():

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
	SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
	String orderBy = null;
	switch (sUriMatcher.match(uri)) {
		case STUDENTS:
			qb.setTables(ContractClass.Students.TABLE_NAME);
			qb.setProjectionMap(sStudentsProjectionMap);
			orderBy = ContractClass.Students.DEFAULT_SORT_ORDER;
			break;
		case STUDENTS_ID:
			qb.setTables(ContractClass.Students.TABLE_NAME);
			qb.setProjectionMap(sStudentsProjectionMap);
			qb.appendWhere(ContractClass.Students._ID + "=" + uri.getPathSegments().get(ContractClass.Students.STUDENTS_ID_PATH_POSITION));
			orderBy = ContractClass.Students.DEFAULT_SORT_ORDER;
			break;
		case CLASSES:
			qb.setTables(ContractClass.Classes.TABLE_NAME);
			qb.setProjectionMap(sClassesProjectionMap);
			orderBy = ContractClass.Classes.DEFAULT_SORT_ORDER;
			break;
		case CLASSES_ID:
			qb.setTables(ContractClass.Classes.TABLE_NAME);
			qb.setProjectionMap(sClassesProjectionMap);
			qb.appendWhere(ContractClass.Classes._ID + "=" + uri.getPathSegments().get(ContractClass.Classes.CLASSES_ID_PATH_POSITION));
			orderBy = ContractClass.Classes.DEFAULT_SORT_ORDER;
			break;
		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
	}
	SQLiteDatabase db = dbHelper.getReadableDatabase();
	Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
	c.setNotificationUri(getContext().getContentResolver(), uri);
	return c;
}

Здесь используется объект класса SQLiteQueryBuilder для построения запроса. Методы setTables() и setProjectionMap() задают таблицу и набор столбцов для выборки. Для запроса к определенной строке используется appendWhere():

qb.appendWhere(ContractClass.Classes._ID + "=" + uri.getPathSegments().get(ContractClass.Classes.CLASSES_ID_PATH_POSITION));

который добавляет условие WHERE к запросу. Как вы могли заметить, здесь мы как раз используем ContractClass.Classes.CLASSES_ID_PATH_POSITION (в данном случае для таблицы Classes) - таким образом мы определяем, что номер требуемой строки идет на первой позиции Content Uri сразу после имени таблицы (content://<authority>/classes/1)

Метод update(), по своей структуре, аналогичен предыдущему:

@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
	SQLiteDatabase db = dbHelper.getWritableDatabase();
	int count;
	String finalWhere;
	String id;
	switch (sUriMatcher.match(uri)) {
		case STUDENTS:
			count = db.update(ContractClass.Students.TABLE_NAME, values, where, whereArgs);
			break;
		case STUDENTS_ID:
			id = uri.getPathSegments().get(ContractClass.Students.STUDENTS_ID_PATH_POSITION);
			finalWhere = ContractClass.Students._ID + " = " + id;
			if (where !=null) {
				finalWhere = finalWhere + " AND " + where;
			}
			count = db.update(ContractClass.Students.TABLE_NAME, values, finalWhere, whereArgs);
			break;
		case CLASSES:
			count = db.update(ContractClass.Classes.TABLE_NAME, values, where, whereArgs);
			break;
		case CLASSES_ID:
			id = uri.getPathSegments().get(ContractClass.Classes.CLASSES_ID_PATH_POSITION);
			finalWhere = ContractClass.Classes._ID + " = " + id;
			if (where !=null) {
				finalWhere = finalWhere + " AND " + where;
			}
			count = db.update(ContractClass.Classes.TABLE_NAME, values, finalWhere, whereArgs);
			break;
		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
	}
	getContext().getContentResolver().notifyChange(uri, null);
	return count;
}

Обратите внимание на следующую часть метода:

finalWhere = ContractClass.Students._ID + " = " + id;
if (where !=null) {
	finalWhere = finalWhere + " AND " + where;
}

Здесь мы добавляем к уже имеющемуся условию запроса, условие равенства определенному id записи.

Код метода delete():

@Override
public int delete(Uri uri, String where, String[] whereArgs) {
	SQLiteDatabase db = dbHelper.getWritableDatabase();
	String finalWhere;
	int count;
	switch (sUriMatcher.match(uri)) {
		case STUDENTS:
			count = db.delete(ContractClass.Students.TABLE_NAME,where,whereArgs);
			break;
		case STUDENTS_ID:
			finalWhere = ContractClass.Students._ID + " = " + uri.getPathSegments().get(ContractClass.Students.STUDENTS_ID_PATH_POSITION);
			if (where != null) {
				finalWhere = finalWhere + " AND " + where;
			}
			count = db.delete(ContractClass.Students.TABLE_NAME,finalWhere,whereArgs);
			break;
		case CLASSES:
			count = db.delete(ContractClass.Classes.TABLE_NAME,where,whereArgs);
			break;
		case CLASSES_ID:
			finalWhere = ContractClass.Classes._ID + " = " + uri.getPathSegments().get(ContractClass.Classes.CLASSES_ID_PATH_POSITION);
			if (where != null) {
				finalWhere = finalWhere + " AND " + where;
			}
			count = db.delete(ContractClass.Classes.TABLE_NAME,finalWhere,whereArgs);
			break;
	default:
		throw new IllegalArgumentException("Unknown URI " + uri);
	}
	getContext().getContentResolver().notifyChange(uri, null);
	return count;
}

В следующей части я расскажу как использовать ContentProvider в связке с CursorLoader. Полный исходный код можно найти здесь.

10 комментариев to “Пример создания собственного ContentProvider для работы с SQLite БД — часть 2”

  1. Евгений:

    Отличная статья. Обязательно добавлю в вк. Только вот вопрос, поясните пожалуйста эти строчки:
    Зачем проверять values на наличие данных для конкретного столбца и при их отсутствии добавлять пустые значение, когда можно в БД определить для этих столбцов дефолтные условия? Или же пустые значения все равно необходимы? Просто такое количество кода на ровном месте плодить не хотелось бы ( а если столбцов будет 20? ) Только начал изучать contentProvider, прочитал статью и уже почти все понял.

    if (values.containsKey(ContractClass.Students.COLUMN_NAME_FIRST_NAME) == false) {
    values.put(ContractClass.Students.COLUMN_NAME_FIRST_NAME, «»);
    }
    if (values.containsKey(ContractClass.Students.COLUMN_NAME_SECOND_NAME) == false) {
    values.put(ContractClass.Students.COLUMN_NAME_SECOND_NAME, «»);
    }
    if (values.containsKey(ContractClass.Students.COLUMN_NAME_AVERAGE_SCORE) == false) {
    values.put(ContractClass.Students.COLUMN_NAME_AVERAGE_SCORE, 0.0);
    }
    if (values.containsKey(ContractClass.Students.COLUMN_NAME_FK_CLASS_ID) == false) {
    values.put(ContractClass.Students.COLUMN_NAME_FK_CLASS_ID, -1)

  2. Евгений:

    И еще один момент. При получении доступа на запись к БД в методе insert() почему не ловите исключение? Можем получить исключение из-за неудачи получения доступа на запись и приложение вылетит.

    • NerdGrl:

      Хм, а какое конкретно исключение? В документации не видела, и в жизни ни разу не встречала.

      • Евгений:

        При попытке получить доступ на запись к базе данных, может возникнуть исключение SQLiteException когда базу данных невозможно открыть для записи, например, если запись невозможна из-за нехватки памяти.

  3. Михаил:

    NerdGrl, спасибо за ваш труд. Очень помогла статья!

  4. Ваш комментарий ожидает одобрения. Это его предварительный просмотр, комментарий станет видимым для всех после одобрения.

    https://7filtrov

    Насосы для колодцев и скважин: виды и правильный выбор.
    Насосы для колодцев и скважин являются одним из главных элементов системы водоснабжения любого частного дома. Их характеристики должны позволять бесперебойно обеспечивать водой дом, а при необходимости — и полив приусадебного участка, наполнение бассейна и т.д. О их типах, отличиях и их выборе для водоснабжения дома Вы можете узнать из этой статьи.
    Содержание статьи:
    Критерии выбора насосов.
    При выборе насоса для колодца или скважины в первую очередь необходимо учесть расстояние до уровня воды в них. Если оно небольшое – не более 8-9 метров, то можно выбрать поверхностный самовсасывающий насос или глубинный (погружной). Если же до уровня воды более 8-9 м, то практически вариант один — глубинный (погружной) .
    Выбрав тот или иной вид необходимо определиться с его основными характеристиками: высотой напора и производительностью. По этим характеристикам насос подбирают в зависимости от требуемого давления в системе водоснабжения дома и необходимого расхода воды (необходимая минимальная подача).
    Кроме этого, необходимо учесть расстояние от дома до колодца или скважины и гидравлическое сопротивление в трубах, узлах и запорной арматуре (вентилях, кранах и т.д.). Для компенсации гидравлических потерь необходимо выбирать насос с запасом мощности (не менее 10%).
    Типы насосов для колодцев или скважин.
    Можна выделить два основных вида, в зависимости от их расположения по отношению к воде:
    поверхностные; глубинные или погружные.
    Поверхностные устанавливаются возле источника воды или в помещении, а в воду опускается только всасывающая труба с обратным клапаном на конце. В большинстве случаев, для пуска такого насоса всасывающая труба и сам он должны предварительно заполняться водой.
    центробежными (вихревыми) без эжектора, одно- или многоступенчатыми; самовсасывающими, со встроенным или вынесенным эжектором.
    Поверхностные центробежные насосы без эжектора могут всасывать воду из колодца или скважины с глубины не более, чем 7-8 м. Перед их пуском обязательно необходимо заполнить всасывающую трубу водой, так как если в трубе будет воздух, он самостоятельно ее закачать не сможет. Поэтому, при использовании таких поверхностных насосов на всасывающей трубе, опускаемой в воду, обязательно необходимо установить обратный клапан, чтобы вода из системы при неработающем насосе не уходила обратно. Если клапан будет держать нормально, то всасывающая труба будет постоянно заполнена водой и при последующих пусках её уже не надо будет заливать. Кроме этого, во всасывающую трубу во время работы не должен попадать воздух.
    Одноступенчатые центробежные модели отличаются простотой конструкции, надежностью и невысокой ценой. Но они отличаются и довольно высоким уровнем шума при работе. Многоступенчатые агрегаты такого типа отличаются более низким уровнем шума и меньшим энергопотреблением, при тех же параметрах подачи и напора что и одноступенчатые. Такие п оверхностные насосы для колодцев или скважин без эжектора осуществляют подачу воды благодаря специальной многоступенчатой конструкции гидравлической части. Они практически бесшумны, имеют больший КПД, что является их преимуществом .
    Самовсасывающие поверхностные насосы могут быть со встроенным или вынесенным эжектором, который позволяет им подымать воду с глубины большей за 8 м. Встроенный эжектор позволяет всасывать воду, даже если в трубе есть воздух. Такие насосы обеспечивают подъем воды из скважины или колодца за счет разряжения. Их недостатком является то, что они дороже обычных центробежных (вихревых) агрегатов, а также то, что у них относительно высокий уровень шума и поэтому их целесообразно устанавливать в специальном помещении вне дома.
    Модели с вынесенным эжектором практически бесшумны, так как эжектор располагается в колодце или скважине. Они могут поднимать воду с глубины до 45 м, но они более сложны в монтаже и капризны в работе. К тому же, они очень чувствительны к механическим примесям в воде (песок, ил)
    Одной из важных характеристик при выборе насоса любого вида — это его номинальная подача (м 3 /час) и напор. Большинство из поверхностных насосов для водоснабжения частного дома имеют подачу в пределах 4 — 8 м 3 /час и максимальный напор до 55 м.
    Как правило, именно поверхностные насосы входят в стандартный комплект насосной установки для дома — «безбашенки».
    Погружные (глубинные)
    Как уже говорилось, когда уровень воды в скважине или колодце ниже 8-9 метров, то для подачи воды в систему водоснабжения дома необходимо выбирать только погружной (глубинный) насос. Хотя его вполне можно использовать и при меньшем уровне. Для подачи воды он полностью погружается в воду. Забор воды может осуществляться сверху или снизу.
    При сезонных понижениях уровня воды необходимо следить, чтобы погружной (глубинный) насос всегда был в воде, иначе он выйдет из строя и придётся покупать новый. Этого недостатка лишены модели погружных насосов со специальными «поплавками» — поплавковыми выключателями, которые отключают их при критическом снижении уровня воды и предотвращают тем самым выход из строя.
    Установка и монтаж глубинного насоса.
    Глубинный (погружной) насос с помощью переходников и хомутов подсоединяется к трубе, подающей воду непосредственно в дом или к «безбашенке». Выше насоса обязательно устанавливают обратный клапан того же внутреннего размера, что и труба или большего — для уменьшения гидравлического сопротивления. На корпусе обратного клапана есть стрелка направления потока воды, чтобы не ошибиться при его установке — стрелка должна быть направлена вверх — по направлению движения воды. Если обратный клапан не установить, то при выключении насоса вода из системы будет уходить обратно в колодец или скважину, так как клапаны самих насосов не всегда надежно удерживают воду в системе.
    Обычно сейчас для подачи воды из скважин или колодцев используют различные пластиковые жёсткие или гибкие армированные трубы . Для водоснабжения дома необходимо использовать трубы из так называемого пищевого пластика и рассчитанные на максимальное давление, которое будет в системе водоснабжения дома с достаточным запасом прочности. Кроме этого, обычно, они требуют стабильного напряжение в электросети ( + — 5%). Как правило, обязательно необходимо устанавливать обратный клапан и защиту от сухого хода (поплавковая система, датчики уровня или другие защитные устройства).Почему не работает глубинный насос?
    Какой насос выбрать для водоснабжения дома?
    Сейчас в продаже имеется большое разнообразие насосов для скважин и колодцев разных производителей (как зарубежных — Pedrollo, Grundfos, Calpeda, Wilo, так и отечественных) под самые различные условия эксплуатации и требования. В последнее время много на рынке и китайской продукции. Поэтому без труда можно подобрать оптимальный вариант в соответствии с необходимыми характеристиками, качеством и приемлемой ценой. При этом лучше всего ориентироваться на известного надежного производителя, но такие модели, как правило, дороже или можно посоветоваться со знакомыми, с теми кто уже имеет опыт эксплуатации тех или иных насосов подешевле.

    Источник —
    vodotok насосы
    купить насос для пруда
    Также, рекомендую —
    насос vodotok бцпэ гв и
    насос для пруда на даче

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Translate »