Our calendar application represents an appointment domain as:
Appointment
- ID (PK)
- StartDateTime
- EndDateTime
- ...
AppointmentRole
- AppointmentID (FK)
- PersonOrGroupID (FK) /* joins to a person/group, outside the scope of this question */
- Role
- ...
Appointment has a 1 to many relationship with AppointmentRoles. Each AppointmentRole represents a person or group in a particular role (e.g., drop-off, pick-up, attend, ...).
The relationship serves two purposes:
- it defines an access control list - an authenticated principal can only view an appointment if their access control list matches an associated person or group
- it documents who is attending the appointment and in what roles.
There is also a third table to keep track of notes/comments associated with the appointment. It's on the many side of a 1 to many relationship with Appointment:
AppointmentNote
- AppointmentID (FK)
- ...
To display a calendar of appointments, we currently use something like...
List<IAppointment> GetAppointments(IAccess acl, DateTime start, DateTime end, ...
{
// Retrieve distinct appointments that are visible to the acl
var visible = (from appt in dc.Appointments
where !(appt.StartDateTime >= end || appt.EndDateTime <= start)
join role in
(from r in dc.Roles
where acl.ToIds().Contains(r.PersonOrGroupID)
select new { r.AppointmentID })
on appt.ID equals role.AppointmentID
select new
{
...
}).Distinct();
...
The visible Linq expression selects the distinct appointments that can be seen by the given access control list.
Below, we take visible and join/into roles and notes to pick up all the people and groups involved with an appointment and the appointment notes.
...
// Join/into to get all appointment roles and notes
var q = from appt in visible
orderby appt.StartDateTime, ...
join r in dc.Roles
on appt.ID equals r.AppointmentID
into roles
join note in dc.AppointmentNotes
on appt.ID equals note.AppointmentID
into notes
select new { Appointment = appt, Roles = roles, Notes = notes };
Finally, we enumerate the query, hoping that Linq-To-Sql will generate one fabulously optimized query (no such luck as discussed later)...
// Marshal the anonymous type into an IAppointment
// IAppointment has a Roles and Notes collection
var result = new List<IAppointment>();
foreach (var record in q)
{
IAppointment a = new Appointment();
a.StartDateTime = record.StartDateTime;
...
a.Roles = Marshal(record.Roles);
a.Notes = Marshal(record.Notes);
result.Add(a);
}
The query produced by Linq-to-Sql is very chatty. It generates a single query to determine the visible appointments. But then it generates three queries on each iteration: one to pick up the appointment fields, a second to pick up the roles and a third to pick up the notes. The where clause is always the visible appointment id.
So, we're refactoring GetAppointments and thought we could benefit from the SO community's expertise.
We expect to move everything into a T-SQL stored proc so we have more control. Can you share your thoughts on how you would tackle this problem? Changes to the data model, T-SQL and Linq-to-SQL modifications are all fair game. We would also like advice on indexes. We're using MS-SqlServer 2008 and .NET 4.0.