首页 » Android程序设计:第2版 » Android程序设计:第2版全文在线阅读

《Android程序设计:第2版》使用数据库API:MJAndroid

关灯直达底部

在本节中,我们提出了名为MJAndroid的更高级的示例应用,它演示了在一个虚拟的职位搜索应用中小型数据库的使用。本章我们将探讨该程序的数据库持久性方面。在第15章,我们将查看这些应用如何集成映射功能,并在地图上显示职位查询结果。首先,稍详细地解释该应用。

Android和社交网络

Android手机的一个很大的前景是它们运行的应用可以提高用户之间的社交能力。该前景反映了互联网现实——第一代互联网应用是用户访问信息,很多这种应用已经很流行了。互联网应用的第二个浪潮是把用户互相连接起来。如Facebook、Youtube及很多其他这样的应用,以促进有相似爱好的人之间的连接,并支持应用用户提供应用的部分或所有内容。Android有潜力吸纳这个思想,并增加新的维度——移动。人们认为全新一代的应用会为移动设备用户构建:社交网络可以在人们漫步街道时都很容易使用它,可以知道用户的地理位置,可以支持如图片和视频这样的富信息并轻松共享等。MJAndroid给出了Android在这方面不断努力的一个具体例子。

在MJAndroid MicroJobs应用中,有个用户尝试在其地理位置附近找个临时工作,她可以在那里工作几小时,获取一些额外的收入。前提是想要找临时工的雇主输入了空缺职位、描述和时间,并基于Web数据库提供薪酬,这些职位信息可以在Android手机上获取。寻找小时工的人们可以使用MicroJobs应用来访问该数据库,在其附近查找职位,和朋友交流潜在的雇主和职位,如果感兴趣的话,直接给雇主打电话。在这里,我们不想创建在线服务,而只是想要保存一些手机上的封装好的数据。该应用包含很多特征,它们扩展了移动设备独有的核心思想。

映射(mapping)

Android手机环境提供了对动态、交互式地图的支持,我们将充分利用其功能。在P392“MapView和MapActivity”一节中,会看到通过非常少的代码,就能够显示本地社区的动态地图,从内部GPS中获取地理位置更新,在运动时自动滚动地图。我们能够在两个方向放大和缩小滚动地图,甚至是切换到卫星视图。

寻找朋友和事件

在第15章,将看到在地图上存在图形叠加,将显示在该区域哪个地方有空缺职位,并且只需要轻轻触摸地图上的符号就可以获取关于工作的更多信息。我们将访问Android的联系人管理器应用来获取朋友的地址信息(电话号码、即时消息等),以及访问MicroJobs数据库获取更多发布的职位信息。

即时通信

当我们找到想要聊天的朋友时,就可以通过即时消息联系他们。

和朋友或雇主聊天

如果短信方式太慢或太麻烦,则可以很轻松地给朋友打电话,或者给提供职位的雇主打电话。

浏览Web

大部分雇主有个关联的Web站点,其上提供一些详细的信息。能够从列表或地图中选择一名雇主,并快速登录其网站了解一下情况。

这是一个很有趣的应用,可以进一步把它开发成一个功能完善的服务,但是本书的目的是要说明在自己的应用中开发和集成这些强大的功能是多么容易。本书的所有代码都可以下载。虽然要了解本书的内容不一定要下载这些代码查看,但是强烈建议你把这些源代码下载到计算机。这样你就可以很方便参考,而且很容易查看部分代码并粘贴到你的应用中。现在,我们将使用MJAndroid示例,提供“接近现实”的示例,来深入说明Android数据库API。

图9-3显示了当第一次运行MJAndroid时的屏幕显示。它是本地区地图,包含几个按钮。

图9-3:MJAndroid打开屏幕截图

源文件(src)

MJAndroid的包名是com.microjobsinc.mjandroid。Eclipse展开类似的路径结构,正如对于任何Java项目那样,当打开src目录时会显示全部内容。除了这些包文件夹,还有一个文件夹,包含项目的所有Java文件。这些文件包括:

MicroJobs.java

该应用的主要源文件——首先开始启动的活动,显示应用的核心地图,并调用实现用户界面所需要的其他活动或服务。

MicroJobsDatabase.java

它是一个数据库助手,能够帮助轻松访问本地MJAndroid数据库。使用SQLite在这个文件中存储所有的雇主、用户和职位信息。

AddJob.java和EditJob.java

它们是MJAndroid数据库的一部分。提供用户可以用于增加或编辑数据库中的职位信息的界面。

MicroJobsDetail.java

显示关于某个特定职位的所有详细信息的Activity。

MicroJobsEmpDetail.java

显示关于雇主信息的Activity,包括名字、地址、声誉、邮件地址和电话号码等。

MicroJobsList.java

显示职位列表的Activity(和MicroJobs.java中的地图视图不同)。它显示了简单的雇主和职位列表,支持用户通过任意字段对列表进行排序,以及通过触摸列表上的名字打电话咨询关于职位或雇主的具体信息。

载入和启动应用

从SDK运行MJAndroid很复杂,因为应用使用MapView。当使用MapView时,Android需要特殊的地图API密钥,密钥和特定的开发机关联。在P138“Google地图API密钥”一节中,我们已经了解了签名和启动应用的需求,因为该应用依赖于地图API,故需要设置API密钥,示例才能正常工作。要启动MJAndroid,打开和运行本章给出的Eclipse项目,正如你在其他章节所执行的。

数据库查询及从数据库中读取数据

存在很多种方法可以从SQL数据库中读取数据,但是它们都回到基础的操作顺序:

1.创建SQL语句,描述需要检索的数据

2.在数据库上执行该语句

3.把结果SQL数据映射到可以理解的数据结构中

对于对象-关系映射软件,这个过程可能是非常复杂的,或者直接在应用中编写查询,它相对简单但是需要很大的工作量。对象关系映射(ORM,http://en.wikipedia.org/wiki/Object_relational_mapping)工具把数据库编程代码隐藏起来,通过对象映射避免了字段的复杂性。从处理数据库变化角度看,代码可能更健壮,但是需要更复杂的ORM设置和维护。目前,在Android应用中使用ORM不是很普遍。

直接在应用中编写SQL查询只对于非常小型的项目可行,它不会随着时间变化。直接包含数据库代码的应用会增加代码脆弱的风险,因为当数据库模式发生变化时,必须审查或可能重写引用该模式的任何代码。

常见的折中方式是把所有的数据库逻辑转换成一组对象,其唯一目的是把应用请求转化成数据库请求,并把结果返回给应用。在MJAndroid应用中我们采用的就是这个办法:所有数据库代码都包含在MicroJobsDatabase类中,该类继承自SQLiteOpenHelper。但是有了SimpleFinchVideoContentProvider,数据库就变得非常简单,我们不需要使用额外的字符串。

如果不使用内容提供者,Android支持使用定制的游标,可以在定制的游标内隐藏每个特定数据库的操作的所有信息来进一步减少代码依赖性。代码中首先是MicroJobsDatabase的getJob接口调用。该方法的目标是要返回JobsCursor,它包含从数据库中获取的职位。用户可以选择(通过传递给getJobs方法的参数)通过title列或employer_name列对职位进行排序:


public class MicroJobsDatabase extends SQLiteOpenHelper {...    /** Return a sorted JobsCursor     * @param sortBy the sort criteria     */    public JobsCursor getJobs(JobsCursor.SortBy sortBy) {①        String sql = JobsCursor.QUERY + sortBy.toString;②        SQLiteDatabase d = getReadableDatabase;③        JobsCursor c = (JobsCursor) d.rawQueryWithFactory(④            new JobsCursor.Factory,            sql,            null,            null);        c.moveToFirst;⑤        return c;⑥    }...    public static class JobsCursor extends SQLiteCursor{⑦        public static enum SortBy{⑧            title,            employer_name        }        private static final String QUERY =            "SELECT jobs._id, title, employer_name, latitude, longitude, status "+            "FROM jobs, employers "+            "WHERE jobs.employer_id = employers._id "+            "ORDER BY ";        private JobsCursor(SQLiteDatabase db, SQLiteCursorDriver driver,            String editTable, SQLiteQuery query) {⑨            super(db, driver, editTable, query);        }        private static class Factory implements SQLiteDatabase.CursorFactory{⑩            @Override            public Cursor newCursor(SQLiteDatabase db,                    SQLiteCursorDriver driver, String editTable,                    SQLiteQuery query) {⑪                return new JobsCursor(db, driver, editTable, query);⑫            }        }        public long getColJobsId{⑬            return getLong(getColumnIndexOrThrow("jobs._id"));        }        public String getColTitle{            return getString(getColumnIndexOrThrow("title"));        }        public String getColEmployerName{            return getString(getColumnIndexOrThrow("employer_name"));        }        public long getColLatitude{            return getLong(getColumnIndexOrThrow("latitude"));        }        public long getColLongitude{            return getLong(getColumnIndexOrThrow("longitude"));        }        public long getColStatus{            return getLong(getColumnIndexOrThrow("status"));        }    }  

以下是一些重点代码解释:

① 基于用户请求的排序列(sortBy的参数)的查询函数,作为游标返回结果。

② 创建查询字符串。大多数字符串是静态的(QUERY变量类型),但是该行执行列排序。即使QUERY是private类型,封闭类还是可以用它。因为getJobs方法和JobsCursor类都在MicroJobsDatabase类内,这使得JobsCursor的private类型的数据成员在getJobs方法中也可以访问。

要获取sort列的文本,只需要对传递给调用函数的枚举参数运行toString方法。可以定义关联数组,它可以提供对变量进行命名的更大的灵活性,但是其解决方案也更简单。此外,使用IDE的自动补全功能,很容易弹出列的名称。

③ 返回数据库的句柄。

④ 使用SQLiteDatabase对象的rawQueryWithFactory方法创建JobsCursor游标。该方法允许传递一个factory方法,Android会使用该方法创建需要的准确的游标类型。如果使用了更简单的rawQuery方法,则将获取到通用的Cursor,它缺乏JobsCursor的特性。

⑤ 为了便于调用,游标指向结果的第一条记录。这样游标就易于使用。一个常见的错误是忘记执行moveToFirst调用,导致怎么也找不出Cursor对象为何抛出异常。

⑥ 返回值是游标。

⑦ getJobs方法返回创建游标的类。

⑧ 提供可选的排序条件的方式:在枚举enum中保存列的名称。在第②项中使用enum类型。

⑨ 定制的游标的构造函数。最后一个参数是调用时所传递的查询。

⑩ 创建游标的Factory类,嵌入在JobsCursor类中。

 根据调用传递的查询创建的游标。

 返回指向封装的JobsCursor类的游标。

 从游标下面的行抽取特定列的函数。例如,getColTitle返回游标当前引用的记录的title列的值。它把数据库实现和调用代码分离,使得代码便于阅读。

注意:虽然在单个应用中,继承cursor是使用数据库的一种不错的方式,但它不适用于内容提供者API,因为Android不支持cursor子类进程间共享。此外,MJAndroid应用是构造的示例,用来说明如何使用数据库。在第13章,我们给出的应用包含更健壮的架构,你可能会在生产应用中看到这样的架构。

关于数据库的使用范例如下所示(该代码获得游标,按标题排序,通过getJobs执行调用。然后,它对职位进行迭代遍历):


MicroJobsDatabase db = new MicroJobsDatabase(this);①JobsCursor cursor = db.getJobs(JobsCursor.SortBy.title);②for (int rowNum = 0; rowNum < cursor.getCount; rowNum++) {③    cursor.moveToPosition(rowNum);    doSomethingWith(cursor.getColTitle);④}  

以下是一些重点代码解释:

① 创建对象MicroJobsDatabase。参数this表示之前所述的上下文。

② 创建游标JobsCursor,指向前面提到的SortBy枚举对象。

③ 使用通用的Cursor方法对游标进行迭代。

④ 在循环内,调用其中一个JobsCursor提供的自定义accessor方法,通过每条记录的title列执行用户选定的事项。

使用query方法

虽然对于需要执行较复杂的数据库操作的应用,如前所示,把SQL语句分离开是必要的,但它对于包含简单的数据库操作的应用也是很方便的,例如SimpleFinchVideoContentProvider利用SQLiteDatabase.query方法,如下视频相关的示例所示:


videoCursor = mDb.query(VIDEO_TABLE_NAME, projection,    where, whereArgs,    null, null, sortOrder);  

对于前面所示的SQLiteDatabase.rawQueryWithFactory,query方法的返回值是一个Cursor对象。把该游标赋值给前面定义的videoCursor变量。

query方法在给定表上执行SELECT,在这个例子中常量是VIDEO_TABLE_NAME。query方法接受两个参数。首先,query中只应该显示给出名字的列——其他列不应该在游标结果中显示。对于很多应用,该参数可以接受null值,它会导致结果游标中显示所有的列值。然后,where参数包含SQL where语句,而不需要WHERE关键字。Where参数也可以包含多个'?'字符串,它会被whereArgs值所取代。当我们探讨execSQL方法时,将详细探讨这两个值是如何结合起来的。

修改数据库

当想要从数据库中读取数据时,Android的游标是非常有用的,但是类android.database.Cursor并没有提供方法来创建、更新或删除数据。SQLiteDatabase类提供两个基础的API,可以用它们执行读数据和写数据操作:

·Insert、query、update和delete方法

·更常用的execSQL方法,它接受任何单个SQL语句,它不返回数据,并且在数据库上运行

如果第一组操作合适,建议使用第一种方式。我们将向你介绍两种使用MJAndroid操作的方式。

向数据库中插入数据

当想要向SQL数据库插入数据时,就使用SQL INSERT语句。INSERT语句相当于CRUD理念的创建表create操作。

在MJAndroid应用中,当用户查看职位列表时,可以通过单击Add Job菜单项把职位添加到列表中。然后,可以填写表单,输入employer、job title和description信息。当用户单击表单的Add Job按钮后,执行以下代码:


db.addJob(employer.id, txtTitle.getText.toString,    txtDescription.getText.toString);  

该代码调用addJob函数,传递employer ID、job title和job description。addJob函数执行把job写入数据库的实际工作。

以下示例说明了insert方法的使用:


/** * Add a new job to the database. The job will have a status of open. * @param employer_id       The employer offering the job * @param title             The job title * @param description       The job description */public void addJob(long employer_id, String title, String description) {    ContentValues map = new ContentValues;①    map.put("employer_id", employer_id);    map.put("title", title);    map.put("description", description);    try{        getWritableDatabase.insert("jobs", null, map);②    } catch (SQLException e) {        Log.e("Error writing new job", e.toString);    }}  

以下是一些重点代码解释:

① ContentValues对象是列名到列值的映射。在代码内部,是作为HashMap<String,Object>实现的。但是,和简单的HashMap不同,ContentValues是强类型(strongly types)的。可以指定保存在ContentValues容器中的每个值的数据类型。当从ContentValues容器中读取这些值时,ContentValues会自动把值转换成请求的类型。

② insert方法的第二个参数是nullColumnHack。只有当第三个参数map的值是null时,它才使用默认值,这样该记录就完全为空。

使用execSQL方法。该解决方案在更低层次上工作。它创建SQL,把它传递给库来执行。即使可以对每条语句硬编码,包括用户传递的数据,还是建议最好使用bind参数的方式。

bind参数是一个问号,它保存SQL语句的一个字符,通常是用户传递的参数,如WHERE子句中的值。在通过bind参数创建SQL语句后,可以重复使用它,每次执行前设置bind参数的实际值:


/** * Add a new job to the database. The job will have a status of open. * @param employer_id       The employer offering the job * @param title             The job title * @param description       The job description */public void addJob(long employer_id, String title, String description){    String sql =①        "INSERT INTO jobs " +        "(_id, employer_id, title, description, start_time, end_time, status) " +        "VALUES " +        "(NULL, ?,           ?,     ?,           0,          0,       3)";    Object bindArgs = new Object{employer_id, title, description};    try{        getWritableDatabase.execSQL(sql, bindArgs);②    } catch (SQLException e) {        Log.e("Error writing new job", e.toString);    }}  

以下是一些重点代码的解释:

① 构建名为sql的SQL查询模板,它包含可绑定的参数,用于接收用户数据。可绑定的参数是通过字符串中的问号来表示的。下一步,将构建名为bindArgs的对象数组,在SQL模板中,每个元素包含一个对象。在模板中有3个问号,因此在对象数组中应该有3个元素。

② 通过传递SQL模板字符串和绑定到execSQL的参数来执行SQL命令。

使用SQL模板和绑定参数的方式要远远好于在String或StringBuilder中填充参数构建SQL语句。通过使用包含参数的模板,可以规避应用中存在的SQL注入攻击风险。当某公恶意用户输入信息到表单中故意恶意修改数据库时就会发生这些攻击。攻击者通常是通过提前结束当前的SQL命令,使用SQL语法字符,然后直接在表单中添加新的SQL命令来进行攻击。模板-参数方式还可以避免运行时错误,例如参数中不正确的字符。这种方式还使得代码更干净,因为它避免了通过自动替换问号,手工追加字符串出现的长串问题。

更新已经在数据库中的数据

MicroJobs应用支持用户通过单击Jobs列表中的job并选择Edit Job菜单项来编辑job。然后,用户可以修改editJob表单的employer、job title和description字符串。当用户单击表单上的Update按钮时,会执行以下代码行:


db.editJob((long)job_id, employer.id, txtTitle.getText.toString,  txtDescription.getText.toString);  

这块代码调用editJob方法,传递job ID和用户可以改变的3个项:employer ID、job title和job description。editJob方法执行真正的修改数据库中的job工作。

使用update方法。以下示例说明了update方法的使用:


/** * Update a job in the database. * @param job_id        The job id of the existing job * @param employer_id       The employer offering the job * @param title        The job title * @param description       The job description */public void editJob(long job_id, long employer_id, String title, String description){    ContentValues map = new ContentValues;    map.put("employer_id", employer_id);    map.put("title", title);    map.put("description", description);    String whereArgs = new String{Long.toString(job_id)};    try{        getWritableDatabase.update("jobs", map, "_id=?", whereArgs);①    } catch (SQLException e) {        Log.e("Error writing new job", e.toString);    }}  

以下是一些重点代码的解释:

① 要更新的第一个参数是要操作的表的名称。第二个参数是把列名映射到新值。第三个参数是一块SQL代码。在这个例子中,它是包含一个参数的SQL模板。该参数包含一个问号,其通过第四个参数的内容复制。

使用execSQL方法。以下示例说明了execSQL方法的使用:


/** * Update a job in the database. * @param job_id             The job id of the existing job * @param employer_id       The employer offering the job * @param title             The job title * @param description       The job description */public void editJob(long job_id, long employer_id, String title, String description){    String sql =        "UPDATE jobs " +        "SET employer_id = ?, "+        " title = ?, "+        " description = ? "+        "WHERE _id = ? ";    Object bindArgs = new Object{employer_id, title, description, job_id};    try{        getWritableDatabase.execSQL(sql, bindArgs);    } catch (SQLException e) {        Log.e("Error writing new job", e.toString);    }}  

对于这个示例应用,显示的是最简单的可能功能。这样在书中可以便于理解,但是对于真实的应用是不够的。在真实的应用中,需要检查输入字符串中的无效字符,在尝试更新job之前校验job存在与否,在使用employer_id之前校验其值有效与否,更好地捕捉错误,等等。还可能对多人共享的应用,进行用户身份验证。

删除数据库中的数据

MicroJobs应用不但支持用户删除job,而且支持用户创建和修改job。从主应用界面看,用户可以单击List Jobs按钮,获得职位列表,然后单击某个特定job来查看该job详细信息。在这个阶段,用户可以单击Delete this job菜单按钮删除job。应用会询问确定用户是否真的想要删除job。当用户单击Delete按钮时,会执行MicroJobsDetail.java文件中的以下代码块:


db.deleteJob(job_id);  

这行代码调用MicroJobsDatabase类的deleteJob方法,传递要删除的job ID给该方法。这块代码和我们之前看到的函数类似,同样缺乏现实功能。

使用delete方法。以下代码说明了delete方法的使用:


/** * Delete a job from the database. * @param job_id       The job id of the job to delete */public void deleteJob(long job_id) {    String whereArgs = new String{Long.toString(job_id)};    try{        getWritableDatabase.delete("jobs", "_id=?", whereArgs);    } catch (SQLException e) {        Log.e("Error deleteing job", e.toString);    }}  

使用execSQL方法。以下示例说明了execSQL方法的使用:


/** * Delete a job from the database. * @param job_id       The job id of the job to delete */public void deleteJob(long job_id) {    String sql = String.format(            "DELETE FROM jobs " +            "WHERE _id = '%d' ",            job_id);    try{        getWritableDatabase.execSQL(sql);    } catch (SQLException e) {        Log.e("Error deleteing job", e.toString);    }}