8 数据库与存储

8数据库与存储

         当你在编写你新的,改变世界的应用程序时,有些时候你需要在手机上直接存储信息。这种信息可能是简单的文件,或缓存数据的序列化版本,或甚至一个完整的数据库。由于存储是在手机上的,所以理解你所有的选择是非常重要的,这样你可以做出正确的决定,在哪里存储信息以及采取最少的系统资源来完成这件事。在这一章中你将学习如何做这些决定以及如何实现它们。

存储数据

         应用程序需要数据。数据需要持久化。这是一个巨大的画面。你需要能够把数据存储在一个手机上,供你的应用程序使用。在你的应用程序的每次调用之间这数据需要保持手机上  以及通过重新启动。虽然Windows Phone通过几种方式来支持它,但是你需要了解这些方式之间的差异,同时选择最好的方式来完成数据存储。

         存储数据的两个主要方法来是通过隔离的存储和一个本地数据库。隔离存储允许你在一个虚拟文件系统里将数据存储为文件,只有你的应用程序可以访问。除了存储文件,你也可以将信息存储在一个本地数据库,本地数据库为你提供了一个方法来存储数据同时获取查询数据。从大方面来看似乎将数据存储在一个数据库是好的选择。对那些过渡到手机开发人员来说,这往往是最常见的方法。问题是你需要确定你是否需要一个数据库。

         使用手机的数据库比简单的存储文件需要更多的资源。这似乎很简单,但是这种简单性是有代价的。现实情况是,很多应用程序并不需要支持查询和更新功能的完整数据库系统。通常他们真正需要的是简单地存储数据供应用程序下次启动。

隔离的存储

         如前所述,其中一个选项是使用手机上的文件系统。每个应用程序被分配了一个独立私有的内部文件系统供它的使用。这意味着你存储在这个区域(称为隔离存储)里任何文件是仅供你的应用程序使用的。任何其它应用程序(尽管操作系统能访问这些文件)是无法访问的。当你的应用程序被安装时,文件系统的这一区域被分配完毕。同样,当卸载你的应用程序时,任何存储在隔离存储中的文件都将丢失。

         访问手机上的隔离存储类似于其他版本的Silverlight(甚至标准的.NET)的访问方式。你使用隔离存储类,打开的文件或目录,并使用System.IO堆栈来阅读、编辑和回写文件。让我们开始并看看如何使用这个隔离存储类。

隔离存储配额

         在桌面版Silverlight中隔离存储有指定的配额来限制多少数据可以存储在设备上,但在手机上不同。手机上的配额是没有限制的;只有设备剩余空间的问题。这些API包括手机上的配额的属性和方法,但你永远不应该使用这些,因为配额经常大于设备上的可用空间。

         若要使用隔离存储,你必须首先检索一个对象,该对象将代表手机的存储网关。你可以对IsolatedStorageFile对象(在System.IO.IsolatedStorage命名空间)这样做。这个名字有点误导,因为它不是一个文件,而是一个代表你的应用程序“存储”的对象。你通过调用IsolatedStorageFile的GetUserStoreForApplication静态方法获取该对象:

         using (IsolatedStorageFile store =

                            IsolatedStorageFile.GetUserStoreForApplication()

{

         // Use the store

}

         IsolatedStorageFile类支持 IDisposable,所以你应该把它封装在一个using语句中(如示例中所示)来确保它在使用之后释放所有的资源。

性能提示

         IsolatedStorageFile对象在你访问它之后应该被释放。因为由于访问这个对象是昂贵的,你可能想要缓存这个对象来重复使用。使用using包装该对象的是有效的,除非你打算保存多个文件。在这种情况下,你应该重用IsolatedStorageFile对象。

         IsolatedStorageFile类支持方法来创建和打开文件和目录。你可以将store想象成一个存储你的数据的目录结构的起点。例如,你可以使用CreateFile方法来创建一个新文件,就像这样:

         using (IsolatedStorageFile store =

                            IsolatedStorageFile.GetUserStoreForApplication())

         {

                   using (IsolatedStorageFileStream file = store.CreateFile("settings.txt")

                   {

                            StreamWriter writer = new StreamWriter(file);

                            writer.WriteLine("FavoriteColor=Blue");

                            writer.Close();

                   }

         }

         从创建文件的方法返回的的IsolatedStorageFileStream可以被视为一个简单的流。所以使用标准.NET技术的流写入方法工作得很好(如例子中所示的使用一个StreamWriter)。你可以做同样方法使用OpenFile方法读取文件:

         using (IsolatedStorageFile store =

                            IsolatedStorageFile.GetUserStoreForApplication())

         {

                   using (IsolatedStorageFileStream file =

                            store.OpenFile("settings.txt", FileMode.Open))

                   {

                            StreamReader reader = new StreamReader(file);

                            var line = reader.ReadToEnd();

                            reader.Close();

                   }

         }

隔离存储与模拟器

         当你在模拟器中与隔离存储工作时,在隔离存储中的数据在应用程序多次调用过程是持续存在的,但在两种情况下将被清除:重新启动模拟器和执行一个“Clean”的Visual   Studio项目构建。从Visual Studio中执行一个干净的构建,将导致在模拟器中的应用程序彻底清除隔离储存。这种干净的构建也在“rebuild”期间执行,但是在标准构建或简单运行你的应用程序时并不发生。

序列化

         一般来说,创建你自己的文件和目录会帮助你储存你所需要的信息,但是发明你的文件格式通常是不必要的。有三种常见的方法用结构化的形式储存信息:XML序列化,JSON序列化和隔离存储设置。

XML序列化

         XML是一种常见的方法来格式化在手机上数据。对于XML序列化,.NET提供了内置的支持,使用XmlSerializer类。这个类允许你使用托管对象图谱并创建一个XML版本。另外(至关重要的是),你可以将XML重新读入内存来重建对象图谱。

         开始编写一个对象图谱做为XML前,你需要了解你想要序列化的类。对于这里的例子,假设你有一个这样的类,它保留用户的喜好,像这样:

         public class UserPreferences

         {

                   public DateTime LastAccessed { get; set; }

                   public string FirstName { get; set; }

                   public string LastName { get; set; }

                   public bool UsePushNotifications { get; set; }

         }

         标准XML序列化要求确定的构造函数(空构造函数)来工作。如果你没有显式地创建一个构造函数,编译器将隐式地创建一个空构造函数。因为XML序列化需要一个空构造函数,你可以依赖于一个编译器创建的(如在上面的示例),但是如果你添加一个显式构造函数你需要确保你还包括一个没有参数的构造函数(例如,一个空构造函数)。现在,我们有一些东西来序列化,让我们继续并存储数据。

         首先你需要添加System.Xml.Serialization程序集到你的项目。然后你可以使用XmlSerialization类(位于System.Xml.Serialization命名空间)通过创建一个它的实例(传入你想要序列化的类型):

         using (var store = IsolatedStorageFile.GetUserStoreForApplication())

         using (var file = store.CreateFile("myfile.xml"))

         {

                   XmlSerializer ser = new XmlSerializer(typeof(UserPreferences);

         }

         这创建了一个UserPreferences类型的serializer。这个类的实例可以序列化或反序列你的类型(和关联类型)到XML。为了序列化到XML,简单的调用Serialize方法:

   var someInstance = new UserPreferences();

         using (var store =

                                     IsolatedStorageFile.GetUserStoreForApplication())

         using (var file = store.CreateFile("myfile.xml"))

         {

                   XmlSerializer ser = new

                                                                 XmlSerializer(typeof(UserPreferences));

                   // Serializes the instance into the newly created file

                   ser.Serializefile someInstance;

         }

         这段代码将UserPreferences类的实例存储到之前在隔离存储创建的文件中。要将文件读回,你可以颠倒这个过程,通过使用Deserialize方法,像这样:

         UserPreferences someInstance = null;

         using (var store = IsolatedStorageFile.GetUserStoreForApplication())

         using (var file = store.OpenFile("myfile.xml", FileMode.Open))

         {

                   XmlSerializer ser = new XmlSerializer(typeof(UserPreferences));

                   // Re-creates an instance of the UserPreferences

                   // from the Serialized data

                   someInstance = (UserPreferences)ser.Deserialize(file);

         }

         通过使用Deserialize方法,你可以重建保存的数据和使用序列化的数据实例化一个数据类的新实例。序列化类还支持一个CanDeserialize方法,因此你可以测试看看流中是否包含与类型兼容的可序列化的数据:

         UserPreferences someInstance = null;

         using (var store =

                                     IsolatedStorageFile.GetUserStoreForApplication())

         using (var file = store.OpenFile("myfile.xml", FileMode.Open))

         {

                   XmlSerializer ser =

                                                         new XmlSerializer(typeof(UserPreferences));

                   // Use an XmlReader to allow us to test for serializability

                   var reader = XmlReader.Create(file);

                   // Test to see if the reader contains serializable data

                   if (ser.CanDeserialize(reader))

                   {

                            // Re-creates an instance of the UserPreferences

                            // from the Serialized data

                            someInstance =

                                                        (UserPreferences)ser.Deserialize(reader);

                   }

         }

         要测试数据是否包含序列化的数据,你需要使用XmlReader.Create方法创建一个XmlReader对象包含文件的内容(作为一个流)。一旦你创建了一个XmlReader,你可以使用serializer的CanDeserialize方法。如果你要将文件包裹在一个XmlReader内,你也应该发送reader对象到Deserialize方法(因为这比在XmlSerializer的内部创建一个新的reader更高效)。

XML序列化的选择

         虽然你可以看到如何使用内置XML序列化的类序列化你的对象,你还可以使用第三方类甚至来自WCF的DataContractXmlSerialization 类来执行序列化。概念都是相同的。

JSON序列化

         尽管XML是很多开发人员的首选,但是时至今日可能就不是了。JavaScript Object Notation (JSON)的扩散意味着,本地序列化和任何基于网络的或基于REST的交互可以使用相同的格式是司空见惯的。此外,JSON往往在当序列化时比XML更小。虽然规模本身不是选择JSON的一个理由,更小通常意味着更快,这就是一个很好的理由去选择它。

         你可以使用DataContractJsonSerializer类代替XmlSerializer类。这个类是WCF(或服务模型)的一部分类,也是Windows Phone SDK的一部分。这个类位于System.Runtime.

Serialization命名空间中,但是它是在System.ServiceModel.Web程序集中实现的,这个程序集默认没有包括在项目中,因此你需要手动添加它。

         使用DataContractJsonSerializer类,你可以创建它通过指定数据类型来存储,就像这样:

         UserPreferences someInstance = new UserPreferences();

         using (var store = IsolatedStorageFile.GetUserStoreForApplication())

         using (var file = store.CreateFile("myfile.json"))

         {

                   // Create the Serializer for our preferences class

                   var serializer = new

                            DataContractJsonSerializer(typeof(UserPreferences);

                   // Save the object as JSON

                   serializer.WriteObjectfile someInstance;

         }

         你可以在这里看到的DataContractJsonSerializer类的WriteObject方法用于序列化UserPreferences类的实例为JSON。对于反序列化数据回到UserPreferences类的实例,你可以ReadObject方法代替:

         UserPreferences someInstance = null;

 

         using (var store = IsolatedStorageFile.GetUserStoreForApplication())

         using (var file = store.OpenFile("myfile.json", FileMode.Open))

         {

                   // Create the Serializer for our preferences class

                   var serializer = new

                            DataContractJsonSerializer(typeof(UserPreferences));

                   // Load the object from JSON

                   someInstance = UserPreferencesserializer.ReadObjectfile;

         }

         在这种情况,序列化代码仅仅使用了DataContactJsonSerializer类的ReadObject方法通过读取早些时候被保存到存储的JSON文件,来加载UserPreferences类的一个新实例。

隔离存储设置

         有时让系统为你处理序列化是更容易的事。这就是隔离存储设置背后的概念。这个IsolatedStorageSettings类代表了访问一个存储在手机上的简单的内容字典。当你需要存储而又不想建立一个完整的序列化架构时,这种类型的存储对小块的信息是很有用的(例如,应用程序设置)。

         为了开始,你需要获取你的应用程序的settings对象,通过IsolatedStorageStatic上的一个静态访问器叫做ApplicationSettings(在System.IO.IsolatedStorage命名空间中),就像这样:

         IsolatedStorageSettings settings =

                   IsolatedStorageSettings.ApplicationSettings;

         这类公开了一个属性字典,为你自动序列化到隔离存储。例如,如果你想要在关闭应用程序时存储一个设置并且在应用程序启动时把它读回来,你可以使用IsolatedStorageSettings来完成,就像这样:

Color _favoriteColor = Colors.Blue;

const string COLORKEY = "FavoriteColor";

 

void Application_Launching(object sender, LaunchingEventArgs e)

{

         if (IsolatedStorageSettings.ApplicationSettings.Contains(COLORKEY))

         {

                   _favoriteColor =

                            (Color)IsolatedStorageSettings.ApplicationSettings[COLORKEY];

         }

}

 

void Application_Closing(object sender, ClosingEventArgs e)

{

         IsolatedStorageSettings.ApplicationSettings[COLORKEY] = _favoriteColor;

}

         通过使用IsolatedStorageSettings类的ApplicationSettings属性,你可以设置和获取简单值。你可以看到在启动应用程序过程中,代码首先检查键是否在ApplicationSettings中(为了确保键值对至少被保存了一次),如果存在,它从隔离存储中加载值。    

         IsolatedStorageSettings类与简单的类型工作的很好,但是如果你想要存储的数据要与XML序列化兼容(如我们前面的示例),设置文件也支持保存它:

         UserPreferences _preferences = new UserPreferences();

         const string PREFKEY = "USERPREFS";

         void Application_Launching(object sender, LaunchingEventArgs e)

         {

                   if (IsolatedStorageSettings.ApplicationSettings.Contains(PREFKEY))

                   {

                            _preferences = (UserPreferences

                                     IsolatedStorageSettings.ApplicationSettings[PREFKEY];

                   }

         }

         void Application_Closing(object sender, ClosingEventArgs e)

         {

                   IsolatedStorageSettings.ApplicationSettings[PREFKEY] =

                            _preferences;

         }

本地数据库

         当你正在构建的应用程序需要数据,这些数据必须能够查询并支持智能更新,本地数据库是最好的方式来实现这一目标。Windows Phone支持直接在手机上存在的数据库。创建一个Windows Phone应用程序时,你可能不会有直接访问数据库,相反的,而你可以使用一个变种的LINQ to SQL结合一种code-first的方法来建立一个数据库,以完成你的数据库访问。让我们浏览一下这个功能的内容。

入门指南

         开始时,你会需要一个数据库文件。在后台数据库是SQL Server精简版,所以你可以为你项目创建一个.sdf文件,但是你通常会首先要通知数据库APIs来为你创建数据库。

向前看

 你可以引入本地数据库文件(SQL Server精简版或.sdf文件)到你的应用程序。我们将在本章后面讨论这个。

         第一步是要有一个类(或几个),代表你想要存储的数据。你可以从一个简单的类开始:

         public class Game

         {

                   public string Name { get; set; }

                   public DateTime? ReleaseDate { get; set; }

                   public double? Price { get; set; }

         }

         这个类存储一些数据块,这些数据块我们希望能够存储在数据库中。在我们可以将其存储在数据库中之前,我们必须添加属性告诉LINQ to SQL,这描述了一个表:

[Table]

public class Game

{

[Column]

public string Name { get; set; }

[Column]

public DateTime? ReleaseDate { get; set; }

[Column]

public double? Price { get; set; }

}

         通过使用这些属性,我们正在创建一个类,表示存储于数据库中的一个表。一些列信息是被推断出来(例如nullability在ReleaseDate列)。使用这个定义,我们可以从数据库读取,但是在我们可以添加或更改的数据前,我们需要定义一个主键:

         [Table]

         public class Game

         {

                   [Column(IsPrimaryKey = true, IsDbGenerated = true)]

                   public int Id { get; set; }

                   [Column]

                   public string Name { get; set; }

                   [Column]

                   public DateTime? ReleaseDate { get; set; }

                   [Column]

                   public double? Price { get; set; }

         }

         如你所见,列的attribute有几个properties,可以用于为每一列设置指定的信息。在这种情况下,列attribute指定的Id列是主键,并且该键应该由数据库生成。为了支持更改跟踪和回写到数据库中,你必须有一个主键。

         像任何其他数据库引擎,SQL Server精简版允许你加入自己的索引来提高查询性能。你可以通过添加索引属性到表类(table classes)来实现:

         [Table]

         [Index(Name = "NameIndex", Columns = "Name", IsUnique = true)]

         public class Game

         {

                   // ...

         }

         Index属性允许你指定一个名称、一个字符串,该字符串包含要被索引的列名,并且可以选择索引是否是唯一索引。这个attribute是在你创建或更新数据库时使用的。你还可以指定一个索引在多个列的上,通过逗号分隔列名:

         [Table]

         [Index(Name = "NameIndex", Columns = "Name", IsUnique = true)]

         [Index(Columns = "ReleaseDate,IsPublished")]

         public class Game : INotifyPropertyChanging, INotifyPropertyChanged

         {

                   // ...

         }

         此时,你已经定义了一个简单的表有两个索引,可以继续创建一个数据上下文类。这个类是你访问数据库本身的入口点。它是一个类继承于 DataContext类,如下所示:

         public class AppContext : DataContext

         {

         }

         泛型类封装了你的数据表类来表示一组可用于查询对象。这种方式,你的上下文类不仅会让你可以访问存储在数据库中的对象,而且替你跟踪他们。基类(DataContext)是大多数魔法发生的地方。因为DataContext类没有一个空构造函数,你还需要实现一个构造函数:

         public class AppContext : DataContext

         {

                   public AppContext()

                            : base("DataSource=isostore:/myapp.sdf;"

                   {

                   }

                   public Table<Game> Games;

         }

         典型的调用基类的构造函数,你需要发送一个连接字符串。对于手机,所有的这些连接字符串需要的是一个描述,数据库的存在位置或者它将被创建的位置。你通过指定URI来指定数据库文件属于的位置。URI是一个无论是从隔离存储或应用程序文件夹到文件的路径。

指定一个文件存在(或被创建)于隔离存储,你可以使用isostore标记对象就像这样:

         isostore:/myapp.sdf

         对于一个只与你的应用程序一起发布的数据库(并将被交付在.xap文件中),你也可以使用appdata标记。如果你想访问驻留在应用程序文件夹中的数据,你只能读取数据库,不能编辑它。在本章后面你将看到如何复制数据库到isostore文件夹,如果你需要写入一个appdelivered的数据库。你可以使用appdata标记就像isostore标记,指定应用程序文件夹中数据库的位置:

         appdata:/myapp.sdf

         对于实际的文件,URI的结束应该是一个路径和文件名。底层的数据库是SQL Server精简版(即SQL CE),所以该文件是一个.sdf文件。如果你想要将你的数据库文件放在一个子文件夹中,你可以在URI中指定它,使用文件夹的名字就像这样:

         isostore:/data/myapp.sdf

         一旦你创建了你的数据上下文类,你可以通过调用CreateDatabase方法来创建数据库   (以及检查看看它是否存在通过调用DatabaseExists):

         // Create the Context

         var ctx = new AppContext();

         // Create the Database if it doesn't exist

         if (!ctx.DatabaseExists())

         {

                   ctx.CreateDatabase();

         }

         上下文的表成员允许你在底层数据上执行CRUD。例如,要创建一个数据库中新的游戏对象,你只需要创建一个实例,并将它添加到游戏成员中:

         // Create a new game object

         var game = new Game()

         {

                   Name = "Gears of War",

                   Price = 39.99,

         };

         // Queue it as a change

         ctx.Games.InsertOnSubmit(game);

         // Submit all changes (inserts, updates and deletes)

         ctx.SubmitChanges();

         新的Game对象可以通过InsertOnSubmit方法传递给上下文的Games成员,以告诉上下文来保存这个对象在下次提交更改到数据库。SubmitChanges方法将获得从上下文对象创建以来已经发生的任何更改(或自上次调用SubmitChanges以来),并批量的处理他们到底层数据库。注意,游戏的新实例没有设置Id属性。这是不必要的因为Id属性被标志为主键(这是需要的来支持写入数据库)而且也是由数据库生成的。这意味着当SubmitChanges被调用时,它让数据库生成ID并更新你的对象的ID为数据库生成的ID。

         查询数据库中存储的游戏采用的是LINQ查询形式。所以如果你已经创在数据库中建了一些数据,你可以像这样来查询它:

         var qry = from g in ctx.Games

                            where g.Price >= 49.99

                            order by g.Name

                            select g;

         var results = qry.ToList();

         这个查询将直接从数据库返回一组包含数据的游戏对象。对你来说当这段代码调用ToList方法时,这段LINQ查询转换为参数化的SQL查询并针对本地数据库进行执行。事实上,在调试时你可以在Visual Studio中查看到翻译过的查询,如图8.1所示。

                       

图8.1 SQL查询

         如果你改变这些对象,context类为你跟踪这些变化,这可能不是显而易见的。所以如果你改变一些数据,同时调用上下文的SubmitChanges方法更新数据库:

         var qry = from g in ctx.Games

                            where g.Name == "Gears of War"

                            select g;

         var game = qry.First();

         game.Price = 34.99;

         // Saves any changes to the game

         ctx.SubmitChanges();

         此外,你可以删除个别条目,使用上下文类中的表成员,调用DeleteOnSubmit方法来就像这样:

         var qry = from g in ctx.Games

                            where g.Name == "Gears of War"

                            select g;

         var game = qry.FirstOrDefault();

         ctx.Games.DeleteOnSubmit(game);

         // Saves any chances to the game

         ctx.SubmitChanges();

         你确实需要检索实体以便删除它们(不像的完整版本的LINQ to SQL,你可以执行任意SQL)。你可以通过调用DeleteAllOnSubmit提交删除并提供一个查询:

         var qry = from g in ctx.Games

                            where g.Price > 100

                            select g;

         ctx.Games.DeleteAllOnSubmit(qry);

         ctx.SubmitChanges();

         在这个例子中查询定义了要从数据库中删除的项目。查询并不理解获取它们,但使用查询来定义要删除的项目。一旦所有项目都标记为删除,调用SubmitChanges使删除发生(以及任何其他的变化被检测到)。

         通过创建你的表类和一个上下文类,你可以访问数据库并执行所有的查询和必要的更新数据库。接下来让我们看看其他的数据库特性,你可能会要考虑成为你的手机应用程序的一部分。

优化上下文类

         尽管上下文类会追踪你的对象,你可以帮助上下文类,确保你的表类支持INotifyPropertyChanging改变和INotifyPropertyChanged接口。实现这些接口在协助Silverlight上的数据绑定还有其他的好处。因此,建议你所有的表类支持这个接口,就像这样:

         [Table]

         public class Game : INotifyPropertyChanging INotifyPropertyChanged

         {

                   // …

                   public event PropertyChangingEventHandler PropertyChanging;

                   public event PropertyChangedEventHandler PropertyChanged;

                   void RaisePropertyChangedstring propName

                   {

                            if PropertyChanged != null

                            {

                                     PropertyChanged(this, new PropertyChangedEventArgs(propName));

                            }

                   }

                   void RaisePropertyChangingstring propName

                   {

                            if PropertyChanging != null

                            {

                                     PropertyChanging(this

                                     new PropertyChangingEventArgspropName));

                            }

                   }

         }

         实现接口需要添加PropertyChanging和PropertyChanged事件到你的类中。就像这里看到的,创建一个简单的助手方法来引发这些事件是一种常见的做法。现在,接口已经实现了,你必须使用它们。这涉及到在每个属性的setter调用助手方法。我们最初的游戏类使用  自动属性公开列,但是因为我们需要调用助手方法,所以我们需要标准的属性:

         [Table]

         public class Game : INotifyPropertyChanged

         {

                   int _id;

                   [Column(IsPrimaryKey = true, IsDbGenerated = true)]

                   public int Id

                   {

                            get { return _id; }

                            set

                            {

                                     RaisePropertyChanging("Id");

                                     _id = value;

                                     RaisePropertyChanged("Id");

                            }

                   }

                   string _name;

                   [Column]

                   public string Name

                   {

                            get { return _name; }

                            set

                            {

                                      RaisePropertyChanging("Name");

                                     _name = value;

                                     RaisePropertyChanged("Name");

                            }

                   }

                   DateTime? _releaseDate;

                   [Column]

                   public DateTime? ReleaseDate

                   {

                            get { return _releaseDate; }

                            set

                            {

                                     RaisePropertyChanging("ReleaseDate");

                                     _releaseDate = value;

                                     RaisePropertyChanged("ReleaseDate");

                            }

                   }

                   double? _price;

                   [Column]

                   public double? Price

                   {

                            get { return _price; }

                            set

                            {

                                     RaisePropertyChanging("Price");

                                     _price = value;

                                     RaisePropertyChanged("Price");

                            }

                   }

                   // ...

         }

         你应该注意每个属性现在拥有一个支持字段成员(如_id为Id属性)和调用RaisePropertyChanging和RaisePropertyChanged方法使用属性的名称当setter被调用时。通过使用这些接口,这个上下文对象内存的占用要小得多,因为它使用这些接口来监控变化。

         除了这些接口,你可以提高你的更新和删除查询的大小,通过在你的类中包含一个版本成员:

         [Table]

         public class Game : INotifyPropertyChanging, INotifyPropertyChanged

         {

                   // ...

                   [Column(IsVersion = true)]

                   private Binary _version;

         }

         Version列(IsVersion = true)是可选的,但是在使用数据库数据时,这将提高变更跟踪的性能。版本必须是Binary类型,来自于System.Data.Linq命名空间。它可以是一个私有字段(这样,它对于用户是不可见),但是对于LINQ to SQL它需要被标记成IsVersion = true,才能认为它是version列。

性能建议

         建议你的表类支持主键列和一个版本列,并且为他们实现INotifyPropertyChanging   和INotifyPropertyChanged接口,使你的数据库访问代码变得尽可能的高效。

    最后,如果你的数据库是只执行查询,你可以告诉上下文类,你并不希望监视任何变更管理。你可以通过设置上下文类的ObjectTrackingEnabled属性设为false来完成,就像这样:

    using (var ctx = new AppContext())

         {

                   ctx.ObjectTrackingEnabled = false;

                   var qry = from g in ctx.Games

                   where g.Price < 19.99

                  orderby g.ReleaseDate descending

                   select g;

                   var results = qry.ToList();

         }

         通过禁用变更管理,上下文对象将变得更为轻量。而且,因为上下文不需要跟踪变更,你可以局部的创建它和当查询完成时释放它。通常你会在整个页面或应用程序的生命周期中保持上下文,以便它可以监视和批处理这些更改回到数据库,但因为你只是从数据库中读取数据,如果需要的话可以缩短它的生命周期。

关联

         你见过的所有表类每个属性的数据类型都是简单类型。这些类型都是简单类型是因为他们需要被存储在本地的数据库中。为了储存在本地数据库,他们需要转换为数据库类型(例如,字符串存储为NVARCHARs)。尽管你将会去处理这些类,你还需要记住在底层这是一个关系数据库。所以当你需要更多的结构,你将需要关联(或组合)表。

         例如,让我们假定我们有一个(第二个)表类,用于容纳关于一个游戏的出版商信息:

[Table]

public class Publisher :

         INotifyPropertyChanging, INotifyPropertyChanged

{

int _id;

[Column(IsPrimaryKey = true, IsDbGenerated = true)]

public int Id

{

get { return _id; }

set

{

RaisePropertyChanging("Id");

_id = value;

RaisePropertyChanged("Id");

}

}

string _name;

[Column]

public string Name

{

get { return _name; }

set

{

RaisePropertyChanging("Name");

_name = value;

RaisePropertyChanged("Name");

}

}

string _website;

[Column]

public string Website

{

get { return _website; }

set

{

RaisePropertyChanging("Website");

_website = value;

RaisePropertyChanged("Website");

}

}

[Column(IsVersion = true)]

private Binary _version;

public event PropertyChangingEventHandler PropertyChanging;

public event PropertyChangedEventHandler PropertyChanged;

void RaisePropertyChanged(string propName)

{

     if (PropertyChanged != null)

     {

              PropertyChanged(this, new PropertyChangedEventArgs(propName));

     }

}

void RaisePropertyChanging(string propName)

{

     if (PropertyChanging != null)

     {

              PropertyChanging(this,

                       new PropertyChangingEventArgs(propName));

     }

}

}

         这个新类的实现就像游戏类一样(因为我们想要它允许变更管理)。为了能够将它保存在数据库中,我们需要在我们的上下文类中以一个公共字段公开它:

         public class AppContext : DataContext

         {

                   public AppContext()

                            : base("DataSource=isostore:/myapp.sdf;")

                   {

                   }

                   public Table<Game> Games;

                   public Table<Publisher> Publishers;

         }

         此时,你可以创建、编辑、查询和删除游戏和出版商这两个对象。但是你真正想要的是两个对象能够彼此联系。这就是关联的由来。

         为了添加一个关系,我们首先需要在游戏类中有一个列来代表出版商表的主键:

         [Table]

         public class Game : INotifyPropertyChanging, INotifyPropertyChanged

         {

                   // ...

                   [Column]

                   internal int _publisherId;

         }

         这个新列被用来保存这个特定的游戏相关的出版商ID。数据没有公开在这种情况下(它是内部),因为这个类的用户不会显式地设置这个值。作为代替,你将创建一个非公开的成员,它将存储一个叫做EntityRef的对象。这个EntityRef类是一个泛型类,它封装了一个相关的实体:

         [Table]

public class Game : INotifyPropertyChanging, INotifyPropertyChanged

{

         // ...

         [Column]

         internal int _publisherId;

         private EntityRef<Publisher> _publisher;

}

         这个EntityRef类在这里非常重要,因为它还将支持延迟加载相关的实体,因此大型对象图谱不会意外的加载。但真正使列和EntityRef联系起来的魔法发生在相关实体的public属性里:

    [Table]

         public class Game : INotifyPropertyChanging, INotifyPropertyChanged

         {

                   // ...

                   [Column]

internal int _publisherId;

 

private EntityRef<Publisher> _publisher;

 

[AssociationIsForeignKey = true

                            Storage = "_publisher"

                            ThisKey = "_publisherId"

                            OtherKey = "Id")]

public Publisher Publisher

{

         get { return _publisher.Entity; }

         set

         {

                   // Handle Change Management

                                      RaisePropertyChanging("Publisher");

                   // Set the entity of the EntityRef

                   _publisher.Entity = value;

                   if value != null

                   {

                            // Set the foreign key too

                            _publisherId = value.Id;

                   }

                   // Handle Change Management

                                     RaisePropertyChanged("Publisher");

         }

                   }

         }

         在这个属性上发生了很多的事情,但是让我们一次分析一块。首先,让我们看一下Association属性。这个属性有很多参数,但这些都是基本的设置。IsForeignKey参数告诉关联,这是一个外键关系。Storage参数描述了为这个关系存储EntityRef的类成员名称。ThisKey和Otherkey是关联双方的列的键值。ThisKey指的是在这类(游戏)列的名称和OtherKey指的是关联另一边 (出版商)的列名。

         当有人访问该属性,你将返回在EntityRef对象中的实体,如在上述属性的getter所示。

         最后, setter中有一系列的操作。在setter中第一个和最后一个操作处理变更管理通知,就像任何你的表类中的列属性。然后它使用属性值,并把属性值设置给EntityRef对象中的Entity最后,如果正在设置的值不为null,它设置表类的外键ID,以便代表外键列被设置。

         通过所有的这些,你就可以拥有在两个表类上的一个一对多的关系。但到目前为止,只有单向关联。为了完成关联,你可能需要在出版商表类中的一个集合,代表所有的游戏出版商。

         添加关系的另一侧是相似的,但是在这种情况下你需要的是一个实例称为EntitySet的泛型类:

    [Table]

         public class Publisher :

                   INotifyPropertyChanging, INotifyPropertyChanged

         {

                   // ...

                  EntitySet<Game> _gameSet;

                   [AssociationStorage = "_gameSet"

                            ThisKey = "Id"

                            OtherKey = "_publisherId")]

                   public EntitySet<Game> Games

                   {

                            get { return _gameSet; }

                            set

                            {

                                     // Attach any assigned game collection to the collection

                                     _gameSet.Assignvalue;

                            }

                   }

         }

    这个EntitySet类封装了一个与这个表类关联的元素集合。在这种情况下,EntitySet封装了属于一个出版商的一组游戏。作为关系的另一侧,指定了Storage、ThisKey和OtherKey帮助上下文对象算出如何创建关联。唯一的真正令人吃惊的是,当Games属性的setter被调用时,它会附加任何分配给它的的游戏来设置Games属性。这是通常只有上下文类执行一个查询时调用。

         虽然不明显, _gameSet字段的构造过程没有显示。这需要我们在构造函数中完成:

    [Table]

public class Publisher :

                  INotifyPropertyChanging, INotifyPropertyChanged

{

// ...

public Publisher()

{

_gameSet = new EntitySet<Game>

new Action<Game>this.AttachToGame),

new Action<Game>this.DetachFromGame));

}

void AttachToGameGame game

{

                     RaisePropertyChanging("Game");

game.Publisher = this;

}

void DetachFromGameGame game

{

                            RaisePropertyChanging("Game");

                            game.Publisher = null;

}

}

         在构造函数中,你必须创建EntitySet。注意,在构造函数中你还将传入两种行动来处理从集合中附加或分离一个游戏。这两个行动的目的是确保单独的游戏被附加/分离,同时设置或清除他们的关联属性。此外,当关联变更时,引发PropertyChanging事件使上下文对象变得非常的高效。

使用一个已经存在的数据库

         因为底层数据库是SQL Server精简版,你可能想要使用一个现有的数据库(.sdf文件)。要做到这一点,你可以简单的添加它到你的手机项目中(作为内容),如图8.2所示。

 

图8.2SQL Server精简版数据库作为内容

         通过标记数据库为内容,当你的应用程序安装时数据库将被部署到应用程序数据文件夹中。使用一个现有的数据库意味着你要构建你的上下文和类文件来匹配现有的数据库。目前还没有工具为你来构建这些类。

         注:目前有在线的演示使用桌面工具来构建这些类,然后重构它们供手机使用,但是,这并不是一件容易的工作。

         当你有一个数据库作为你的项目的一部分, 在建立一个上下文对象时,你可以引用它使用appdata标记,就像这样:

    public class AppContext : DataContext

         {

                   public AppContext()

                            : base("DataSource=appdata:/DB/LocalDB.sdf;File Mode=read only;")

                   {

                   }

                   // ...

         }

         当你直接使用一个在应用程序目录中的数据库,数据库只能进行读访问。这意味着你必须包含“文件模式”指令在连接字符串中,来表示数据库是只读的。

         通常倾向于使用应用程序目录中的数据库作为你的数据库模板。为了做到这一点,你必须首先复制数据库到隔离存储:

    // Get a Stream of the database from the Application Directory

         var dbUri = new Uri("/DB/LocalDB.sdf", UriKind.Relative);

         using (var dbStream = Application.GetResourceStream(dbUri).Stream)

         {

                   // Open a file in isolated storage for writing

                   using (var store =

                                               IsolatedStorageFile.GetUserStoreForApplication())

                   using (var file = store.CreateFile("LocalDB.sdf"))

                   {

                            byte[] buffer = new byte[4096];

                            int sizeRead;

                            // Write the database out

                            while ((sizeRead = dbStream.Read(buffer, 0, buffer.Length)) > 0)

                            {

                                     file.Write(buffer, 0, sizeRead);

                            }

                   }

         }

         你可以通过从应用程序目录简单的拷贝数据库来完成这些,使用Silverlight的Application类来获取一个包含数据库的流。然后仅仅在隔离存储中创建一个新文件(如本章之前所示)并使用新文件保存数据。如果你复制数据库,你可以使用你的上下文类与简单的isostore标记来读取和写入刚刚复制的数据库。

更新Schema

         既然,你已经创建了你的数据库驱动的应用程序,并且你现在准备好将其更新到新版本。但是你的用户一直尽职尽责地添加数据到你的数据库,你必须修改数据库。你会怎么做那?

         Windows Phone SDK可以帮助你完成本地数据库的堆栈。在SDK中,有一个DatabaseSchemaUpdater类,可以利用一个现有的数据库并进行附加变更,这些操作对于数据库都是安全的。这些包括添加可以为空的列,添加表,添加关联,并添加索引。

         开始你需要获得DatabaseSchemaUpdater类的一个实例。你使用DataContext的Create-

DatabaseSchemaUpdater方法来获取这个类:

         using (AppContext ctx = new AppContext())

         {

                   // Grab the DatabaseSchemaUpdater

                   var updater = ctx.CreateDatabaseSchemaUpdater();

         }

         这个updater类允许你不仅使用额外的修改,同时还可以处理一个数据库的版本。这为你提供了一个简单的方法来确定任何数据库的更新级别。Updater类支持一个简单的属性叫做DatabaseSchemaVersion:

         var version = updater.DatabaseSchemaVersion;

         使用这个数据库版本,你可以进行增量更新:

         // If specific version, then update

         if (version == 0)

         {

                   // Some simple updates (Add stuff, no remove or migrate)

                   updater.AddColumn<Game>("IsPublished");

                   updater.DatabaseSchemaVersion = 1;

                   updater.Execute();

         }

         数据库版本总是从0开始,使用updater可以将数据库版本改变成一个特定的版本。由于是典型的架构更改,你会添加任何新列,表,索引,或关联。然后你将更新数据库架构版本以确保这个更新不会执行第二次。然后,随着时间的推移,你可以测试更多的版本块。例如,随着你的应用程序接收到更多更新,代码看起来可能像这样:

         // If specific version, then update

         if (version == 0)

         {

                   // So simple updates (Add stuff, no remove or migrate)

                   updater.AddColumn<Game>("IsPublished");   

                   updater.DatabaseSchemaVersion = 1;

                   updater.Execute();

         }

         else if (version == 1

         {

                   // So simple updates (Add stuff, no remove or migrate)

                   updater.AddIndex<Game>("NameIndex");

                   updater.DatabaseSchemaVersion = 2;

                   updater.Execute();

         }

         你可以看到,对于第一个更新,版本增加了。然后当应用程序成熟,它添加了一个新的更新。这是数据库版本的核心用法。

         所支持的四个不同的更新如下:

         updater.AddTable<Genre>();

         updater.AddColumn<Game>("IsPublished");

         updater.AddIndex<Game>("NameIndex");

         updater.AddAssociation<Game>("Genre");

         当添加一个表,整个表被添加(包括所有列,关系和索引)。这意味着,当你添加一个表,你不需要特殊的枚举所有的列,索引和关系。添加一列增加了一个指定的新列。任何新的列必须是nullable,因为没有办法来指定迁移到非空列。添加一个索引是基于索引的名称。最后,关系被添加,并且关系是基于属性的,它包含Association特性(attribute)。

复杂的模式变化

         如果你需要进行的架构更改对于DatabaseSchemaUpdater类来完成太复杂,你需要做艰苦工作创建一个新的数据库,以及手动传输和迁移数据。没有完这项工作的捷径。

数据库安全

         虽然你是唯一可以访问数据库的人,数据库是包含在应用程序目录中或者在隔离存储里,有时你可能需要增加数据库的安全性,提高数据库本身的安全级别。

         两个主要的方式来保护你的数据库,添加一个访问数据库的密码和启用加密。当创建一个Windows Phone OS 7.1应用程序,你可以同时使用这两种方式。你可以直接在数据库连接字符串中指定一个数据库的密码。这通常是在DataContext类中指定:

         public class AppContext : DataContext

         {

                   public AppContext()

                            : base("DataSource=isostore:/Games.sdf;Password=P@ssw0rd!;")

                   {

                   }

                   public Table<Game> Games;

                   public Table<Publisher> Publishers;

         }

         当你指定一个密码在你创建数据库之前时,该数据库将是密码保护的以及加密的。你不能为已经创建了的数据库添加一个密码或加密。如果你决定在你的应用程序部署后添加一个密码(和加密),你将需要创建一个新数据库并手动迁移所有数据。

我们在哪了?

         当你建立一个Windows Phone应用程序时,以一个明智的方式类处理数据对于你的应用程序成功是非常重要的。通过了解隔离存储和支持本地数据库两者的一些基本知识,你能够以最有效的方法,存储你想要存储的各种类型数据。

         再次重申,使用数据库引擎最大的好处是支持以一种有效的方式查询底层的数据。如果你不需要查询支持,你就会发现仅使用隔离存储和序列化来保存你的数据总体上更为有效率。在这里可以作出正确的决定是一个快速,平滑的应用程序,还是一个缓慢的,昏昏沉沉的混乱的应用程序之间的差异。

posted @ 2012-08-28 17:34  newetmscontact  阅读(452)  评论(0编辑  收藏  举报