八、探索外部数据库

在上一章中,我们介绍了从仅存在于安卓客户端的完全本地化的数据库,转向利用外部数据库的概念,该数据库可以在整个开发过程中以多种方式帮助我们。

我们看到了如何通过使用外部数据库,我们能够提高我们的安卓应用中的内存使用(也就是说,不必存储非常大的数据库文件),而不会因为使用缓存而牺牲太多性能。此外,我们看到了使用外部数据库如何允许我们备份用户数据(以防用户切换电话或卸载您的应用),防止用户看到过时的数据(因为所有数据都存在于一个中心位置),以及潜在地看到其他用户的数据(请记住全局高分示例)。

使用您的应用可以通过网络与之通信的外部数据库将使您成为一个更加通用的应用开发人员,并为您提供创建完全可扩展的以数据为中心的应用的工具。

不同的外部数据库

那么到底有哪些外部数据库呢?就像安卓、iOS、掌上电脑等都是允许您开发移动应用的操作系统一样,有几个易于访问的平台允许您托管和开发外部数据库。

一个这样的“平台”只是建立一个具有数据库功能的传统专用服务器。例如,一个流行的例子是有一台专用计算机托管一台连接到 T2 MySQL 数据库的 Apache Tomcat T1 服务器。我不会详细介绍如何建立这种服务器-数据库连接(主要是因为您可以通过多种方式来实现),而是让我们先考虑一些高级概念,然后再来看一个简单的利弊列表。

在高层次上,Apache Tomcat 服务器充当处理所有传入和传出 HTTP 请求(即网络请求)的中介。服务器本身监听所有这些传入的请求,并在收到一个请求时,有代码告诉它如何处理请求,以及随后作为响应返回什么。处理请求并返回响应的代码通常被称为 HTTP servlet,,在接下来的章节中,我们将实际实现其中的几个 servlet,让您更好地了解它们是如何工作的。

接下来,Apache Tomcat 服务器还通过 Java 数据库连接驱动程序( JDBC )连接到 MySQL 数据库。一旦配置好,这将允许我们处理传入的 HTTP 请求,然后告诉服务器向 MySQL 数据库发出查询。一旦 MySQL 数据库检索到查询,它将执行它并返回所需的数据,最终被发送回原始请求者。

使用这种模型,优点是它是完全可定制的,并且您可以完全控制每个部分的实现方式。然而,这可能是一把双刃剑,也可能是好事或坏事,这取决于谁在处理服务器和数据库。将重点放在数据库部分,因为它是完全可定制的,所以我们可以完全控制我们想要使用什么样的数据库管理系统(DBMS) ,此外,我们的数据库模式对于我们给定的数据库管理系统应该是什么样子。在整个应用开发过程中,如果我们觉得有必要,我们甚至可以选择切换我们的数据库管理系统或更改我们的模式——例如,如果我们需要一个更具可伸缩性的数据库管理系统。

这就是问题所在。虽然 MySQL 是迄今为止世界上使用最多的数据库管理系统,并且在大多数情况下都做得很好,但它并不是为了具有极高的可扩展性而设计的。因此,对于大型、数据量大的应用,使用 MySQL 将是一个次优的设计决策。回到我最初的观点,即使用完全可定制的服务器和数据库可能是一把双刃剑,人们可以很容易地看到在这种情况下灵活性和责任是如何齐头并进的。随着我们在系统的设计/实现方面获得更多的灵活性,我们同时在做出智能设计决策时也有了更多的责任——否则,我们的应用性能可能会迅速恶化(也就是说,想象一下,如果谷歌的所有数据都托管在一台计算机上——这是一场多么可怕的噩梦)。

其他缺点是,这些系统最初通常需要更高的成本,因为我们实际上需要购买计算机/服务器。此外,由于这些计算机/服务器容易出现故障,我们必须定期管理它们,以确保它们不会崩溃。由于其灵活性,许多公司和初创公司选择了这种模式,尽管许多公司最终雇佣了专门维护这些服务器的专家以及专门构建这些服务器和数据库的后端开发人员。

然而,最近云计算的想法变得越来越流行,在这里我将介绍两个这样的平台。首先是亚马逊的 Web Services (AWS) ,它提供了一套云计算服务,但具体来说亚马逊的弹性计算云(EC2)亚马逊的关系数据库服务(RDS) 。两者之间的主要区别在于,亚马逊的 EC2 被设计成一个全功能和全虚拟的计算环境,允许您控制任意多的服务器/数据库实例(从而使其具有固有的可扩展性)。另一方面,亚马逊的 RDS 被设计成只作为一个云数据库,尽管该服务包含的功能可以让你选择扩展你的计算和存储能力。因此,根据您的应用,您可以选择最合适的服务。亚马逊的计算服务现在被许多人使用,包括像 Yelp、Reddit、Quora、FourSquare、Hootsuite 等知名初创公司,在设计任何未来后端时,这绝对是要记住的。

另一项云计算服务是谷歌的应用引擎(GAE) ,我们将对此进行更深入的研究。AWS 和 GAE 都很容易设置(相对于传统的服务器方法),而 GAE 的用户友好性稍高。然而,我们要看 GAE 而不是 AWS 的主要原因(除了这是一本谷歌主题的书!)是,GAE 允许您免费运行小规模应用(达到某些预定义的限制),而 AWS 只允许您在一年内访问其免费定价层。这样,当我们在后面的部分中查看更多代码时,每个人都会理解。

最后,传统的服务器/数据库模式和新的云计算模式的区别在于,我们实际上不需要拥有和管理专用的服务器!这些云计算服务使我们能够基本上“出租”亚马逊/谷歌各种数据中心内的服务器空间,并使我们能够快速/廉价地创建可靠、可扩展的应用。然而,我们放弃的是实现中的一些控制和灵活性,我将在下一节讨论谷歌应用引擎的 Java 数据对象(JDO) 数据库时讨论这一点。

谷歌应用引擎和 JDO 数据库

那么谷歌应用引擎到底是什么,我们为什么需要它?嗯,GAE 是一个平台,使你能够在谷歌应用的相同系统上构建和托管网络应用。GAE 允许我们快速开发和部署应用,而不必担心可靠性、可扩展性、硬件、补丁或备份等问题。然而,这种可靠性和可伸缩性是有代价的,这个代价就是我们选择数据库管理系统和设计数据库模式的灵活性。事实上,当你选择使用 GAE 作为你的后端时,这两者或多或少都是为你选择的!

GAE 有一个 JDO 数据库——这意味着它有一个特殊的数据库,允许你直接将 Java 对象转换成称为实体的数据行(因此得名)。这个 JDO 数据库是建立在一个叫做 BigTable 的特殊网络数据库之上的,BigTable 的设计是非常快速和可扩展的,实际上并不是像 MySQL 那样的关系型 DBMS(见http://en.wikipedia.org/wiki/BigTable))。这主要意味着我们在第 3 章SQLite 查询中学习的关于 SQL(即 JOINS)的所有功能在这里都不适用,因此您关于数据库模式外观的设计决策有些受限。

有鉴于此,谷歌在为您提供名为 GQL、的 SQL 变体方面做得很好,这是一种用于从应用引擎可扩展数据存储中检索实体的查询语言。还是那句话,虽然有一些不同,但是 GQL 的总体感觉和 SQL 很像:有带 WHERE过滤器的 SELECT语句和其他熟悉的子句,比如 ORDER BYLIMIT。这样,对于那些只熟悉像 MySQL 这样的关系系统的人来说,理解起来应该不会太难。

为了完整起见,其他差异包括不需要构建索引就能够在多个条件下进行筛选,不能够在同一查询中的多个列上使用不等式筛选器,以及不能够筛选缺少字段的行等。所有这些看似随意的差异的原因都与 BigTable 数据库的架构有关。由于它的设计方式以及它对插入的每一行进行索引的方式,某些在关系数据库(如 MySQL)中可用的查询将不再适用于 BigTable。然而,由于这种架构,BigTable 本质上是可扩展的,因此在两者之间进行选择时,请记住这些权衡。

无论如何,文字只能带你走这么远,一旦我们开始看到一些实际的代码,所有这些差异和相似之处就会变得更加清晰。因此,除了安装安卓软件开发工具包之外,我邀请您花一些时间使用以下网址作为指南来设置谷歌应用引擎:

http://code . Google . com/appengine/downloads . html # Download _ the _ Google _ App _ Engine _ SDK

此时,我们已经准备好深入研究一些代码,并尝试为我们的安卓应用拼凑一个功能齐全的谷歌应用引擎后端!

GAE:电子游戏的典范

在接下来的几章中,我们将通过一个例子,我们希望创建一个应用,让我们看到什么视频游戏可以通过 Blockbuster 获得。这将最终涉及到所有的事情,从编写一个刮刀来从 Blockbuster 的网站获取和检索那些视频游戏,将这些游戏对象存储到我们的 GAE 数据库,编写 servlets 来通过 HTTP 请求从我们的 GAE 数据库中获取/移除游戏对象,最后但同样重要的是,用一些安卓客户端的代码来完成它。

在这一章中,我们将着重于设置我们的数据库和编写包装器方法,以帮助我们存储、检索、更新和删除数据,供以后的步骤使用。首先,每个 GAE 应用都需要首先定义一个基本的实体类,它本质上定义了我们数据库中的一行是什么。请注意,每个实体都需要有一个与之相关联的标识或密钥,因此我们真正需要的唯一字段是标识字段。这里是 ModelBase类,我们将使用它作为我们的基本实体类,并且我们将覆盖我们创建的所有对象:

@PersistenceCapable(detachable = "true")
@Inheritance(strategy = InheritanceStrategy.SUBCLASS_TABLE)
public class ModelBase {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;
public Long getId() {
return id;
}
}

所以我们会注意到这个类的一般结构类似于一个相对简单的 Java 对象,但是有一些奇怪的 @标签。让我们看看前两个:

@PersistenceCapable(detachable = "true")
@Inheritance(strategy = InheritanceStrategy.SUBCLASS_TABLE)

第一个告诉我们这个类需要是 PersistenceCapable。当您将一个对象定义为能够持久化时,您告诉 JDO 数据库的是,这个对象能够从数据存储中存储和检索。将实体类声明为 PersistenceCapable,然后将所需字段声明为 Persistent,这一点很重要。你会看到还有一个参数叫做 detachable,我们设置为 true。这给了我们编辑和修改我们从数据库中检索到的实体的权限,即使我们已经关闭了它。现在,这并不意味着那些修改会因为数据库关闭而保留在数据库中,但至少我们有这样做的权限。

接下来是一个 Inheritance标签,它基本上意味着我们可以创建覆盖这个基本实体的实体,从而继承基本实体。另外两个标签很容易解释。第一个声明我们的标识(我会很快注意到,在我的例子中,我选择使用长类型作为我的标识,但是也可以使用键类型对象)充当我们实体的 PrimaryKey。对于有 SQL 背景的人来说,这应该会立即敲响警钟,但基本上这只是告诉 JDO 数据库,这个实体的对象将有一个唯一的长标识字段用于查找,等等。最后一个标签是我们前面简单提到的标签,即 Persistent标签,它简单地告诉我们这个长的标识字段应该作为它自己的列存储在我们的表中。

现在,对于实际的 VideoGame对象,首先注意我们如何扩展(继承)先前的 ModelBase类,然后我们继续定义所有需要的持久化字段以及实现构造函数等等,如下所示:

// NOTE HOW WE DECLARE OUR OBJECT AS PERSISTENCE CAPABLE
@PersistenceCapable
public class VideoGame extends ModelBase {
// NOTE THE PERSISTENT TAGS
@Persistent
private String name;
// USE A SPECIAL GOOGLE APP ENGINE LINK CLASS FOR URLS
@Persistent
private Link imgUrl;
@Persistent
private int consoleType;
public VideoGame(String name, String url, String consoleType) {
this.name = name;
this.imgUrl = new Link(url);
// CONVERT ALL CONSOLES TO INTEGER TYPES
this.consoleType = VideoGameConsole.convertStringToInt(consoleType);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Link getImgUrl() {
return imgUrl;
}
public void setImgUrl(Link imgUrl) {
this.imgUrl = imgUrl;
}
public int getConsoleType() {
return consoleType;
}
public void setConsoleType(int consoleType) {
this.consoleType = consoleType;
}
public static class VideoGameConsole {
public static final String XBOX = "Xbox";
public static final String PS3 = "Ps3";
public static final String WII = "Wii";
public static final String PSP = "Psp";
public static final String DS = "NintendoDS";
public static final String PS2 = "Ps2";
public static final String[] CATEGORIES = { "Xbox", "Ps3", "Wii", "Psp", "NintendoDS", "Ps2" };
public static int convertStringToInt(String type) {
if (type == null) { return -1; }
if (type.equalsIgnoreCase(XBOX)) {
return 0;
} else if (type.equalsIgnoreCase(PS3)) {
return 1;
} else if (type.equalsIgnoreCase(PS2)) {
return 2;
} else if (type.equalsIgnoreCase(PSP)) {
return 3;
} else if (type.equals(WII)) {
return 4;
} else if (type.equals(DS)) {
return 5;
} else {
return -1;
}
}
}
}

一旦你明白了 @标签的作用,剩下的就不言自明了。这里我只是简单地声明几个字段是持久的,然后我实现了一个构造函数和一个方便的内部类。我之所以喜欢有一个便利类(在这种情况下就是 VideoGameConsole)是因为通常在表中,查询整数比查询字符串要高效可靠得多(一:不需要担心大小写匹配,二:整数比较通常比字符串比较高效得多)。因此,理想情况下,我希望有一种方法可以将字符串转换为整数,甚至有可能将一组字符串映射为整数(也就是说,“PS 3”可以映射为 1,“Playstation 3”或“PS3”也可以)。

现在我们已经定义了 VideoGame实体,我们准备开始实现我们的数据库,并告诉它如何与这些 VideoGame实体交互。

持久性管理器和查询

因此,第一步是定义一种在服务器和数据库之间建立连接的方法。还记得本书开头我们在进行任何查询之前必须调用 getWritableDatabase()等方法吗?这里也是如此,但是我们将定义一个 PersistenceManager类如下,而不是使用 SQLiteOpenHelper类:

public final class PMF {
private static final PersistenceManagerFactory pmfInstance = JDOHelper.getPersistenceManagerFactory("transactions-optional");
private PMF() {
}
public static PersistenceManagerFactory get() {
return pmfInstance;
}
}

请注意,为了提高效率,它被定义为单例,我们所做的只是打开一个可以处理事务(查询)的持久性(数据库)管理器。然后在我们未来的查询中,我们不再需要通过重复请求 PersistenceManager来牺牲性能,而是可以抓取现有的实例。

一旦我们定义了 PersistenceManager,我们就可以开始实现我们的包装器系列,我们将从如何插入新的游戏对象开始:

public class VideoGameJDOWrapper {
/**
* INSERT A SINGLE VIDEOGAME OBJECT
*
* @param g
* - a video game object
*/
public static void insertGame(VideoGame g) {
PersistenceManager pm = PMF.get().getPersistenceManager();
try {
pm.makePersistent(g);
} finally {
pm.close();
}
}
/**
* INSERT MULTIPLE VIDEOGAME OBJECTS - MORE EFFICIENT METHOD
*
* @param games
* - a list of video game objects
*/
public static void batchInsertGames(List<VideoGame> games) {
PersistenceManager pm = PMF.get().getPersistenceManager();
try {
// ONLY NEED TO RETRIEVE AND USE PERSISTENCEMANAGER ONCE
pm.makePersistentAll(games);
} finally {
pm.close();
}
}
}

还不错吧?这个想法很简单,就是我们之前看到的简单地获取 PersistenceManager的实例(也就是说,我们与数据库的连接),并使传入的 VideoGame对象持久化。请再次记住,当使用 GAE 时,持久性的概念与插入相同,因此通过使对象持久化,我们实际上是告诉数据库将我们的实体转换为我们的 VideoGame表的一行。我们还可以看到,当一次添加许多实体时,GAE 通过使用批处理插入为我们提供了一种有效的方法。现在让我们看看如何从数据库中获取视频游戏对象。查询实体比简单地插入实体要复杂得多,但与其用一整章的时间来介绍提交查询的所有不同方式(就像我们在第 3 章SQLite query 中所做的那样)我来告诉你一个方便直观的方法,如果你好奇,我邀请你来看看:

http://code . Google . com/appengine/docs/Java/datastore/query . html

但是,是的,这里有一种方法可以做到这一点,它应该会让你想起我们之前遇到的 SQLiteQueryBuilder类:

public class VideoGameJDOWrapper {
public static void insertGame(VideoGame g) {
. . .
}
public static void batchInsertGames(List<VideoGame> games) {
. . .
}
/**
* GET ALL VIDEO GAMES OF A CERTAIN PLATFORM
*
* @param platform
* - desired platform of games
* @return
*/
public static List<VideoGame> getGamesByType(String platform) {
PersistenceManager pm = PMF.get().getPersistenceManager();
// CONVERT STRING OF PLATFORM TO INT TYPE
int type = VideoGameConsole.convertStringToInt(platform);
// INIT A NEW QUERY AND SPECIFY THE OBJECT TYPE
Query query = pm.newQuery(VideoGame.class);
// SET THE FILTER - EQUIVALENT TO SQL WHERE FILTER
query.setFilter("consoleType == inputType");
// TELL THE QUERY WHAT PARAMETERS YOU WILL SEND
query.declareParameters("int inputType");
List<VideoGame> ret = null;
try {
// EXECUTE QUERY WITH PARAMETERS
ret = (List<VideoGame>) query.execute(type);
} finally {
// CLOSE THE QUERY AT THE END
query.closeAll();
}
return ret;
}
/**
* GET ALL VIDEO GAMES OF A GIVEN PLATFORM WITH A LIMIT ON THE NUMBER OF
* RESULTS
*
* @param platform
* - desired platform of games
* @param limit
* - max number of results to return
* @return
*/
public static List<VideoGame> getGamesByTypeWithLimit (String platform, int limit) {
int type = VideoGameConsole.convertStringToInt(platform);
PersistenceManager pm = PMF.get().getPersistenceManager();
Query query = pm.newQuery(VideoGame.class);
query.setFilter("consoleType == inputType");
query.declareParameters("int inputType");
// SAME QUERY AS ABOVE BUT THIS TIME SET A MAX RETURN LIMIT
query.setRange(0, limit);
List<VideoGame> ret = null;
try {
ret = (List<VideoGame>) query.execute(type);
} finally {
query.closeAll();
}
return ret;
}
/**
* QUICKEST WAY TO RETRIEVE OBJECT IF YOU HAVE THE ID
*
* @param id
* - row id of the object
* @return
*/
public static VideoGame getVideoGamesById(long id) {
PersistenceManager pm = PMF.get().getPersistenceManager();
return (VideoGame) pm.getObjectById(VideoGame.class, id);
}
}

让我们一点一点地剖析第一种方法:

PersistenceManager pm = PMF.get().getPersistenceManager();
// CONVERT STRING OF PLATFORM TO INT TYPE
int type = VideoGameConsole.convertStringToInt(platform);
// INIT A NEW QUERY AND SPECIFY THE OBJECT TYPE
Query query = pm.newQuery(VideoGame.class);

这里,我们获取我们的 PersistenceManager,然后我们将传入的平台转换为整数类型,因为我们将按平台进行过滤。接下来,我们告诉我们的 PersistenceManager我们想要打开一个新的查询(也就是说,开始一个新的 SELECT语句),因此我们调用我们的 newQuery()方法。然后,我们用以下方法设置查询的细节:

// SET THE FILTER - EQUIVALENT TO SQL WHERE FILTER
query.setFilter("consoleType == inputType");
// TELL THE QUERY WHAT PARAMETERS YOU WILL SEND
query.declareParameters("int inputType");

这里我们首先设置我们的过滤器,并指定我们要在哪个列上执行过滤(即设置查询的 WHERE部分)。接下来,我们为将要传入的参数设置一个占位符。前面的占位符),最后,我们执行查询并传入平台类型参数。在接下来的方法中,除了使用以下方法设置的附加 LIMIT过滤器之外,一切都保持不变:

query.setRange(0, limit);

我们实现的第三种方法相对简单——JDO 数据库允许您通过调用 PersistenceManager 的 getObjectById()方法快速检索实体,如果您有它们的唯一键或标识。同样,在 GAE 有许多执行查询的方法,还有许多我在本书中不会涉及的其他条款和细节,但是现在您应该已经有了基本的想法,并且应该能够执行所需的绝大多数查询。最后,让我们看看如何更新和删除数据库中的对象:

public class VideoGameJDOWrapper {
public static void insertGame(VideoGame g) {
}
public static void batchInsertGames(List<VideoGame> games) {
}
public static List<VideoGame> getGamesByType(String platform) {
}
public static List<VideoGame> getGamesByTypeWithLimit (String platform, int limit) {
. . .
}
public static VideoGame getVideoGamesById(long id) {
. . .
}
/**
* METHOD FOR UPDATING THE NAME OF A VIDEO GAME
*
* @param newName
* - new name of the game
* @param id
* - the row id of the object
* @return
*/
public static boolean updateVideoGameName(String newName, long id) {
PersistenceManager pm = PMF.get().getPersistenceManager();
boolean success = false;
try {
// AS LONG AS PERSISTENCE MANAGER IS OPEN THEN ANY CHANGES TO OBJECT
// WILL AUTOMATICALLY GET UPDATED AND STORED
VideoGame v = (VideoGame) pm.getObjectById(VideoGame. class, id);
if (v != null) {
// KEEP PERSISTENCEMANAGER OPEN
v.setName(newName);
success = true;
}
} catch (JDOObjectNotFoundException e) {
e.printStackTrace();
success = false;
} finally {
// ONCE CHANGES ARE MADE - CLOSE MANAGER
pm.close();
}
return success;
}
/**
* DELETE ALL GAMES OF A CERTAIN PLATFORM
*
* @param platform
* - specify the platform of the games you want to delete
*/
public static void deleteGamesByType(String platform) {
PersistenceManager pm = PMF.get().getPersistenceManager();
int type = VideoGameConsole.convertStringToInt(platform);
// INIT QUERY AGAIN
Query query = pm.newQuery(VideoGame.class);
// SAME WHERE FILTERS
query.setFilter("consoleType == inputType");
query.declareParameters("int inputType");
// NOW CALL THE DELETE METHOD
query.deletePersistentAll(type);
}
}

同样,让我们采用第一种方法——我们的更新方法——并一点一点地剖析它:

PersistenceManager pm = PMF.get().getPersistenceManager();
boolean success = false;
try {
VideoGame v = (VideoGame) pm.getObjectById(VideoGame.class, id);
if (v != null) {
// KEEP PERSISTENCEMANAGER OPEN
v.setName(newName);
success = true;
}
}

就像前面的例子一样,我们从获取与 JDO 数据库的连接开始。然后我们试图通过调用 getObjectById()方法并传入我们想要更新的实体的唯一标识来检索我们的 VideoGame对象。这是你应该记住的关于 PersistenceManager的奇怪的事情之一。

而不是像我们现在习惯看到的那样有一个显式的更新方法,只要连接打开,你对对象所做的任何更改都会自动在数据库中得到更新。请注意,在这个方法中,第一步是检索实体,在连接仍然打开时更新它,然后在实体更新后关闭连接。

当然,在这个例子中,我们一次只更新一个特定的标识,但是我们可以看到,通过记住这个细节,我们可以很容易地编写一个方法,一次更新一组实体——只需查询它们的列表,并在 PersistenceManager仍然打开的时候更新每个实体。

最后但并非最不重要的一点是,对于我们的 delete 方法,我们看到所有的步骤都与前面的 get 方法相同,除了最后一行,我们在那里使用方法:

// NOW CALL THE DELETE METHOD
query.deletePersistentAll(type);

否则,所有的先验逻辑保持不变。就这样!现在我们有了一个 JDO 数据库包装器类,它允许我们抽象掉所有混乱的 PersistenceManager语法,并给我们一个快速的方法从我们的 GAE 后端插入、检索、更新和删除数据!接下来的一步是实际找出一种检索视频游戏数据的方法,在这一点上,我们可以简单地将它包装在我们的 VideoGame实体类中,并将其推入我们的数据库。

总结

在这一章中,我们离开了安卓平台,开始扩展我们对外部数据库的理解。首先,我们粗略地看一下我们的选择:传统的具有数据库连接的专用服务器(例如,连接到 MySQL 服务器的 Apache Tomcat 服务器)或云计算服务器/数据库组合,如亚马逊网络服务(AWS)谷歌应用引擎(GAE)

谷歌应用引擎很好,因为它更容易设置,也允许我们构建简单、相对小规模的应用,不受成本和时间的限制。这两种云计算解决方案都配有可靠的服务器以及高效、可扩展的数据库,但限制了您对后端的控制力,尤其是与您购买自己的专用服务器时拥有的无限自由相比。

坚持 GAE,我们开始建立一个简单的视频游戏应用,向我们展示所有的游戏通过百视达可用。我们在 GAE 引入了持久性的概念,并编写了第一个实体类。然后,我们编写了自己的 PersistenceManager singleton 类,并实现了一个方便类,用于从数据库中获取、插入、更新和删除数据。

在这一章中,我们讨论了很多内容,但是在拥有一个完整的、功能齐全的应用之前,我们还有很长的路要走。在下一章中,我们将研究如何检索数据,然后使用本章中编写的包装器方法存储数据。