Memfilter database dengan Linq

Oct 06 2020

Saya sedang membangun aplikasi web MVC dengan ASP.CORE 3.1 dan EF yang terhubung ke MSSQL.

Hari ini, akhirnya setelah 3 hari, saya agak mencapai kode yang berfungsi untuk memfilter tampilan saya.

Saya punya database

  • Stempel tanggal (interval 15 menit) dan
  • Nilai INT mobil (3 jenis (OA, NA, NS))
  • melalui Border (13 dari mereka).
  • Data untuk kedua arah (Plus, Minus).
  • Saya perlu memfilter ini dengan di atas ditambah mengubah interval waktu (60, 30, 15 menit)
  • dan pada Hari Kerja (Senin, Jumat, Sab, Minggu, dan Biasa (Selasa-Kamis)).

Tapi saya melakukan penyaringan untuk pertama kalinya dalam hidup saya di sini. Senang itu berhasil tetapi ingin mendengar bagaimana saya bisa meningkatkan ini. Set data tidak besar. Sekitar 17 kolom x 300.000 baris.

Visualisasi

Model

Borders.cs

public class Borders
{
    [Key]
    public int Id { get; set; }

    public int TransitId { get; set; }

    public DateTime Day { get; set; }
    public DateTime Start { get; set; }
    public DateTime End { get; set; }

    public int OA_Plus { get; set; }
    public int NA_Plus { get; set; }
    public int NS_Plus { get; set; }
    public int ALL_Plus { get; set; }

    public int OA_Minus { get; set; }
    public int NA_Minus { get; set; }
    public int NS_Minus { get; set; }
    public int ALL_Minus { get; set; }

    public int OA_Sum { get; set; }
    public int NA_Sum { get; set; }
    public int NS_Sum { get; set; }
    public int ALL_Sum { get; set; }
}

ViewModel

BordersViewModel.cs

public class BordersViewModel
{
    // DB items
    public IEnumerable<Borders> Borders { get; set; }

    // Other ViewModels
    public IEnumerable<ChartIntensityVM> ChartIntensityVM { get; set; }
    // Contains only String Key, Int Value

    // Filters
    public string FilterTransitNumber { get; set; } = "All";
    public string FilterSeason { get; set; } = "All";
    public string FilterDay { get; set; } = "All";
    public string FilterTimeInterval { get; set; } = "1";
    public string FilterDirection { get; set; } = "All";

    public bool FilterChkboxOA { get; set; } = true;
    public bool FilterChkboxNA { get; set; } = true;
    public bool FilterChkboxNS { get; set; } = true;

    // SelectListItems
    public List<SelectListItem> ListTransitNumbers { get; set; } = new List<SelectListItem>()
    {
        new SelectListItem { Value = "All", Text = "All" },
    };
    public List<SelectListItem> ListSeasons { get; set; } = new List<SelectListItem>()
    {
        new SelectListItem { Value = "All", Text = "All" },
        new SelectListItem { Value = "Q1", Text = "Spring (1-3)" },
        new SelectListItem { Value = "Q2", Text = "Summer (3-6)" },
        new SelectListItem { Value = "Q3", Text = "Fall (6-9)" },
        new SelectListItem { Value = "Q4", Text = "Winter (9-12)" },
    };
    public List<SelectListItem> ListDays { get; set; } = new List<SelectListItem>()
    {
        new SelectListItem { Value = "All", Text = "All" },
        new SelectListItem { Value = "Ordinary", Text = "Ordinary day (Tue, Wed, Thr)" },
        new SelectListItem { Value = "Friday", Text = "Friday" },
        new SelectListItem { Value = "Sunday", Text = "Sunday" },
        new SelectListItem { Value = "Monday", Text = "Monday" },
    };
    public List<SelectListItem> ListTimeIntervals { get; set; } = new List<SelectListItem>()
    {
        new SelectListItem { Value = "60", Text = "1 hr" },
        new SelectListItem { Value = "30", Text = "0.5 hr" },
        new SelectListItem { Value = "15", Text = "0.25 hr" },
    };
    public List<SelectListItem> ListDirections { get; set; } = new List<SelectListItem>()
    {
        new SelectListItem { Value = "All", Text = "All" },
        new SelectListItem { Value = "Plus", Text = "Plus" },
        new SelectListItem { Value = "Minus", Text = "Minus" },
    };
}

Melihat

Index.cshtml

classical view, you can see that in the picture above
SelectLists, Checkboxes, Filterbutton that is doing form POST method

Kontroler

BordersController.cs

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(BordersViewModel vm)
{
    if (ModelState.IsValid)
    {
        vm.Borders = await _db.Borders.OrderBy(x => x.Start).ToListAsync();
        populateListTransitNumbers(vm);
    }

    // FILTER - TransitNumber (1 - 16)
    //================================
    if (vm.FilterTransitNumber != "All")
    {
        vm.Borders = vm.Borders.Where(x => x.TransitId == Convert.ToInt32(vm.FilterTransitNumber));
    }

    // // FILTER - Day (Monday, Friday, Sunday, Ordinary [Tuesday, Wednesday, Thursday])
    //==================================================================================
    if (vm.FilterDay != "All")
    {
        List<string> ordinaryDays = new List<string>() { "Tuesday", "Wednesday", "Thursday" };

        switch (vm.FilterDay)
        {
            case "Monday":
            case "Friday":
            case "Sunday":
                vm.Borders = vm.Borders.Where(x => x.Start.DayOfWeek.ToString() == vm.FilterDay);
                break;
            default:
                vm.Borders = vm.Borders.Where(x => ordinaryDays.Contains(x.Start.DayOfWeek.ToString()));
                break;
        }
    }

    // FILTER - Season (Q1 - Q4)
    //==========================
    if (vm.FilterSeason != "All")
    {
        switch (vm.FilterSeason)
        {
            case "Q1":
                vm.Borders = vm.Borders.Where(x => x.Start.Month >= 1 && x.Start.Month <= 3);
                break;
            case "Q2":
                vm.Borders = vm.Borders.Where(x => x.Start.Month >= 3 && x.Start.Month <= 6);
                break;
            case "Q3":
                vm.Borders = vm.Borders.Where(x => x.Start.Month >= 6 && x.Start.Month <= 9);
                break;
            case "Q4":
                vm.Borders = vm.Borders.Where(x => x.Start.Month >= 9 && x.Start.Month <= 12);
                break;
        }
    }

    // FILTER - TimeInterval (60, 30, 15)
    //===================================
    var filteredBordersInterval = vm.Borders
        .GroupBy(x =>
        {
            var stamp = x.Start;
            stamp = stamp.AddMinutes(-(stamp.Minute % Convert.ToInt32(vm.FilterTimeInterval)));
            stamp = stamp.AddMilliseconds(-stamp.Millisecond - 1000 * stamp.Second);
            return stamp;
        })
        .Select(g => new
        {
            TransitId = g.First().TransitId,
            Start = g.Key,
            OA_Plus = g.Sum(gi => gi.OA_Plus),
            NA_Plus = g.Sum(gi => gi.NA_Plus),
            NS_Plus = g.Sum(gi => gi.NS_Plus),
            OA_Minus = g.Sum(gi => gi.OA_Minus),
            NA_Minus = g.Sum(gi => gi.NA_Minus),
            NS_Minus = g.Sum(gi => gi.NS_Minus),
        });

    // ORDER BY TIME INTERVAL
    // Ignore the whole DateTime day, just group by HH:mm and SUM column rows
    //=======================
    var filteredBordersGrouped = filteredBordersInterval
        .GroupBy(x => x.Start.ToString("HH:mm"))
        .Select(g => new
        {
            DayTime = g.Key,
            OA_Plus = g.Sum(gi => gi.OA_Plus),
            NA_Plus = g.Sum(gi => gi.NA_Plus),
            NS_Plus = g.Sum(gi => gi.NS_Plus),
            OA_Minus = g.Sum(gi => gi.OA_Minus),
            NA_Minus = g.Sum(gi => gi.NA_Minus),
            NS_Minus = g.Sum(gi => gi.NS_Minus),
        });

    // FINAL FILTER into X/Y values for the Chart
    // Sum only those cars that are checked and both or individual directions
    //===========================================
    vm.ChartIntensityVM = filteredBordersGrouped
        .GroupBy(x => x.DayTime)
        .Select(g =>
        {
            int PlusSum = g.Sum(gi => vm.FilterChkboxOA == true ? gi.OA_Plus : 0)
                        + g.Sum(gi => vm.FilterChkboxNA == true ? gi.NA_Plus : 0)
                        + g.Sum(gi => vm.FilterChkboxNS == true ? gi.NS_Plus : 0);

            int MinusSum = g.Sum(gi => vm.FilterChkboxOA == true ? gi.OA_Minus : 0)
                        + g.Sum(gi => vm.FilterChkboxNA == true ? gi.NA_Minus : 0)
                        + g.Sum(gi => vm.FilterChkboxNS == true ? gi.NS_Minus : 0);

            int AllSum = vm.FilterDirection == "All" ? PlusSum + MinusSum : (vm.FilterDirection == "Plus" ? PlusSum : MinusSum);

            return new ChartIntensityVM
            {
                Key = g.Key,
                Value = AllSum,
            };
        });

    return View(vm);
}

Jawaban

2 BCdotWEB Oct 06 2020 at 23:21

Beberapa komentar singkat

  • Ikuti standar penamaan. Nama properti, dll. Tidak boleh berisi apa pun kecuali karakter alfanumerik. Tidak ada garis bawah dll.

  • Gunakan nama yang bermakna. "OA"tidak ada artinya, "OA_Plus"bahkan lebih membingungkan.

  • Sebuah nama kelas tidak boleh jamak (beberapa pengecualian berlaku): Borders.

  • Begitu banyak string ajaib. "All"muncul berkali-kali, misalnya. Pertimbangkan untuk memindahkan ini ke statickelas sebagai public const stringproperti.

  • Jangan menyingkat tanpa tujuan: memberi nama Chkboxtidak akan membuat kode Anda berjalan lebih cepat.

  • Jangan menyebut sesuatu sebagai "ListXXXX", misalnya ListDays. Jika itu adalah daftar hari, beri nama "Hari".

  • ListDays tampaknya menghilangkan hari Sabtu, dan "Thr" bukanlah singkatan yang benar untuk "Kamis".

  • Musim Anda / perempat tampaknya tumpang tindih: "Spring (1-3)"vs "Summer (3-6)". IMHO harus "1-3", "4-6", dll. Namun, ini juga diterapkan dengan cara ini dalam logika bisnis Anda - vm.Borders.Where(x => x.Start.Month >= 3 && x.Start.Month <= 6);- jadi saya rasa itu adalah logika "yang benar"? Tetap saja, tidak masuk akal bagi saya bahwa Q1 menampilkan data selama tiga bulan, sedangkan yang lain menampilkan data selama empat bulan. IMHO ini terlihat seperti bug besar.

  • public async Task<IActionResult> Index(BordersViewModel vm)lebih dari 100 baris. Saya akan memindahkan hampir semua kode itu ke kelas terpisah.


Judul pertanyaan Anda mengatakan "Memfilter database dengan Linq", namun pertanyaan Anda berfungsi sepenuhnya dengan model tampilan. Saya benar-benar bertanya-tanya apakah beberapa pemfilteran yang Anda lakukan dapat dilakukan dengan lebih efisien dalam kueri, alih-alih (apa yang tampaknya Anda lakukan) membuang semua data yang tersedia dalam Bordersobjek dan kemudian menerapkan banyak logika pemfilteran.