Skomplikowane zapytanie LINQ, można uprościć?

Skomplikowane zapytanie LINQ, można uprościć?
EP
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad 6 lat
  • Postów:122
0

Mamy takie oto zapytanie LINQ:

Kopiuj
public ProfileDTO GetProfileByUserID(int id)
        {
            var profile = _databaseContext
                .Users.Where(user => user.ID == id)
                .Select(user => new ProfileDTO()
                {
                    JoinTime = user.JoinTime,

                    PostsCount = user.Posts.Count(),
                    PostsPerDay = user.Posts.Count() / (float)(DateTime.Now - user.JoinTime).TotalDays,
                    PercentageOfAllPosts = (float)user.Posts.Count() / _databaseContext.Posts.Count(),

                    MostActiveTopicName = user.Posts.GroupBy(post => post.Topic.ID)
                                                    .OrderByDescending(post => post.Count())
                                                    .SelectMany(p => p, (group, post) => post)
                                                    .First().Topic.Name,

                    MostActiveTopicAlias = user.Posts.GroupBy(post => post.Topic.ID)
                                                    .OrderByDescending(post => post.Count())
                                                    .SelectMany(p => p, (group, post) => post)
                                                    .First().Topic.Alias,

                    MostActiveCategoryName = user.Posts.GroupBy(post => post.Topic.ID)
                                                    .OrderByDescending(post => post.Count())
                                                    .SelectMany(p => p, (group, post) => post)
                                                    .First().Topic.Category.Name,

                    MostActiveCategoryAlias = user.Posts.GroupBy(post => post.Topic.ID)
                                                    .OrderByDescending(post => post.Count())
                                                    .SelectMany(p => p, (group, post) => post)
                                                    .First().Topic.Category.Alias
                }).Single();

            return profile;
        }

Sam nie wierzę, że stworzyłem to arcydzieło w nocy, ale do rzeczy. Zapytanie to ma zwrócić dane o profilu użytkownika forum, czyli między innymi liczbę postów oraz nazwę i alias wątku i kategorii w których był najbardziej aktywny (tj. wysłał najwięcej postów).

Czy tak zawiłe zapytania to coś, co nie jest normalne i powinno być bardziej rozbite/uproszczone? Nie jestem pewny reakcji kogoś, kto by zobaczył taki twór na technicznej rozmowie kwalifikacyjnej, dlatego wolę się zapytać niż zasiać jutro takie zapytania w całym projekcie.


Wenn ist das Nunstück git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput!
edytowany 3x, ostatnio: EntityPamerano
2

Widziałem gorsze. Jak chcesz uprościć, to zrób sobie widok w bazie.

1

Serio ? musialem przeczytac wszystkie zapytania zeby zauwazyc ze powtarzasz ciagle

Kopiuj
user.Posts.GroupBy(post => post.Topic.ID)
    .OrderByDescending(post => post.Count())
    .SelectMany(p => p, (group, post) => post)
    .First()
EP
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad 6 lat
  • Postów:122
0

To też jest problem, bo bardzo nie lubię łamania DRY. Oczywiście można podzielić to na kilka podzapytań, dzięki temu byłoby o wiele czytelniej, problem w tym że nie wykonywałbym wtedy 1 zapytania, tylko N (chyba że wydzielić jakoś IQueryable do oddzielnych metod i używać ich w głównym zapytaniu, nie próbowałem tego jeszcze i nie wiem czy tak zadziała).


Wenn ist das Nunstück git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput!
katelx
  • Rejestracja:około 10 lat
  • Ostatnio:5 miesięcy
  • Lokalizacja:Hong Kong
1

nie znam EF, co do linq to:

Kopiuj
var user = [tutaj query zeby wyciagnac usera];
var topic = [tutaj query zeby wyciagnac top topic];
return new ProfileDTO { //blablabla

btw uzycie SelectMany jest zbedne

EP
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad 6 lat
  • Postów:122
0

O, dobre rozwiązanie, dzięki :) Natomiast SelectMany zrobiłem, ponieważ w przeciwnym wypadku mam to:

Kopiuj
user.Posts.GroupBy(post => post.Topic.ID)
          .OrderByDescending(post => post.Count())
          .First().First().Topic.Name,

Podwójny First() wydaje mi się potwornie paskudnym i brzydkim zapisem, tym Selectem grupuje sobie wszystkie listy z IGrouping i z nich już wybieram pierwszy element jednym Firstem.

Całe to rozwiązanie nie jest jeszcze odporne na wypadek gdy user nie ma żadnych postów, ale to jest do dopracowania.


Wenn ist das Nunstück git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput!
edytowany 3x, ostatnio: EntityPamerano
katelx
  • Rejestracja:około 10 lat
  • Ostatnio:5 miesięcy
  • Lokalizacja:Hong Kong
1

co kto lubi, dla mnie First jest fajniejszy od SelectMany(p => p, (group, post) => post).
btw imo duzo czytelniej by bylo to napisac jako query a nie przez method chaining (wtedy sobie mozesz tez let wrzucic gdzies w srodku zeby sie nie powtarzac)

EP
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad 6 lat
  • Postów:122
0

Dłuższą chwilę posiedziałem na tym zapytaniem, pooglądałem to co generuje EF w Express Profilerze i wygląda na to, że działa elegancko. Kod z pierwszego posta ma najwidoczniej problem N+1, czego wcześniej nie zobaczyłem. Dopiero po dodaniu większej liczby postów, okazało się że EF robi zapytanie odpytując bazę o każdą kategorię, wątek itp.

Nowy kod załatwia wszystko elegancko jednym zapytaniem + jest bardziej rozbity i mam nadzieję czytelniejszy.

Kopiuj
public ProfileDTO GetProfileByUserID(int id)
{
	var selectedUser = _databaseContext.Users
		.Include(user => user.Posts)
		.Include(user => user.Posts.Select(post => post.Topic))
		.Include(user => user.Posts.Select(post => post.Topic.Category))
		.FirstOrDefault(u => u.ID == id);

	if(selectedUser == null)
		throw new UserProfileNotFoundException();

	var userMostActiveTopic = selectedUser.Posts
		.GroupBy(post => post.Topic.ID)
		.OrderByDescending(post => post.Count())
		.First()
		.Select(post => new
		{
			TopicName = post.Topic.Name,
			TopicAlias = post.Topic.Alias,
			CategoryAlias = post.Topic.Category.Alias
		}).First();

	var userMostActiveCategory = selectedUser.Posts
		.GroupBy(post => post.Topic.Category.ID)
		.OrderByDescending(post => post.Count())
		.First()
		.Select(post => new
		{
			CategoryName = post.Topic.Category.Name,
			CategoryAlias = post.Topic.Category.Alias
		}).First();

	return profile = new ProfileDTO()
	{
		JoinTime = selectedUser.JoinTime,

		PostsCount = selectedUser.Posts.Count(),
		PostsPerDay = selectedUser.Posts.Count() / (float)(DateTime.Now - selectedUser.JoinTime).TotalDays,
		PercentageOfAllPosts = (float)selectedUser.Posts.Count() / _databaseContext.Posts.Count(),

		MostActiveTopicName = userMostActiveTopic.TopicName,
		MostActiveTopicAlias = userMostActiveTopic.TopicAlias,
		MostActiveTopicCategoryAlias = userMostActiveTopic.CategoryAlias,

		MostActiveCategoryName = userMostActiveCategory.CategoryName,
		MostActiveCategoryAlias = userMostActiveCategory.CategoryAlias
	};
}

whatever.png


Wenn ist das Nunstück git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput!
edytowany 3x, ostatnio: EntityPamerano
Azarien
  • Rejestracja:ponad 21 lat
  • Ostatnio:około 10 godzin
0

tym Selectem grupuje sobie wszystkie listy z IGrouping i z nich już wybieram pierwszy element jednym Firstem.

ale po co grupować skoro potrzebny ci tylko pierwszy? Fajnie, może ci się akurat zapytanie zoptymalizuje i będzie tak samo szybko jak z firstem, a może nie.
Nie rób zbędnych operacji.

EP
Ok, podmieniłem SelectMany na First w najnowszym poście, możliwe że faktycznie jest to bardziej czytelne.
EP
  • Rejestracja:prawie 8 lat
  • Ostatnio:ponad 6 lat
  • Postów:122
0

Jeszcze jedno pytanie, bardziej hipotetyczne. Załóżmy że mamy następujące tabele: Sekcja, Kategoria, Wątek, Post, Użytkownik. Relacje między nimi są raczej jasne, dla przykładu encja "Kategoria" zawiera dwie navigaton property: Sekcja i ICollection<Wątek>.

Teraz wyobraźmy sobie że wchodzimy na stronę główną forum. Musimy skonstruować zapytanie, które pobierze:

  • listę sekcji
  • listę kategorii w poszcególnych sekcjach (tj. ich nazwy i opisy)
  • liczbę wątków w poszczególnych kategoriach
  • nazwę autora ostatnio napisanego postu w poszczególnych kategoriach

Szybko można wywnioskować, że trzeba będzie zaprząc wszystkie wymienione wcześniej tabele. Ponieważ jednym zapytaniem się tego nie ogarnie, to jeśli zrobiłem dobry research to mamy dwa podejścia:

  • pobrać wszystkie tabele za pomocą .Include(...) i na danych w pamięci skompletować sobie te potrzebne - podejrzewam że jest to bardzo złe wyjście, bo w praktyce jednym zapytaniem ładujemy 95% zawartości bazy danych (wszystkie posty, wątki, kategorie, userów itp.) - przy większym systemie mamy przepis na katastrofę.
  • podzielić to na mniejsze zapytania. Na przykład zrobić oddzielne zapytanie na wydobycie listy kategorii, oraz oddzielne dla każdej kategorii które wydobędzie nazwę autora ostatniego napisanego posta. W wersji pesymistycznej takich zapytań może być nawet kilkadziesiąt na jeden request, więc też nie jest to idealne. Poza tym, to jak podejrzewam, to typowy problem N+1.

Co robić, jak żyć? Pisząc forum natknąłem się pierwszy raz na potrzebę wygrzebywania spod paru joinów różnych danych, a że "odkryłem" profiler SQL to widzę jak zapytania się mnożą na potęgę.


Wenn ist das Nunstück git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput!
edytowany 3x, ostatnio: EntityPamerano
EP
Ok, problem rozwiązany. Przy odrobinie chęci wszystko da się zrobić jednym zapytaniem bez wyciągania całej zawartości do pamięci Includami :P EF jest sprytniejszy niż myślałem.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.