C# Скорость фильтрации созданного свойства, который берёт данные из других List
У меня есть большой List из БД MS SQL Server, содержащий в себя на данный момент ≈ 17 000 записей и они постоянно добавляются. Мне нужно отобразить этот List в таблице, но необходимо создать ещё дополнительные столбцы для отображения данных из других List.
Мне удалось выполнить задание, но проблема появилась в скорости фильтрации этих созданных столбцов (почти минута на обработку).
Готов выслушать предложения по изменению кода.
List созданный с помощью EF:
public partial class ContrsuplSale
{
public int Cnssid { get; set; }
public int Cnssauth { get; set; }
public byte Cnsstype { get; set; }
public string Cnssagent1 { get; set; }
public string Cnssagent2 { get; set; }
public string Cnssreason { get; set; }
public short Cnssduration { get; set; }
public string Cnssaccount { get; set; }
public double CnssprePaymPerc { get; set; }
public int CnssintIsnum { get; set; }
public string CnssdirectumNum { get; set; }
public DateTime CnssdateContract { get; set; }
public string CnssdirectumLink { get; set; }
public int CnssidBill { get; set; }
public byte CnsstpDoc { get; set; }
public DateTime? Cnssdtend { get; set; }
public byte? Cnssfax { get; set; }
public int? CnssmaxDelivTime { get; set; }
public string Cnssagreement { get; set; }
public double CnsspercMarkup { get; set; }
public double? CnsspercFine { get; set; }
public double? CnsspercPenalty { get; set; }
public string Cnssaddress { get; set; }
public byte? CnssaddressFromBd { get; set; }
public string CnssserviceName { get; set; }
public byte? CnssneedServiceName { get; set; }
}
Новые свойства для отображение данных в столбцах:
public partial class ContrsuplSale
{
public string FULLNAMEstaff
{
get
{
var s = ContrsuplSaleDTO.GetFULLNAMEstaff(Cnssauth);
string fullname = $"{s.ScName} {s.FtName[0]}.{s.Patronymic[0]}.";
return fullname;
}
}
public string typeTable
{
get
{
var t = ContrsuplSaleDTO.GetTypeTable(Cnsstype);
return t.Cstname;
}
}
public string client
{
get
{
var cl = ContrsuplSaleDTO.GetClient(Cnssid, Cnsstype, CnsstpDoc);
if (cl == null)
{
return "";
}
return cl.Clname;
}
}
public string bill
{
get
{
string bill = ContrsuplSaleDTO.GetBill(Cnssid, Cnsstype, CnsstpDoc);
if (bill == null)
{
return "";
}
return bill;
}
}
public string MaxDelivTime
{
get
{
var max = ContrsuplSaleDTO.GetMaxDelivTime(Cnssid);
if (max.Cnsstype == 1 || max.Cnsstype == 16)
{
switch (max.CnssmaxDelivTime)
{
case 0:
return "на складе";
case null:
return "-";
}
return $"{max.CnssmaxDelivTime} дн.";
}
else return "-";
}
}
Функции для взятия данных из разные List с условиями:
public class ContrsuplSaleDTO : Notifer
{
// вывод таблицы contrsupl_sale
public static List<ContrsuplSale> GetAllContrsupl_sale()
{
using (Intis6Context db = new Intis6Context())
{
var result = from con in db.ContrsuplSales
join rel in db.Reldocs on con.Cnssid equals rel.Rlddoc1Id
where rel.Rldtype == 6 && rel.Rlddoc1Tp == 65
select con;
return result.OrderByDescending(p => p.Cnssid).ToList();
}
}
//вывести FULLNAME
public static staff GetFULLNAMEstaff(int id)
{
using (Intis6Context db = new Intis6Context())
{
var staff = db.staff.FirstOrDefault(s => s.IdStaff == id);
return staff;
}
}
//вывести Type
public static ContrsuplType GetTypeTable(int idtype)
{
using (Intis6Context db = new Intis6Context())
{
var cnsstype = db.ContrsuplTypes.FirstOrDefault(t => t.Cstid == idtype);
if (cnsstype == null)
{
var contrsuplTypes = db.ContrsuplTypes.ToList();
contrsuplTypes.Insert(0, new ContrsuplType { Cstname = "?" });
return contrsuplTypes.FirstOrDefault();
}
return cnsstype;
}
}
//вывести client
public static Client GetClient(int id, int type, int tpDoc)
{
using (Intis6Context db = new Intis6Context())
{
var realdoc = db.Reldocs.FirstOrDefault(r => r.Rlddoc1Id == id && r.Rldtype == 6 && r.Rlddoc1Tp == 65);
if (tpDoc == 21)
{
var outbill_j = db.OutbillJs.FirstOrDefault(o => o.Objid == realdoc.Rlddoc2Id && realdoc.Rlddoc2Tp == tpDoc);
var client = db.Clients.FirstOrDefault(c => c.Clid == outbill_j.Objclient);
return client;
}
var outdeal = db.Outdeals.FirstOrDefault(o => o.Odlid == realdoc.Rlddoc2Id && realdoc.Rlddoc2Tp == tpDoc);
if (type != 1 && tpDoc == 23)
{
if (outdeal == null)
{
return null;
}
var client = db.Clients.FirstOrDefault(c => c.Clid == outdeal.Odlclient);
return client;
}
else if (type == 1 && tpDoc == 23)
{
if (outdeal == null)
{
return null;
}
var outbill_paym = db.OutbillPayms.FirstOrDefault(o => o.ObprefTp == 23 && o.ObprefId == outdeal.Odlid);
if (outbill_paym == null)
{
return null;
}
var client = db.Clients.FirstOrDefault(c => c.Clid == outdeal.Odlclient);
return client;
}
return null;
}
}
//вывести bill
public static string GetBill(int id, int type, int tpDoc)
{
using (Intis6Context db = new Intis6Context())
{
var realdoc = db.Reldocs.FirstOrDefault(r => r.Rlddoc1Id == id && r.Rldtype == 6 && r.Rlddoc1Tp == 65);
if (tpDoc == 21)
{
var outbill_j = db.OutbillJs.FirstOrDefault(o => o.Objid == realdoc.Rlddoc2Id && realdoc.Rlddoc2Tp == tpDoc);
return $"{outbill_j.ObjnumbPref}{outbill_j.Objnumb} от {outbill_j.Objdate.ToString().Substring(0, 8)}";
}
else if (tpDoc == 22)
{
var outbill_f = db.Outbillves.FirstOrDefault(o => o.Obfid == realdoc.Rlddoc2Id && realdoc.Rlddoc2Tp == tpDoc);
return $"{outbill_f.ObfnumbPref}{outbill_f.Obfnumb} от {outbill_f.Obfdate.ToString().Substring(0, 8)}";
}
var outdeal = db.Outdeals.FirstOrDefault(o => o.Odlid == realdoc.Rlddoc2Id && realdoc.Rlddoc2Tp == tpDoc);
if (type != 1 && tpDoc == 23)
{
if (outdeal == null)
{
return null;
}
return $"{outdeal.OdlnumbPref}{outdeal.Odlnumb}{outdeal.OdlnumbPost} от {outdeal.Odldate.ToString().Substring(0, 8)}";
}
else if (type == 1 && tpDoc == 23)
{
if (outdeal == null)
{
return null;
}
var outbill_paym = db.OutbillPayms.FirstOrDefault(o => o.ObprefTp == 23 && o.ObprefId == outdeal.Odlid);
if (outbill_paym == null)
{
return null;
}
return $"{outbill_paym.ObpnumbPref}{outbill_paym.Obpnumb}/{outbill_paym.ObpnumbPost} от {outbill_paym.Obpdate.ToString().Substring(0, 8)}";
}
else return null;
}
}
//вывести MaxDelivTime
public static ContrsuplSale GetMaxDelivTime(int id)
{
using (Intis6Context db = new Intis6Context())
{
var type = db.ContrsuplSales.FirstOrDefault(t => t.Cnssid == id);
return type;
}
}
}
Код во ViewModel:
public class ContractVM : Notifer
{
public ICommand FilterCommand { get; set; }
Intis6Context db;
private ObservableCollection<ContrsuplSale> _allContrsupl_saleDTO;
public ObservableCollection<ContrsuplSale> AllContrsupl_saleDTO { get; set; }
private ICollectionView _contrsupl_saleView;
public ICollectionView Contrsupl_saleView
{
get { return _contrsupl_saleView; }
set { _contrsupl_saleView = value; NotifyPropertyChanged(); }
}
public ContractVM()
{
db = new Intis6Context();
AllContrsupl_saleDTO = new ObservableCollection<ContrsuplSale>(ContrsuplSaleDTO.GetAllContrsupl_sale());
Contrsupl_saleView = CollectionViewSource.GetDefaultView(AllContrsupl_saleDTO);
var d = db.ContrsuplTypes.ToList();
d.Insert(0, new ContrsuplType { Cstname = "Все" });
AllContrsupl_type = new List<ContrsuplType>(d);
FilterCommand = new RelayCommand((s) => true, FilterExecute);
}
private void FilterExecute(object obj)
{
if (string.IsNullOrWhiteSpace(BillFilter) && SelectedType == 0)
{
AllContrsupl_saleDTO.Clear();
AllContrsupl_saleDTO = new ObservableCollection<ContrsuplSale>(ContrsuplSaleDTO.GetAllContrsupl_sale());
Contrsupl_saleView = CollectionViewSource.GetDefaultView(AllContrsupl_saleDTO);
return;
}
Contrsupl_saleView.Filter += Filter;
}
private bool Filter(object obj)
{
ContrsuplSale contrsuplSale = obj as ContrsuplSale;
int idSelectedType = 0;
switch (SelectedType)
{
case 1:
idSelectedType = 1;
break;
case 2:
idSelectedType = 16;
break;
case 3:
idSelectedType = 17;
break;
case 4:
idSelectedType = 24;
break;
case 5:
idSelectedType = 25;
break;
case 6:
idSelectedType = 26;
break;
case 7:
idSelectedType = 27;
break;
case 8:
idSelectedType = 28;
break;
case 9:
idSelectedType = 33;
break;
}
if (!string.IsNullOrWhiteSpace(BillFilter) && SelectedType != 0)
{
return contrsuplSale.bill.Contains(BillFilter) && contrsuplSale.Cnsstype == idSelectedType;
return true;
}
else if (string.IsNullOrWhiteSpace(BillFilter))
{
return contrsuplSale.Cnsstype == idSelectedType;
}
else
return contrsuplSale.bill.Contains(BillFilter);
}
}
Код XAML
<DataGrid Background="White" Name="ContractTable" IsReadOnly="True" AutoGenerateColumns="False" EnableColumnVirtualization="True" EnableRowVirtualization="True"
ItemsSource="{Binding Contrsupl_saleView, IsAsync=True}"
SelectedItem="{Binding ContractSelect}"
Grid.Row="0" Margin="0,39,0,0">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding Path=Cnssid}"/>
<DataGridTextColumn Header="Автор" Binding="{Binding Path=FULLNAMEstaff}"/>
<DataGridTextColumn Header="Тип" Binding="{Binding Path=typeTable}"/>
<DataGridTextColumn Header="Клиент" Binding="{Binding Path=client}"/>
<DataGridTextColumn Header="Срок" Binding="{Binding Path=Cnssduration}"/>
<DataGridTextColumn Header="Счет" Binding="{Binding Path=bill}"/>
<DataGridTextColumn Header="№Directum" Binding="{Binding Path=CnssdirectumNum}"/>
<DataGridTextColumn Header="Дата" Binding="{Binding Path=CnssdateContract,StringFormat={}{0:dd.MM.yyyy}}"/>
<DataGridTextColumn Header="Ссылка" Binding="{Binding Path=CnssdirectumLink}"/>
<DataGridTextColumn Header="Предопл." Binding="{Binding Path=CnssprePaymPerc}"/>
<DataGridTextColumn Header="Истекает" Binding="{Binding Path=Cnssdtend, StringFormat={}{0:dd.MM.yyyy}}"/>
<DataGridTextColumn Header="Договоренности" Binding="{Binding Path=Cnssagreement}"/>
<DataGridTextColumn Header="Max срок пост." Binding="{Binding Path=MaxDelivTime}"/>
<DataGridTextColumn Header="План. % наценки" Binding="{Binding Path=CnsspercMarkup}"/>
</DataGrid.Columns>
</DataGrid>
Ответы (2 шт):
В методе GetTypeTable есть код:
if (cnsstype == null)
{
var contrsuplTypes = db.ContrsuplTypes.ToList();
contrsuplTypes.Insert(0, new ContrsuplType { Cstname = "?" });
return contrsuplTypes.FirstOrDefault();
}
Что я вижу: материализуется полностью таблица из БД, что дорого. Затем в начало списка вставляется новый элемент (все остальные сдвигаются назад, что тоже дорого). И в итоге берётся первый элемент из списка, т. е. тот самый, который мы только что вставили! Следовательно, список вообще не нужен! И не надо получать его из БД.
Итого, код может выглядеть так:
if (cnsstype == null)
{
return new ContrsuplType { Cstname = "?" };
}
В конструкторе ContractVM есть код:
var d = db.ContrsuplTypes.ToList();
d.Insert(0, new ContrsuplType { Cstname = "Все" });
AllContrsupl_type = new List<ContrsuplType>(d);
Получаем список d, потом на его основе получаем новый список. Зачем? Можно же просто:
var d = db.ContrsuplTypes.ToList();
d.Insert(0, new ContrsuplType { Cstname = "Все" });
AllContrsupl_type = d;
Если размер списка d велик, то вставка в начало обходится дорого. В таком случае можно попробовать поступить так:
var d = new List<ContrsuplType>();
d.Add(new ContrsuplType { Cstname = "Все" });
foreach (var x in db.ContrsuplTypes)
d.Add(x);
AllContrsupl_type = d;
Создаём список, добавляем нужный элемент, далее копируем в него данные из БД. В данном случае не будет лишнего копирования данных при сдвигании назад.
К тому же можно задать начальную ёмкость списка: new List<ContrsuplType>(1000).
Позже посмотрю код, может ещё что замечу.
Если я правильно понимаю, на каждую из 17000 строк для каждого дополнительного свойства происходит запрос в БД.
Тут нужно либо сразу для всех свойств строки получать данные одним запросом, что мне кажется вполне осуществимым. Либо получать данные одного свойства для всех строк сразу. На первый взгляд это сложнее, но эффективнее.
Надо подумать...
Замечания по архитектуры приложения и БД:
ContrsuplSale.CnsstypeтипаByte, в методContrsuplSaleDTO.GetTypeTableон передаётся какInt32,ContrsuplTypes.CstidтипаByteилиInt32? К методуContrsuplSaleDTO.GetClientиContrsuplSaleDTO.GetBillтакой же вопрос, в сущности БД типByte, а в методеInt32, что за тип данных уReldocs.Rlddoc2Tp?var realdoc = db.Reldocs.FirstOrDefault(r => r.Rlddoc1Id == id && r.Rldtype == 6 && r.Rlddoc1Tp == 65);методFirstOrDefaultработает таким образом
Возвращает первый элемент последовательности или значение по умолчанию, если ни одного элемента не найдено.
https://docs.microsoft.com/ru-ru/dotnet/api/system.linq.enumerable.firstordefault?view=net-6.0
Дальше нет никакой проверки его на null, если вы только не уверенны на 100% что запись там есть? Если не уверены то делайте так:
var realdoc = db.Reldocs.FirstOrDefault(r => r.Rlddoc1Id == id && r.Rldtype == 6 && r.Rlddoc1Tp == 65);
if(realdoc == null
{
return null;
}
var outdeal = db.Outdeals.FirstOrDefault(o => o.Odlid == realdoc.Rlddoc2Id && realdoc.Rlddoc2Tp == tpDoc);
if (outdeal == null) // Лучше сразу проверить перед остальными проверками
{
return null;
}
Магические числа:
tpDoc == 21,r.Rldtype == 6 && r.Rlddoc1Tp == 65,type != 1 && tpDoc == 23. Не используйте их, все их значения не удержите в голове, придумайте под них переменные, имена которых будут описывать их назначение.Проверки на
nullvar max = ContrsuplSaleDTO.GetMaxDelivTime(Cnssid);
ContrsuplSaleDTO.GetMaxDelivTime тоже может вернуть null
Если в коде много подобных проверок
if (bill == null) { return ""; } return bill;
То можно сделать вот так:
internal static class StringExtension
{
internal static String GetValidValueForEntity(this String? str)
{
return String.IsNullOrWhiteSpace(str) ? String.Empty : str.Trim();
}
}
return bill.GetValidValueForEntity()
Objdate.ToString().Substring(0, 8)замените наObjdate.ToString("dd.MM.yyyy")или любой удобный формат даты https://docs.microsoft.com/en-us/dotnet/api/system.datetime.tostring?view=net-6.0