当你正在构建的应用程序需要数据,这些数据必须能够查询并支持智能更新,本地数据库是最好的方式来实现这一目标。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 RaisePropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
void RaisePropertyChanging(string propName)
{
if (PropertyChanging != null)
{
PropertyChanging(this,
new PropertyChangingEventArgs(propName));
}
}
}
实现接口需要添加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;
[Association(IsForeignKey = 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;
[Association(Storage = "_gameSet",
ThisKey = "Id",
OtherKey = "_publisherId")]
public EntitySet<Game> Games
{
get { return _gameSet; }
set
{
// Attach any assigned game collection to the collection
_gameSet.Assign(value);
}
}
}
这个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 AttachToGame(Game game)
{
RaisePropertyChanging("Game");
game.Publisher = this;
}
void DetachFromGame(Game 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;
}
当你指定一个密码在你创建数据库之前时,该数据库将是密码保护的以及加密的。你不能为已经创建了的数据库添加一个密码或加密。如果你决定在你的应用程序部署后添加一个密码(和加密),你将需要创建一个新数据库并手动迁移所有数据。