3.3 Associations
The data types for each property in the table
classes you have seen have been simple types. The types have been simple
because they need to be stored in the local database. To be stored in
the local database, they need to be convertible to database types (for
example, strings are stored as NVARCHARs). Even though you’re going to
be dealing with classes, you will still have to remember that it is a
relational database underneath the covers. So when you need more
structure, you will need associated tables (or associations).
For instance, let’s assume we have a second table class that holds information about the publisher of a game:
[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));
}
}
}
This new class is implemented just like the Game
class (because we want it to allow change management). To be able to
save it in the database, we need to expose it on our context class as
well as on a public field:
public class AppContext : DataContext
{
public AppContext()
: base("DataSource=isostore:/myapp.sdf;")
{
}
public Table<Game> Games;
public Table<Publisher> Publishers;
}
At this point you could create, edit, query, and delete both the Game
and Publisher
objects. But what you really want is to be able to relate the two objects to each other. That’s where associations come in.
To add an association, you need to start by having a column on the Game
class that represents the publisher’s primary key:
[Table]
public class Game : INotifyPropertyChanging, INotifyPropertyChanged
{
// ...
[Column]
internal int _publisherId;
}
This new column is used to hold the ID of the
related publisher for this particular game. The data is not public (it
is internal in this case) because users of this class won’t set this
value explicitly. Instead, you will create a nonpublic member that will
store an object called an EntityRef
. The EntityRef
class is a generic class that wraps a related entity:
[Table]
public class Game : INotifyPropertyChanging, INotifyPropertyChanged
{
// ...
[Column]
internal int _publisherId;
private EntityRef<Publisher> _publisher;
}
The EntityRef
class is important
here because it will also support lazy loading of the related entity so
that large object graphs aren’t loaded accidentally. But the real magic
of linking the column and the EntityRef
happens in the public
property for the related entity:
[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");
}
}
}
There is a lot going on in this property, so let’s take it one piece at a time. First, let’s look at the Association
attribute. This attribute has a number of parameters, but these are the basic ones to set. The IsForeignKey
parameter tells the association that this is a foreign key relationship. The Storage
parameter describes the name of the class’s member that holds the EntityRef
for this association. The ThisKey
and OtherKey
are the columns of the keys on each side of the association. ThisKey
refers to the name of the column on this class (Game
); OtherKey
refers to the column name on the other side of the association (Publisher
).
When someone accesses this property, you will return the entity from within the EntityRef
object as shown previously in the property getter.
Finally, the setter has a number of operations.
The first and last operations in the setter handle the change management
notification just like any column property on your table class. Then it
takes the value of the property and sets it to the Entity
inside the EntityRef
object. Finally, if the value being set is not null, it sets the
foreign key ID on the table class so that the column that represents the
foreign key is set.
By doing all of this, you can have a one-to-many
relationship between two table classes. But so far that association is
only one-way. To complete the association, you might want to have a
collection on the Publisher
table class that represents all the games by that publisher.
Adding the other side of the relationship is similar, but in this case you need an instance of a generic class called 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);
}
}
}
The EntitySet
class wraps around a collection of elements associated with a table class. In this case, the EntitySet
wraps around a collection of games that belong to a publisher. As in the other side of the association, specifying the Storage
, ThisKey
, and OtherKey
helps the context object figure out how the association is created. The only surprising thing is that when the setter on the Games
property is called, it attaches whatever games are assigned to it to the set of Games
. This is typically called by the context class only when executing a query.
Although not obvious, the construction of the _gameSet
field isn’t shown. This needs to be done in the constructor:
[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;
}
}
In the constructor you must create the EntitySet
.
Note that in the constructor, you will also pass in two actions that
handle attaching and detaching a game to and from the collection. The
purpose of these two actions is to ensure that the individual games that
are attached/detached also set or clear their association property. In
addition, raising the PropertyChanging
event helps the context object to be very efficient when the association is changing.