3. Local Databases
When you are building an application that
needs data that must be queried and support smart updating, a local
database is the best way to accomplish that. Windows Phone supports
databases that exist directly on the phone. When
building a Windows Phone application, you won’t have access to the
database directly; instead you can use a variant of LINQ to SQL married
to a code-first approach to build a database to accomplish your database
access. Let’s walk through the meat of the functionality.
3. Getting Started
To get started, you need a database file.
Under the covers the database is SQL Server Compact Edition (SQL CE), so
you could just create an .sdf file for your project, but usually you
start by telling the database APIs to create the database for you.
The first step is to have a class (or several) that represents the data you want to store. You can start with a simple class:
public class Game
{
public string Name { get; set; }
public DateTime? ReleaseDate { get; set; }
public double? Price { get; set; }
}
This class holds some piece of data you want to
be able to store in a database. Before you can store it in the database,
you have to add attributes to tell LINQ to SQL that this describes a
table:
[Table]
public class Game
{
[Column]
public string Name { get; set; }
[Column]
public DateTime? ReleaseDate { get; set; }
[Column]
public double? Price { get; set; }
}
By using these attributes, you are creating a
class that represents the storage for a table in the database. Some of
the column information is inferred (such as nullability in the ReleaseDate
column). With this definition, you can read from the database, but
before you can add or change data, you need to define a primary key:
[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; }
}
As you can see, the Column
attribute has several properties that can be set to specify information about each column. In this case, the Column
attribute specifies that the Id
column is the primary key and that the key should be generated by the
database. To support change tracking and writing to the database, you
must have a primary key.
Like any other database engine, SQL CE enables
you to improve query performance by adding your own indexes. You can do
this by adding the Index
attribute to the table classes:
[Table]
[Index(Name = "NameIndex", Columns = "Name", IsUnique = true)]
public class Game
{
// ...
}
The Index
attribute enables you to specify a name, a string containing the column
names to be indexed, and optionally whether the index should also be a
unique constraint. This attribute is used when you create or update the
database. You can also specify an index on multiple columns by
separating the column names with a comma:
[Table]
[Index(Name = "NameIndex", Columns = "Name", IsUnique = true)]
[Index(Columns = "ReleaseDate,IsPublished")]
public class Game : INotifyPropertyChanging, INotifyPropertyChanged
{
// ...
}
At this point, you have defined a simple table
with two indexes and can move on to creating a data context class. This
class will be your entry point to the database itself. It is a class
that derives from the DataContext
class, as shown here:
public class AppContext : DataContext
{
}
This class is responsible for exposing access to
the database as well as handling change management. You will expose
your “tables” as a public field:
public class AppContext : DataContext
{
public Table<Game> Games;
}
The generic class wraps your table class to
represent a queryable set of those objects. This way, your context class
will not only give you access to the objects stored in the database,
but also track them for you. The base class (DataContext
) is where most of the magic happens. Because the DataContext
class does not have an empty constructor, you’ll also need to implement a constructor:
public class AppContext : DataContext
{
public AppContext()
: base("DataSource=isostore:/myapp.sdf;")
{
}
public Table<Game> Games;
}
The typical call to the base class’s constructor
requires that you send it a connection string. For the phone, all this
connection string requires is a description of where the database exists
or where it will be created. You specify this by specifying a URI to
where the database file belongs. The URI is a path to the file from
either the local folder or the application folder. To specify a file to
exist (or be created) in the local folder, you use the isostore
moniker like so:
isostore:/myapp.sdf
For a database that ships with your application (and will be delivered in the .xap file), you can use the appdata
moniker as well. If you want to access data that resides in the
application folder, you will be able to only read the database, not
write to it. To specify the location of the database in the
application folder, you can use the appdata
moniker just like the isostore
moniker:
appdata:/myapp.sdf
The end of the URI should be a path and a file
name to the actual file. The underlying database is SQL CE, so the file
is an .sdf file. If you want your database file to be within a
subfolder, you can specify it in the URI with the folder name, like so:
isostore:/data/myapp.sdf
After you have created your data context class, you can create the database by calling the CreateDatabase
method (as well as checking whether it exists by calling DatabaseExists
):
// Create the Context
var ctx = new AppContext();
// Create the Database if it doesn't exist
if (!ctx.DatabaseExists())
{
ctx.CreateDatabase();
}
The context’s table members allow you to perform CRUD on the underlying data. For example, to create a new Game
object in the database, you would just create an instance and add it to the Games
member:
// 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();
The new Game
object can be passed to the Games
member of the context through the InsertOnSubmit
method to tell the context to save this the next time changes are submitted to the database. The SubmitChanges
method will take any changes that have occurred since the creation of the context
object (or since the last call to SubmitChanges
) and batch them to the underlying database. Note that the new instance of Game
didn’t set the Id
property. This is unnecessary because the Id
property is marked not only as the primary key (which is required to
support writing to the database), but also as database-generated. This
means that when SubmitChanges
is called, it will let the database generate the Id
and update your object’s ID to the database-generated one.
Querying the Games
stored in the
database takes the form of LINQ queries. So, if you have created some
data in the database, you can query it like this:
var qry = from g in ctx.Games
where g.Price >= 49.99
order by g.Name
select g;
var results = qry.ToList();
This query will return a set of Game
objects with the data directly from the database. This LINQ query is
translated into a parameterized SQL query and executed against the local
database for you when this code calls the ToList
method.
What might not be obvious is that if you change
these objects, the context class tracks those changes for you. So if you
change some data, calling the context’s SubmitChanges
method updates the database as well:
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();
In addition, you can delete individual items using the table members on the context class by calling DeleteOnSubmit
like so:
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();
You do need to retrieve the entities to delete
them (unlike the full version of LINQ to SQL where you could execute
arbitrary SQL). You can submit a deletion by calling DeleteAllOnSubmit
and supplying a query:
var qry = from g in ctx.Games
where g.Price > 100
select g;
ctx.Games.DeleteAllOnSubmit(qry);
ctx.SubmitChanges();
The query in this example defines the items to
be deleted in the database. It does not retrieve them in this place; it
uses the query to define which items are to be deleted. After all the
items are marked for deletion, the call to SubmitChanges
causes the deletion to happen (as well as any other changes detected).
By creating your table classes and a context
class, you can access the database and perform all the necessary queries
and changes to the database. Next let’s look at additional database
features you will probably want to consider as part of your phone
application.