c# - ¿Cómo hacer una combinación externa completa en Linq?
linq-to-sql outer-join (5)
Basado en la respuesta de Shaul, pero con una pequeña 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 unión externa completa, esto también puede funcionar. Deje fuera la cláusula where
y use la primera select
anterior, en lugar de la segunda.
Heredé 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 del tipo de cosas que tengo que hacer:
Digamos que tenemos una tabla de Student
, una tabla de StudentClass
guarda el registro de todas las clases a las que asistió, y una tabla de Profesor StudentTeacher
que almacena a todos los maestros que enseñaron a este alumno. 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 maestro pero no clases, o una clase pero no maestros. 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 haces eso en Linq?
Creo que tengo la respuesta aquí, que no es tan elegante como esperaba, pero debería ser 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 };
Probablemente puedas exprimir estas dos declaraciones en una, pero creo que sacrificarías la claridad del código.
Método de 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"} );
}
Un inicio...
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...........};
Consulte también http://www.linqpad.net/ para obtener más ejemplos. Buena herramienta para jugar con
para las 2 colecciones a y b dadas, una unión externa completa requerida podría ser la siguiente:
a.Union(b).Except(a.Intersect(b));
Si a y b no son del mismo tipo, se requieren 2 uniones externas izquierdas separadas:
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í hay 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});