multiple DAOs and take care of the remaining duties from one place. Such duties will include storing data in multiple tables and dealing with transactions. Our application
components will ultimately use this class to save and retrieve data. This technique, shown in the next listing, will keep all of the SQL and logic out of our application components and views.
The DataManager interface defines all possible operations:
//DataManager interface used to define data operations.
public interface DataManager {
// movie
public Movie getMovie(long movieId);
public List<Movie> getMovieHeaders();
public Movie findMovie(String name);
public long saveMovie(Movie movie);
public boolean deleteMovie(long movieId);
// optional -- used for CursorAdapter
public Cursor getMovieCursor();
// category
public Category getCategory(long categoryId);
public List<Category> getAllCategories();
public Category findCategory(String name);
public long saveCategory(Category category);
public void deleteCategory(Category category);
}
Our application components will use references to this interface to perform data operations.
implement the DataManager Interface:
/**
*DataManagerImpl to encapsulate SQL and DB details.
* Includes SQLiteOpenHelper, and uses Dao objects
* to create/update/delete data.
*/
public class DataManagerImpl implements DataManager {
private Context context;
private SQLiteDatabase db;
private CategoryDao categoryDao;
private MovieDao movieDao;
private MovieCategoryDao movieCategoryDao;
public DataManagerImpl(Context context) {
this.context = context;
//construct OpenHelper
SQLiteOpenHelper openHelper = new OpenHelper(this.context);
//get SQLiteDatabase reference
db = openHelper.getWritableDatabase();
Log.i(Constants.LOG_TAG, "DataManagerImpl created, db open status: " + db.isOpen());
categoryDao = new CategoryDao(db);
movieDao = new MovieDao(db);
movieCategoryDao = new MovieCategoryDao(db);
}
public SQLiteDatabase getDb() {
return db;
}
private void openDb() {
if (!db.isOpen()) {
db = SQLiteDatabase.openDatabase(DataConstants.DATABASE_PATH, null, SQLiteDatabase.OPEN_READWRITE);
// since we pass db into DAO, have to recreate DAO if db is re-opened
categoryDao = new CategoryDao(db);
movieDao = new MovieDao(db);
movieCategoryDao = new MovieCategoryDao(db);
}
}
private void closeDb() {
if (db.isOpen()) {
db.close();
}
}
private void resetDb() {
Log.i(Constants.LOG_TAG, "Resetting database connection (close and re-open).");
closeDb();
SystemClock.sleep(500);
openDb();
}
// we only expose methods app is actually using, and we can combine DAOs, with logic in one place
@Override
//wrap daos to get movie
public Movie getMovie(long movieId) {
// TODO Auto-generated method stub
Movie movie = movieDao.get(movieId);
if (movie != null) {
movie.getCategories().addAll(movieCategoryDao.getCategories(movie.getId()));
}
return movie;
}
@Override
public List<Movie> getMovieHeaders() {
// TODO Auto-generated method stub
return movieDao.getAll();
}
@Override
public Movie findMovie(String name) {
// TODO Auto-generated method stub
Movie movie = movieDao.find(name);
if (movie != null) {
movie.getCategories().addAll(movieCategoryDao.getCategories(movie.getId()));
}
return movie;
}
@Override
//wrap for save movie
public long saveMovie(Movie movie) {
// TODO Auto-generated method stub
// here though, to keep it simpler, we use the DAOs directly
long movieId = 0L;
// put it in a transaction, since we're touching multiple tables
/**
* A transaction ensures that if one part of our operation fails,
* the entire thing will be rolled back.
* This prevents us from ending up with an inconsistent state.
* it may not be appropriate for the DAO itself
* (the manager has the contextual information to know a transaction is needed).
*/
try {
db.beginTransaction();
// first save movie
movieId = movieDao.save(movie);
// second, make sure categories exist, and save movie/category association
// (this makes multiple queries, but usually not many cats, could just save and catch exception too, but that's ugly)
if (movie.getCategories().size() > 0) {
for (Category c : movie.getCategories()) {
long catId = 0L;
Category dbCat = categoryDao.find(c.getName());
if (dbCat == null) {
catId = categoryDao.save(c);
} else {
catId = dbCat.getId();
}
MovieCategoryKey mcKey = new MovieCategoryKey(movieId, catId);
if (!movieCategoryDao.exists(mcKey)) {
movieCategoryDao.save(mcKey);
}
}
}
db.setTransactionSuccessful();
} catch (SQLException e) {
Log.e(Constants.LOG_TAG, "Error saving movie (transaction rolled back)", e);
movieId = 0L;
} finally {
// an "alias" for commit
db.endTransaction();
}
return movieId;
}
@Override
//wrap for delete movie
public boolean deleteMovie(long movieId) {
// TODO Auto-generated method stub
boolean result = false;
// NOTE switch this order around to see constraint error (foreign keys work)
try {
db.beginTransaction();
// make sure to use getMovie and not movieDao directly, categories need to be included
Movie movie = getMovie(movieId);
if (movie != null) {
for (Category c : movie.getCategories()) {
movieCategoryDao.delete(new MovieCategoryKey(movie.getId(), c.getId()));
}
movieDao.delete(movie);
}
db.setTransactionSuccessful();
result = true;
} catch (SQLException e) {
Log.e(Constants.LOG_TAG, "Error deleting movie (transaction rolled back)", e);
} finally {
db.endTransaction();
}
return result;
}
@Override
public Cursor getMovieCursor() {
// TODO Auto-generated method stub
// note that query MUST have a column named _id
return db.rawQuery("select " + MovieColumns._ID + ", " + MovieColumns.NAME + ", " + MovieColumns.THUMB_URL
+ " from " + MovieTable.TABLE_NAME, null);
}
@Override
public Category getCategory(long categoryId) {
// TODO Auto-generated method stub
return categoryDao.get(categoryId);
}
@Override
public List<Category> getAllCategories() {
// TODO Auto-generated method stub
return categoryDao.getAll();
}
@Override
public Category findCategory(String name) {
// TODO Auto-generated method stub
return categoryDao.find(name);
}
@Override
public long saveCategory(Category category) {
// TODO Auto-generated method stub
return categoryDao.save(category);
}
@Override
public void deleteCategory(Category category) {
// TODO Auto-generated method stub
categoryDao.delete(category);
}
}
Using the DataManager:
For instance the MyMovies activity is still the main ListView our application needs. But instead of using an Adapter that’s backed with List<String>, we change to one that uses List<Movie> and we get that data from our database as follows:
private MovieAdapter adapter;
private List<Movie> movies;
...
movies.addAll(app.getDataManager().getMovieHeaders());
adapter.notifyDataSetChanged();
A CursorAdapter is an adapter that can pull data from a database. Sometimes this is convenient, such as when multiple things might modify the underlying database. With a CursorAdapter, a ListView can manage the cursor and automatically update the view items as data is added. One downside with CursorAdapter is that database
concepts such as Cursor leak into the logic of the activity (for example, when a list item is clicked). So usage of a CursorAdapter makes sense sometimes, and other
times it’s easier to do without it. For MyMoviesDatabase we’ve included both approaches in the source code for the project (see the comments in the MyMovies activity to switch between them).