Wybierz wiele rekordów na podstawie listy identyfikatorów za pomocą linq

122

Mam listę zawierającą identyfikatory mojego UserProfilestołu. Jak mogę wybrać wszystko UserProfilesna podstawie listy identyfikatorów, które mam w varużyciu LINQ?

var idList = new int[1, 2, 3, 4, 5];
var userProfiles = _dataContext.UserProfile.Where(......);

Utknąłem tutaj. Mogę to zrobić używając pętli for itp. Ale wolałbym to zrobić z LINQ.

Yustme
źródło
4
wyszukiwanie i znajdowanie to dwie różne rzeczy. Ale skoro możesz spojrzeć przez ramię przez internet, czy możesz mi powiedzieć, skąd wiesz, że nie szukałem? czekaj, nie mów! Widziałeś to dobrze? dokładnie o co mi chodzi.
Yustme
5
zadanie pytania kosztuje więcej czasu niż wyszukiwanie. następnym razem załóżmy, że „on / ona” przeprowadził wyszukiwanie lub 10.
Yustme
2
To wciąż przyciąga sporo uwagi, więc pomyślałem, że wspomnę, że ReSharper bardzo dobrze radzi sobie z sugerowaniem miejsc, w których można zamienić kod iteracyjny w instrukcje LINQ. Dla osób nowych w LINQ może to być niezbędne narzędzie do tego celu.
Fuj,

Odpowiedzi:

207

Możesz użyć Contains()do tego. Będzie to trochę cofnięte, gdy naprawdę próbujesz stworzyć INklauzulę, ale powinno to wystarczyć:

var userProfiles = _dataContext.UserProfile
                               .Where(t => idList.Contains(t.Id));

Zakładam też, że każdy UserProfilerekord będzie miał int Idpole. Jeśli tak nie jest, musisz odpowiednio dostosować.

Fuj
źródło
Cześć, tak, rekordy profili użytkowników zawierają identyfikatory. Więc jakoś zrobiłbym coś takiego jak t => t.id == idList.Contains (id)?
Yustme
Contains()obsłuży to sprawdzenie równości dla każdej idwartości, jeśli użyjesz jej tak, jak napisałem w odpowiedzi. Nie musisz ==nigdzie jawnie pisać, gdy próbujesz porównać elementy jednego zestawu (tablicy) z innym (tabela bazy danych).
Fuj
Cóż, problem polega na tym, że t przechowuje cały obiekt UserProfile, a idList zawiera tylko int. Kompilator narzekał na coś, ale udało mi się to naprawić. Dzięki.
Yustme
2
@ Yuck - nie działa dla mnie, mówi, że upłynął limit czasu funkcji! Wyłączyłem leniwe ładowanie, ale nadal nie działa.
bhuvin
1
Otrzymuję komunikat „Nie można przekonwertować wyrażenia lambda na typ„ int ”, ponieważ nie jest to typ delegata”. Jak to naprawić?
Stian
92

Rozwiązanie z .Where i .Contains ma złożoność O (N kwadrat). Prosty .Join powinien mieć dużo lepszą wydajność (blisko O (N) ze względu na haszowanie). Więc poprawny kod to:

_dataContext.UserProfile.Join(idList, up => up.ID, id => id, (up, id) => up);

A teraz wynik mojego pomiaru. Wygenerowałem 100 000 profili użytkowników i 100 000 identyfikatorów. Dołączenie zajęło 32 ms, a gdzie z .Contains zajęło 2 minuty i 19 sekund! Użyłem czystego IEnumerable do tego testu, aby udowodnić moje stwierdzenie. Jeśli używasz listy zamiast IEnumerable, .Where i .Contains będą działać szybciej. Zresztą różnica jest znacząca. Najszybszy .Gdzie .Contains to Set <>. Wszystko zależy od złożoności bazowych kolekcji dla .Contains. Spójrz na ten post, aby dowiedzieć się o złożoności linq Spójrz na moją próbkę testową poniżej:

    private static void Main(string[] args)
    {
        var userProfiles = GenerateUserProfiles();
        var idList = GenerateIds();
        var stopWatch = new Stopwatch();
        stopWatch.Start();
        userProfiles.Join(idList, up => up.ID, id => id, (up, id) => up).ToArray();
        Console.WriteLine("Elapsed .Join time: {0}", stopWatch.Elapsed);
        stopWatch.Restart();
        userProfiles.Where(up => idList.Contains(up.ID)).ToArray();
        Console.WriteLine("Elapsed .Where .Contains time: {0}", stopWatch.Elapsed);
        Console.ReadLine();
    }

    private static IEnumerable<int> GenerateIds()
    {
       // var result = new List<int>();
        for (int i = 100000; i > 0; i--)
        {
            yield return i;
        }
    }

    private static IEnumerable<UserProfile> GenerateUserProfiles()
    {
        for (int i = 0; i < 100000; i++)
        {
            yield return new UserProfile {ID = i};
        }
    }

Wyjście konsoli:

Upłynęło. Czas dołączenia: 00: 00: 00,0322546

Upłynęło .Gdzie .Zawiera czas: 00: 02: 19.4072107

David Gregor
źródło
4
Czy możesz to poprzeć liczbami?
Yustme
Fajnie, jednak ciekawi mnie, jakie byłyby czasy, kiedy Listjest używany. +1
Yustme
Ok, oto czasy, którymi jesteś zainteresowany: Lista zajęła 13,1 sekundy, a HashSet 0,7 ms! Więc .Where .Contains jest najlepsze tylko w przypadku HashSet (gdy .Contains ma złożoność O (1)). W innych przypadkach .Join jest lepszy
David Gregor
5
Local sequence cannot be used in LINQ to SQL implementations of query operators except the Contains operator.Podczas korzystania z kontekstu danych LINQ2SQL pojawia się błąd.
Mayank Raichura
3
@Yustme - zawsze liczy się wydajność . (Nienawidzę być facetem "to powinna być akceptowana odpowiedź", ale ...)
jleach
19

Ładne odpowiedzi powyżej, ale nie zapominaj o jednej WAŻNE - dają różne wyniki!

  var idList = new int[1, 2, 2, 2, 2]; // same user is selected 4 times
  var userProfiles = _dataContext.UserProfile.Where(e => idList.Contains(e)).ToList();

Spowoduje to zwrócenie 2 wierszy z bazy danych (i może to być poprawne, jeśli chcesz mieć tylko odrębną posortowaną listę użytkowników)

ALE w wielu przypadkach możesz potrzebować niesortowanej listy wyników. Zawsze musisz myśleć o tym jak o zapytaniu SQL. Zobacz przykład z koszykiem sklepu internetowego, aby zilustrować, co się dzieje:

  var priceListIDs = new int[1, 2, 2, 2, 2]; // user has bought 4 times item ID 2
  var shoppingCart = _dataContext.ShoppingCart
                     .Join(priceListIDs, sc => sc.PriceListID, pli => pli, (sc, pli) => sc)
                     .ToList();

To zwróci 5 wyników z DB. W tym przypadku użycie atrybutu „zawiera” byłoby niewłaściwe.

Tomino
źródło
13

To powinno być proste. Spróbuj tego:

var idList = new int[1, 2, 3, 4, 5];
var userProfiles = _dataContext.UserProfile.Where(e => idList.Contains(e));
Fabian Bigler
źródło