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

0

Mamy takie oto zapytanie LINQ:

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.

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

user.Posts.GroupBy(post => post.Topic.ID)
    .OrderByDescending(post => post.Count())
    .SelectMany(p => p, (group, post) => post)
    .First()
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).

1

nie znam EF, co do linq to:

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

btw uzycie SelectMany jest zbedne

0

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

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.

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)

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.

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

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.

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ę.

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