Because our DAOs are each unaware of any table other than their own, we’re also going to create a DataManager interface, an implementation class that will wrap the
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).