Zapytanie linq/ef z warunkiem w tabeli many-to-many

0

Witam,

Mam taki mały problem: (tutaj przyklad):
Mam klasę User (identityUser), w której mam relationship many-to-many do "friends'ów" (czyli tej samej klasy).

 // tutaj zmienne typu email, id etc pominąłem
 public ICollection<UserUser> FriendWith { get; set; }
 public ICollection<UserUser> FriendOf { get; set; }

oraz klase łączącą UserUser:

public class UserUser
    {
        public virtual User User { get; set; }
        public string UserId { get; set; }
        public virtual User Friend { get; set; }
        public string FriendId { get; set; }

        public bool Confirmed { get; set; }
    }

W EF modelBuilder mam:

 modelBuilder.Entity<UserUser>()
                .HasKey(k => new {k.UserId, k.FriendId});

            modelBuilder.Entity<UserUser>()
                .HasOne(pt => pt.User)
                .WithMany(p => p.FriendWith)
                .HasForeignKey(pt => pt.UserId)
                .OnDelete(DeleteBehavior.Restrict);

            modelBuilder.Entity<UserUser>()
                .HasOne(pt => pt.Friend)
                .WithMany(t => t.FriendOf)
                .HasForeignKey(pt => pt.FriendId)
                .OnDelete(DeleteBehavior.Restrict);

Bez problemu pobieram "friendsow" itp ale zaciąłem się kiedy chciałbym podać np email któregoś usera i otrzymać z powrotem listę wszystkich userów ale żebym mógł sprwadzić jaką ma wartość Confirmed dla danego usera w stosunku do ID zalogowanego usera (w many-to-many) i ustawic w zwracanym modelu. Czy da się to zrobić w jedym zapytaniu czy muszę to jakoś rozbić?
na chwilę obecną mam:

            var users = await _userManager.Users
                .Where(u => (u.Email.Contains(request.substring) || u.UserName.Contains(request.substring)) && u.Id != request.Id)
                .Select(user=> new UserInfo()
                {
                    // tutaj tworze model z danymi ktore potrzebuje
                    confirmed = isConfirmed // tutaj potrzebuję ustawic na true/false/null
                })
                .ToListAsync(cancellationToken);

Do pobierania User'a z firiend-listy używam:

.SelectMany(e => e.FriendWith, (user, friend) =>

Z góry dziękuję za pomoc.
Zaciąłem się na tym nie wiem dlaczego...

Edit:
Czy cos w stylu:

 .Include(s => s.FriendOf)

a później przy tworzeniu modelu:

.Select(user => new UserInfo()
{
      confirmed = user.FriendOf.FirstOrDefault(e => e.FriendId == request.Id).Confirmed
})

czy takie działania są poprawne??

Edit 2:
Działać->działa ale czy tak to się robi "profesjonalnie" czy może jest jakiś lepszy sposób?
Ostatecznie mam:

 var users = await _userManager.Users
     .Include(s => s.FriendWith)
     .Where(u => (u.Email.Contains(request.substring) || u.UserName.Contains(request.substring)) && u.Id != request.Id)
       .Select(user => new UserInfo()
           {
              confirmed = user.FriendWith.FirstOrDefault(e => e.FriendId == request.Id).Confirmed
           }).ToListAsync(cancellationToken);
0

Zainstaluj z nugeta Z.EntityFramework.Plus.EF6 i zobaczy przykłady z IncudeFilter
https://entityframework-plus.net/query-include-filter

0

Dzieki,
Ale w opisie jest :
Entity Framework Core:
Not supported yet.
Many to Many relation:
Not supported yet

Wiec mi sie nie przyda :/ (uzywam core i many-to-many).
Takie zapytanie mi ladnie dziala, tylko czy jest to "profesjonalne" rozwiazanie?...

var users = await _userManager.Users
     .Include(s => s.FriendWith)
     .Where(u => (u.Email.Contains(request.substring) || u.UserName.Contains(request.substring)) && u.Id != request.Id)
       .Select(user => new UserInfo()
           {
              confirmed = user.FriendWith.FirstOrDefault(e => e.FriendId == request.Id).Confirmed
           }).ToListAsync(cancellationToken);

W skrocie co chce osiagnac to Lista osob ktore moge zaprosic (wyszukiwanie):
Pobrac z back-endu liste userow ktorych email/username zawieraja ciag znakow (request.substring) i ustawic Confirmed na false/true/null w zaleznosci jaka jest/lub brak wartosci w UserUser.Confirmed (nullable bool?). Na froncie mam friend-liste ktora wyswietla element friend-list-item w zaleznosci od tego Confirmed. Jak jest null -> to wyswietla z buttonem Invite, jak jest true to nie dodaje do listy w ogóle (juz jest znajomym), a jak false to wyswietla z buttonem (cancel invitation request i/albo accept invitation -> w zaleznosci od requestSendByMe dolaczany do modelu). (pozniej bedzie dodane - pagination)

W miejscu:

.Where(u => (u.Email.Contains(request.substring) || u.UserName.Contains(request.substring)) && u.Id != request.Id)

moglbym dac u.Id != request.Id na poczatek ifa zeby nie musiec sprwadzac reszty

0

Tu nie będzie czasem n+ przez to, co jest w Select? Najlepiej odpal profielr i sprawdź.
Jeśli nie ma n+, to chyba i tak lepiej nie będzie. W końcu to EF.

0

No wlasnie o to co jest w select sie martwie najbardziej.
Pozniej jak bede przy kompie to zobacze profilerem.
A moze jakas alternatywa? Moze normalnie sql'em? bez EF'a ?

Do tego dochodzi jeszcze zapytanie czy user jest tym ktory dal invite... nie wiem czy nie za duzo tego na jeden "call".

0

Moze mi ktos pomoze,
@somekind
Moglbys mnie naprowadzic jak to najlepiej sprwadzic?

Uzylem Jetbrains dotTrace, jednak nie widze zadnych sql-calls, nie wiem dlaczego nie moge ich pokazac. (powinna byc mozliwosc wybrania jezeli jakies byly, ale pewnie robie cos zle)
Na focie nizej widac ze cala metoda Handle trwa 1.7ms z czego Linq to 0.4ms. (ale to przy kilku userach). 50% to system code.
Musze doczytac jak EF przerabia takie include a potem select na nim, moze nie robi kolejnego query do bazy danych?
trace.jpg

0

Chodziło mi o SQL profiler, żeby zobaczyć, czy czasem nie ma n+1 zapytań do bazy.

0

@somekind:
Moglbys spojzec i przeanalizowac? (naprawde sql'e itp to nie moja mocna strona).
Mi to wyglada na 1 zapytanie, ale ja to glupi jestem wiec lepiej zapytac kogos madrzejszego.

trace.jpg

Czy o to chodzilo?
Z gory dzieki.

0

Owszem, to jest jedno zapytanie. Jeśli jesteś pewien, że to wszystkie zapytania, które idą po sieci podczas wykonywania tego kodu, to jest dobrze.

0

Dzieki za pomoc.
Tak to jest tylko jedno query, robilem zapytanie postmanem do API i tylko to wyskoczylo.

No to moge kontynuowac.
Widze ze dobrze kombinowalem, jeszcze az tak glupi nie jestem :P

0

Napotkalem kolejna przeszkode.
Mianowicie w ktoryms momencie potrzebuje pobrac FriendsWith i FriendsOf jednego usera, przeiterowac po obydwuch i zwrocic odpowiedni model (ten sam dla obu, jedynie jedna zmienna ma inna wartosc true/false).
Zaczalem z takim czyms:

       var me = await _userManager.Users
                .Include(fw => fw.FriendWith.Select(ffw => ffw.Confirmed == false))
                .Include(fo => fo.FriendOf.Select(ffo => ffo.Confirmed == false))

Teraz mam usera razem z dwoma listami.
Czy istnieje mozliwosc zrobienia Select/SelectMany na obu? Bo w momencie zrobienia np

.SelectMany(u => u.FriendWith, (user, useruser) => new { object }

to kolejny .SelectMany zadziala na wyniku poprzedniego, wiec nie moge zrobic .SelectMany na u.FriendOf.
Chyba ze jest jakis trick ktorego po prostu nie znam.

Chwilowym rozwiazaniem (zeby chociaz dzialalo) jest foreach na kazdej liscie z osobna i dodania do jednej wspolnej kolekcji, ale to wydaje mi sie take "amatorskie" rozwiazanie.

Z gory dzieki.

0

ok, doszedlem do takiego czegos:

           var sth = _userManager.Users
                .Include(u => u.FriendWith)
                .Include(u => u.FriendOf)
                .Where(u => u.Id == request.Id)
                .Select(i => i.FriendWith.Concat(i.FriendOf).Where(f => f.Confirmed == false)
                    .Select(u => new
                    {
                        // tutaj juz mam wszystko co potrzebne
                    })).ToList();

Jednak mam 2 problemy:
Wykonuje to 2 zapytania do bazy danych + nie moge zrobic tego async (z await).
Problem jest z typami IAsyncEnumerable i IEnumerable: zdaje sie ze concat nie dziala z async.

Moge pobrac calosc jako User (z includami) a pozniej dopiero na listach zrobic concat i utworzyc response z potrzebna lista.
Chyba ze jest jakis lepszy sposob.

EDIT:
Co do problemu ilosci zapytan: tak to jest zaimplementowane w EF, wybor pomiedzy jednym wielkim mega zapytaniem/danymi a kilkoma mniejszymi.
cytat:
"Multiple Includes blow up the SQL result set. Soon it becomes cheaper to load data by multiple database calls instead of running one mega statement. Try to find the best mixture of Include and Load statements."
Teraz tylko wymyslec jak zrobic concat dzialajacy w async. moze Task.FromResult ?

1 użytkowników online, w tym zalogowanych: 0, gości: 1