2010-01-18 11 views
36

He heredado una base de datos que no fue diseñada de manera óptima, y ​​necesito manipular algunos datos. Permítanme dar una analogía más común de la clase de cosas que tengo que hacer:¿Cómo hacer una unión externa completa en Linq?

Digamos que tenemos una tabla Student, un mantenimiento de registros StudentClass tabla de todas las clases a las que asistió, y una mesa de StudentTeacher que almacena todos los maestros quien enseñó a este estudiante Sí, sé que es un diseño tonto y tendría más sentido almacenar al profesor en la mesa de clase, pero con eso estamos trabajando.

Ahora quiero limpiar los datos, y quiero encontrar todos los lugares donde un estudiante tiene un profesor pero no hay clases, o una clase pero no hay profesores. SQL así:

select * 
from StudentClass sc 
full outer join StudentTeacher st on st.StudentID = sc.StudentID 
where st.id is null or sc.id is null 

¿Cómo se hace eso en Linq?

+11

Nota: esto no es realmente una combinación externa completa, ya que desea excluir las filas donde la unión interna tuvo éxito. Solo menciono esto ya que este es un resultado de búsqueda superior para 'full outer join linq' - entonces si eso es lo que alguien está buscando, entonces las respuestas pueden no ser correctas –

+1

podría ser una solución de alto rendimiento, refiérase a la pregunta a continuación. http: // stackoverflow.com/questions/5489987/linq-full-outer-join/8501701 # 8501701 – vrluckyin

Respuesta

28

creo que tengo la respuesta aquí, que no es tan elegante como yo esperaba, pero debe hacer el truco:

var studentIDs = StudentClasses.Select(sc => sc.StudentID) 
    .Union(StudentTeachers.Select(st => st.StudentID); 
    //.Distinct(); -- Distinct not necessary after Union 
var q = 
    from id in studentIDs 
    join sc in StudentClasses on id equals sc.StudentID into jsc 
    from sc in jsc.DefaultIfEmpty() 
    join st in StudentTeachers on id equals st.StudentID into jst 
    from st in jst.DefaultIfEmpty() 
    where st == null^sc == null 
    select new { sc, st }; 

que probablemente podría exprimir estas dos declaraciones en una sola, pero creo que' d claridad de código de sacrificio.

+3

Establecer uniones automáticamente hace las cosas diferentes http://en.wikipedia.org/wiki/Union_(set_theory) –

+0

@VisionarySoftwareSolutions correcto, usted responde - editó –

+0

¡Muchas gracias! –

1

Un comienzo ...

var q = from sc in StudentClass 
      join st in StudentTeachers on sc.StudentID equals st.StudentID into g 
      from st in g.DefaultIfEmpty() 
      select new {StudentID = sc.StudentID, StudentIDParent = st == null ? "(no StudentTeacher)" : st.StudentID...........}; 

Ver también http://www.linqpad.net/ para más muestras buena herramienta para jugar con

+0

Creativo, pero no tan elegante como esperaba. Le daré +1 por el enlace a LinqPad, que parece una buena pieza de software. :) –

+0

;-)) Usted tiene ejemplos más elegantes en LinqPad Tiene una conexión db genial y tiene posibilidades de vincular en su dll: s etc ... El autor también ha escrito el mejor libro C# en pocas palabras http://www.youtube.com/watch?v=Z6-iUNfJsJw&feature=channel – salgo60

+3

Dos puntos a tener en cuenta: 1) Esto genera una UNIÓN EXTERIOR IZQUIERDA no una combinación externa completa y 2) la comprobación st == nulo no es requerido en linq-to-sql, en su lugar puede hacer st.StudentID ?? "(no StudentTeacher)" –

18

de los dados 2 colecciones un y b, una combinación externa completa requerida unirse podría ser el siguiente:

a.Union(b).Except(a.Intersect(b)); 

Si a y B no son del mismo tipo, a continuación, 2 separada externa izquierda se une se requieren:

var studentsWithoutTeachers = 
    from sc in studentClasses 
    join st in studentTeachers on sc.StudentId equals st.StudentId into g 
    from st in g.DefaultIfEmpty() 
    where st == null 
    select sc; 
var teachersWithoutStudents = 
    from st in studentTeachers 
    join sc in studentClasses on st.StudentId equals sc.StudentId into g 
    from sc in g.DefaultIfEmpty() 
    where sc == null 
    select st; 

aquí es una opción de una línea usando concat():

(from l in left 
join r in right on l.Id equals r.Id into g 
from r in g.DefaultIfEmpty() 
where r == null 
select new {l, r}) 
    .Concat(
    from r in right 
    join sc in left on r.Id equals sc.Id into g 
    from l in g.DefaultIfEmpty() 
    where l == null 
    select new {l, r}); 
+0

Es una buena declaración semántica del problema, pero no ayuda, porque para que eso funcione en Linq, a y b tienen que ser del mismo tipo, que no es el caso aquí. –

+0

Esto es INCORRECTO. La declaración de combinación externa correcta está disponible aquí: http://msdn.microsoft.com/en-us/library/vstudio/bb397895.aspx. La verificación nula no es necesaria y debe introducirse otra variable para seleccionar del grupo –

+1

@grzegorz_p. El ejemplo de msdn muestra la combinación externa IZQUIERDA. La pregunta era acerca de FULL outer join –

17
método

Extensión:

public static IEnumerable<TResult> FullOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,TInner,TResult> resultSelector) 
       where TInner : class 
       where TOuter : class 
      { 
       var innerLookup = inner.ToLookup(innerKeySelector); 
       var outerLookup = outer.ToLookup(outerKeySelector); 

       var innerJoinItems = inner 
        .Where(innerItem => !outerLookup.Contains(innerKeySelector(innerItem))) 
        .Select(innerItem => resultSelector(null, innerItem)); 

       return outer 
        .SelectMany(outerItem => 
         { 
          var innerItems = innerLookup[outerKeySelector(outerItem)]; 

          return innerItems.Any() ? innerItems : new TInner[] { null }; 
         }, resultSelector) 
        .Concat(innerJoinItems); 
      } 

prueba:

[Test] 
public void CanDoFullOuterJoin() 
{ 
    var list1 = new[] {"A", "B"}; 
    var list2 = new[] { "B", "C" }; 

    list1.FullOuterJoin(list2, x => x, x => x, (x1, x2) => (x1 ?? "") + (x2 ?? "")) 
     .ShouldCollectionEqual(new [] { "A", "BB", "C"}); 
} 
+1

+1 para el concepto del método de extensión! Tengo la sensación de que podría ser optimizado internamente, pero una buena respuesta, no obstante. –

1

Sobre la base de la respuesta de Shaul, pero con un poco de racionalización:

var q = 
    from id in studentIDs 
    join sc in StudentClasses on id equals sc.StudentID into jsc 
    join st in StudentTeachers on id equals st.StudentID into jst 
    where jst.Any()^jsc.Any() //exclusive OR, so one must be empty 

    //this will return the group with the student's teachers, and an empty group 
    // for the student's classes - 
    // or group of classes, and empty group of teachers 
    select new { classes = jsc, teachers = jst }; 

    //or, if you know that the non-empty group will always have only one element: 
    select new { class = jsc.DefaultIfEmpty(), teacher = jst.DefaultIfEmpty() }; 

Tenga en cuenta que para una combinación externa completa, esta puede funcionar, también Deje fuera la cláusula where y use la primera select anterior, en lugar de la segunda.

Cuestiones relacionadas