It's not unusual to have many-to-many relationships in an application. A recent project I worked on had multiple service technicians assigned to multiple locations, for instance. It's also not unusual for there to be data that applies only to the relationship. In my client's project, for example, at any location, a service technician has a specific position -- and that position can be different from one location to another. (For instance, at one location, the tech might be the "responsible authority"; at another location, the tech might be the "support technician.") In addition, at each location, a tech is assigned a phone extension number where he can be reached.
In this column, I'll work through this problem using Entity Framework Code First development, LINQ and (for the UI) ASP.NET MVC. By the end of the column you'll have a roadmap for handling many-to-many relationships in your applications.
The Data Design
My client for this project was a "Code First all the way" shop. My first step was to create the class that defines the many-to-many relationship between techs and locations (the Location, Job and ServiceTech classes were already defined):
My client for this project was a "Code First all the way" shop. My first step was to create the class that defines the many-to-many relationship between techs and locations (the Location, Job and ServiceTech classes were already defined):
<Table("TechsLocations")> _ Public Class TechsLocations Public Property TechId As Integer Public Property LocationId As Integer Public Overridable Property Tech As Tech Public Overridable Property Location As Location Public Overridable Property Job As Job Public Property Ext As String End Class
This class establishes that TechsLocations has navigation collections between the two entities in the many-to-many relationship (Tech and Location); it also has a navigation collection with the entity with which it has a one-to-many relationship (Job). Finally, the class gives the TechsLocation entity a property to hold the phone extension number (Ext) for the tech at that location.
With all the entities defined, the next step is to add them to the DataContext:
Public Class DataContext Inherits DbContext Public Property Techs As DbSet(Of Tech) Public Property Locations As DbSet(Of Location) Public Property Jobs As DbSet(Of Job) Public Property TechsLocations As DbSet(Of TechsLocations)
In the OnModelCreating method, I turn off cascading deletes (I don't want to delete a location and find all my service techs deleted):
Protected Overrides Sub OnModelCreating(modelBuilder As DbModelBuilder) modelBuilder.Conventions.Remove(Of OneToManyCascadeDeleteConvention)()
Still in the OnModelCreating method, I establish the relationships between these entities. I begin by defining the composite key (TechId and LocationId) for my TechsLocations entity:
modelBuilder.Entity(Of TechsLocations). HasKey(Function(tl) New With {tl.TechId, tl.LocationId})
Next, I tell Entity Framework that a tech can have many TechsLocations, that there can't be a TechsLocations without a tech, and that the relationship between Techs and TechsLocations is built on the TechId property in the TechsLocations entity:
modelBuilder.Entity(Of Tech). HasMany(Function(t) t.TechsLocations). WithRequired(Function(tl) tl.Tech). HasForeignKey(Function(tl) tl.TechId)
I also need to do the same thing on the Location side of the relationship:
modelBuilder.Entity(Of Location). HasMany(Function(l) l.TechsLocations). WithRequired(Function(tl) tl.Location). HasForeignKey(Function(tl) tl.LocationId)
And, of course, I need to specify that TechsLocations has a one-to-many relationship with the Job table. That's easier to define:
modelBuilder.Entity(Of TechsLocations). HasRequired(Function(tl) tl.Job). WithMany.Map(Sub(tl) tl.MapKey("JobId"))
Adding, Deleting and Updating the Relationship
Now, to add a TechsLocations relationship, I first create (or retrieve) a Location and a Job:
Now, to add a TechsLocations relationship, I first create (or retrieve) a Location and a Job:
Using dc = New DataContext Dim lcn As New Location() With {.Name = "MainSite"} dc.Locations.Add(lcn) Dim jb As New Job() With {.Title = "Responsible Authority"} dc.Jobs.Add(jb) dc.SaveChanges
Then I create (or retrieve) a Tech and create a TechsLocations object that will tie the Tech to the Location. Because I'm adding this TechsLocations object to the Tech object, I don't need to set the TechsLocations object's TechId property (Entity Framework will take care of that). I do, however, need to set the other properties (Location, extension and Job) for TechsLocations:
Dim tch As Tech Dim tch = As New Tech() With {.TechName = "Peter Vogel"} Dim tl As TechsLocations tl = New TechsLocations With {.Location = lcn, .Ext = "1234", .Job = jb})
After creating the TechsLocations object, I initialize the Tech's TechsLocations collection, add my TechsLocations object to the collection, add my Tech object to the data context's Techs collection and save my changes:
tch.TechsLocations = New List(Of TechsLocations) tch.TechsLocations.Add(tl) dc.Techs.Add(tch) dc.SaveChanges
Deleting a Tech now requires two steps: first, delete the TechsLocations entities for the Tech and, after that, delete the Tech. The code to delete the Tech I just created looks like this:
Dim tls = From tl In dc.TechsLocations Where tl.TechId = tch.ID Select tl For Each tl In tls dc.TechsLocations.Remove(tl) Next dc.SaveChanges() dc.Techs.Remove(tch)
Updating requires more complex code because, before doing any updates, I want to check to see if the tech's assignments have been changed. So the first thing I do is convert all of the tech's existing assignments into TechsLocationsDTO objects, like this:
Dim tchTls = (From tl In dc.TechsLocations Where tl.TechId = tch.ID Select New TechsLocationsDTO With { .Selected = True, .Location = tl.Location, .LocationId = tl.LocationId, .Phone_Extension = tl.Ext, .Job = tl.Job, .JobId = tl.Job.ID}).ToList
The reason I convert my TechsLocations entities into TechsLocationsDTO objects is that I'm making an assumption: My code is passed not only a Tech object representing the current tech, but also a list of TechsLocationsDTO objects. Each of those TechsLocationsDTO objects represents a location (with associated information: job and extension) to which a tech could be assigned. If the tech has, in fact, been assigned to a location, the TechsLocationsDTO object's Selected property is set to True (in the next section I'll describe where that collection comes from in ASP.NET MVC). From the list of TechsLocationsDTO objects passed to me, I extract the locations to which the tech has been assigned:
Dim sTls = (From t In TechsLocationsDTOs Where t.Selected = True Order By t.LocationId Select t).ToList
Because I now have two collections of TechsCollectionsDTO objects -- and because my TechsCollectionsDTO implements the IComparable interface (as shown in Listing 1) -- I can use the SequenceEqual method to see if the two collections have the same objects.
Listing 1. Implementing IComparable in a DTO Public Class TechsLocationsDTO Implements IComparable(Of TechsLocationsDTO) Public Property Selected As Boolean Public Property Location As Location Public Property LocationId As Integer? Public Property Ext As String Public Property Job As Job Public Property JobId As Integer? Public Property JobsList As List(Of SelectListItem) Public Function CompareTo(other As TechsLocationsDTO) As Integer _ Implements IComparable(Of TechsLocationsDTO).CompareTo If (TextId - other.TechId) <> 0 Then Return TechID - TechId ElseIf (JobId - other.JobId) <> 0 Then Return JobID - JobId ElseIf (LocationId - other.LocationId) <> 0 Then Return PositionId - other.PositionId ElseIf (Ext.CompareTo(other.Ext)) <> 0 Then Return Ext.CompareTo(other.Ext) Else Return 0 End If End Function End Class
If SequenceEqual returns False, it indicates that some objects are either missing or changed between the objects. In that case, I throw away all the tech's current TechsLocations, create new ones from the selected TechsLocationsDTOs collection I received earlier, add the new TechsLocations objects to my tech object, and (eventually) save the changes to the database, as shown in Listing 2.
Listing 2. Working with the TechsLocationsDTOs collection. If Not sTls.SequenceEqual(tchTls) Then tch.TechsLocations.Clear() Dim tchLcn As TechsLocations For Each tchLcn In sTls tchLcn = New TechsLocations tchLcn.user = tch tchLcn.LocationId = tchLcn.LocationId tchLcn.Job = dc.Jobs.Single(Function(j) j.ID = tchLcn.JobId) tchLcn.Ext = lcn.Ext tch.TechsLocations.Add(tchLcn) Next End If dc.SaveChanges()
Getting Data to the UI
This leads to the obvious question: Where does that TechsLocationsDTOs collection come from? Up until now, everything I've discussed would work in any of the environments built on ASP.NET (Web Forms, Web API, MVC). I'd assume that in Web Forms, users were given a list of TechsLocations in a GridView that let the user select the locations he wanted; in the Web API I'd assume that you'd require the client to send the collection as part of calling whatever method in which this code goes. But supporting this collection in ASP.NET MVC requires some explanation.
This leads to the obvious question: Where does that TechsLocationsDTOs collection come from? Up until now, everything I've discussed would work in any of the environments built on ASP.NET (Web Forms, Web API, MVC). I'd assume that in Web Forms, users were given a list of TechsLocations in a GridView that let the user select the locations he wanted; in the Web API I'd assume that you'd require the client to send the collection as part of calling whatever method in which this code goes. But supporting this collection in ASP.NET MVC requires some explanation.
As its name implies, the TechsLocationsDTO class is a data transfer object (DTO) for moving necessary data to the view. It's just a collection of properties -- one of which is a list of Jobs so that, in the view, I can limit the user to selecting from a dropdown list when assigning a tech to a job at a location.
For a View that lets users update the TechsLocations for a service technician, I begin by building two collections: all the Locations a tech can be assigned to and all the Locations assigned to the current tech:
No comments:
Post a Comment