I have made my query to the database as raw SQL and want to shift to SQL to entities.
Reason for that is performance is terrible in the way it's constructed, not the actual query, which is fast, but afterwards I had to perform multiple SQL statements to get data from associated tables. I was thinking that if I moved to SQL to entities, the associated tables were already fetched and I only had to make that one call to the database.
I construct my old query like this:
List<Database.Product> products = null;
string sql = "SELECT P.* FROM Product p " +
"LEFT JOIN ProductAssigned pa ON pa.ProductId = p.Id " +
"WHERE pa.UserId = #userid AND pa.UnassignedAt IS NULL";
sql = FilterByProductState(productFilter, sql);
if (startDate != default && endDate != default)
{
string start = startDate.Year + "/" + startDate.Month + "/" + startDate.Day;
string end = endDate.Year + "/" + endDate.Month + "/" + endDate.AddDays(1).Day;
sql += " AND (p.StartAt BETWEEN '" + start + "' AND '" + end + "' OR p.CompleteAt BETWEEN '" + start + "' AND '" + end + "')";
}
List<SqlParameter> sqlParameters = new List<SqlParameter>
{
new SqlParameter("#userid", userId)
};
sql += " ORDER BY p.StartAt";
try
{
products = context.Database.SqlQuery<Database.Product>(sql, new SqlParameter("#userid", userId)).ToList();
}
catch (Exception e)
{
throw;
}
The missing method from the code:
private static string FilterByProductState(ProductFilter productFilter, string sql)
{
switch (productFilter)
{
case ProductFilter.Started:
sql += " AND p.StartedAt IS NOT NULL";
sql += " AND p.CompletedAt IS NULL";
sql += " AND p.DeletedAt IS NULL";
break;
case ProductFilter.Closed:
sql += " AND p.CompletedAt IS NOT NULL";
sql += " AND p.DeletedAt IS NULL";
break;
case ProductFilter.AllNotStarted:
sql += " AND p.StartedAt IS NULL";
sql += " AND p.DeletedAt IS NULL";
break;
case ProductFilter.All:
default:
break;
}
return sql;
}
After this like I said I have to go and get certain associated tables.
I have tried to start several things, e.g. using GroupJoin, but nothing seems to pan out. Also I'm creating the SQL statement dynamically, so I don't even know if it could work in SQL to entities.
UPDATE:
Product(the interesting parts) and ProductAssigned is shown here:
public partial class Product
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Product()
{
this.ProductAssigned = new HashSet<ProductAssigned>();
}
public int Id { get; set; }
public string Name { get; set; }
public Nullable<System.DateTime> StartAt { get; set; }
public Nullable<System.DateTime> CompleteAt { get; set; }
public Nullable<System.DateTime> DeletedAt { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<ProductAssigned> ProductAssigned { get; set; }
}
public partial class ProductAssigned
{
public int Id { get; set; }
public int ProductId { get; set; }
public int UserId { get; set; }
public System.DateTime AssignedAt { get; set; }
public Nullable<System.DateTime> UnassignedAt { get; set; }
public virtual Product Product { get; set; }
public virtual User CreatedByUser { get; set; }
public virtual User DeletedByUser { get; set; }
public virtual User UserAssigned { get; set; }
}
The LINQ query can be built in a very similar fashion. Just instead of manual joins, in EF queries you think in objects, references and collections, and EF converts that to the necessary joins.
The first part could be written exactly as the SQL query
IQueryable<Database.Product> query =
from p in context.Products
from pa in p.ProductAssigned.DefaultIfEmpty() // <-- left join
where pa.UserId == userid && pa.UnassignedAt == null
select p;
but it doesn't make much sense. WHERE condition after LEFT JOIN effectively makes it INNER, and also the whole purpose of that join in the original query seems to be to check for existence, so it could be formulated as follows
IQueryable<Database.Product> query = context.Products
.Where(p => p.ProductAssigned.Any(pa =>
pa.UserId == userid && pa.UnassignedAt == null));
Whatever you choose for the first (static) part of the query, the rest of it (the dynamic part) can be built by simply chaining Where calls (at the end they will be combined with AND in the WHERE clause).
query = FilterByProductState(productFilter, query);
if (startDate != default && endDate != default)
{
var start = startDate.Date;
var end = start.AddDays(1);
query = query.Where(p => (p.StartAt >= start && p.StartAt <= end)
|| (p.CompleteAt >= start && p.CompleteAt <= end));
}
query = query.OrderBy(p => p.StartAt);
and the equivalent helper method
private static IQueryable<Database.Product> FilterByProductState(
ProductFilter productFilter,
IQueryable<Database.Product> query)
{
switch (productFilter)
{
case ProductFilter.Started:
query = query.Where(p => p.StartedAt != null
&& p.CompletedAt == null
&& p.DeletedAt == null);
break;
case ProductFilter.Closed:
query = query.Where(p => p.CompletedAt != null
&& p.DeletedAt == null);
break;
case ProductFilter.AllNotStarted:
query = query.Where(p => p.StartedAt == null
&& p.DeletedAt == null);
break;
case ProductFilter.All:
default:
break;
}
return query;
}
Related
I have the following entities:
[Table("Customer", Schema = "dbo")]
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
[Table("Payment", Schema = "dbo")]
public class Payment
{
public int PaymentId { get; set; }
public int CustomerId { get; set; }
public DateTime Period { get; set; }
public int Price { get; set; }
[ForeignKey("CustomerId")]
public Customer Customer { get; set; }
}
Now I want to filter Payment table by Period and Price. Each of predicate must be in its own Where method. So, I get the following:
int price = 200;
var period = new DateTime(2020, 10, 3);
using var db = new TestContext();
// Option 1: anonymous lambda
var payments1 = db.Payments
.Where(p => p.Period < period)
.Where(p => p.Price <= price);
foreach (var payment in payments1)
{
listBox.Items.Add(
$"Payment: Payment Id={payment.PaymentId}, " +
$"Customer Id`={payment.CustomerId}, " +
$"Period={payment.Period.ToShortDateString()}, " +
$"Price={payment.Price}");
}
EF6 generates correct SQL:
exec sp_executesql N'SELECT
[Extent1].[PaymentId] AS [PaymentId],
[Extent1].[CustomerId] AS [CustomerId],
[Extent1].[Period] AS [Period],
[Extent1].[Price] AS [Price]
FROM [dbo].[Payment] AS [Extent1]
WHERE ([Extent1].[Period] < #p__linq__0) AND ([Extent1].[Price] <= #p__linq__1)',
N'#p__linq__0 datetime2(7),#p__linq__1 int',
#p__linq__0='2020-10-03 00:00:00',
#p__linq__1=200
However, if I use Func lambda with the same condition:
// Option 2: Func<T, T> lambda
Func<Payment, bool> func = p => p.Period < period;
var payments2 = db.Payments.Where(func).Where(p => p.Price <= price);
I don't get the same SQL, but get this one:
SELECT
[Extent1].[PaymentId] AS [PaymentId],
[Extent1].[CustomerId] AS [CustomerId],
[Extent1].[Period] AS [Period],
[Extent1].[Price] AS [Price]
FROM [dbo].[Payment] AS [Extent1]
As far as I understand, EF switched to client-side evaluation. I wonder, why this happened? I'm using same lambda for filtering!
You need to use an Expression, not just a Func so EF can work out the names of the properties etc.
Try:
Expression<Func<Payment, bool>> func = p => p.Period < period;
Currently, I have model which represents a table within my database.
public partial class Booking
{
public int BookingId { get; set; }
public string BookingFirstname { get; set; }
public string BookingSurname { get; set; }
public string BookingEmail { get; set; }
public string BookingMobileTel { get; set; }
public string BookingHomeTel { get; set; }
public int BookingAge { get; set; }
public int BookingPassengers { get; set; }
public int DepartureId { get; set; }
public int ArrivalId { get; set; }
}
DepartureId and ArrivalId are foreign keys of two other tables.
public partial class Departure
{
public int DepartureId { get; set; }
public string DepartureName { get; set; }
}
public partial class Arrival
{
public int ArrivalId { get; set; }
public int DepartureId { get; set; }
public string ArrivalName { get; set; }
}
I want to be able to get the Arrival Name and Departure Name from the option selected when the user submits a form from the Book View.
This is currently the [HttpPost] Book Action Result:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Book(Booking bk)
{
List<Departure> departures = new List<Departure>();
List<Arrival> arrivals = new List<Arrival>();
using (BookingEntities db = new BookingEntities())
{
departures = db.Departures.OrderBy(a => a.DepartureName).ToList();
if (bk != null && bk.DepartureId > 0)
{
arrivals = db.Arrivals.Where(a => a.DepartureId.Equals(bk.DepartureId)).OrderBy(a => a.ArrivalName).ToList();
}
}
ViewBag.DepartureId = new SelectList(departures, "DepartureId", "DepartureName", bk.DepartureId);
ViewBag.ArrivalId = new SelectList(arrivals, "ArrivalId", "ArrivalName", bk.ArrivalId);
string emailTo = bk.BookingEmail;
string emailBody = "Thank you for Booking with Callum Airways, " + bk.BookingFirstname + " " + bk.BookingSurname + ".\n" +
"Here is confirmation that your booking was a success. We have listed the information you entered into this email.\n" +
"Email: " + bk.BookingEmail + ",\n" +
"Mobile Tel: " + bk.BookingMobileTel + ",\n" +
"Home Tel: " + bk.BookingHomeTel + ",\n" +
"Age: " + bk.BookingAge + ",\n" +
"Number of Passengers: " + bk.BookingPassengers + ".\n\n" +
// Departure and Arrival Names
"If you want to review/delete your booking please register an account on ************.";
// All the other booking information.
using (BookingEntities db = new BookingEntities())
{
if (ModelState.IsValid)
{
db.Bookings.Add(bk);
db.SaveChanges();
ModelState.Clear();
bk = null;
MailAddress from = new MailAddress("*************#gmail.com");
MailAddress to = new MailAddress(emailTo);
MailMessage message = new MailMessage(from, to);
message.Subject = "Booking Confirmation - ********";
message.Body = emailBody;
SmtpClient smtp = new SmtpClient();
smtp.Host = "smtp.gmail.com";
smtp.Port = 587;
smtp.UseDefaultCredentials = false;
smtp.Timeout = 10000;
smtp.DeliveryMethod = SmtpDeliveryMethod.Network;
smtp.Credentials = new System.Net.NetworkCredential("*********************", "*******");
smtp.EnableSsl = true;
smtp.Send(message);
}
}
return View("~/Views/Home/Index.cshtml", bk);
}
I've been able to get the value of BookingFirstname and others but because there is just the foreign key not the name in the Booking Class. I'm not sure how I can get ArrivalName and DepartureName.
Arrival arrival = new Arrival();
arrival = db.Arrival.Where(w => w.ArrivalId.Equals(bk.ArrivalId).FirstOrDefault();
ViewBag.ArrivalName = arrival.ArrivalName;
Departure departure = new Departure();
departure = db.Departure.Where(w => w.DeaId.Equals(bk.ArrivalId).FirstOrDefault();
ViewBag.DepartureName = departure.DepartureName;
I got this class:
public class PedidosList
{
public virtual int ID_Pedido { get; set; }
public virtual int Numero { get; set; }
public virtual DateTime Fecha { get; set; }
public virtual DateTime FechaEntrega { get; set; }
public virtual int ID_Cliente { get; set; }
public virtual string Cliente { get; set; }
public virtual Decimal Bruto { get; set; }
public virtual Decimal Neto { get; set; }
public virtual Boolean Aprobado { get; set; }
public virtual string Observaciones { get; set; }
public virtual Boolean Entregado { get; set; }
}
To represent a subset of fields from a POCOs class map to an SQL Table with Entity Framework. Then use this function to make optional filter using Linq to Entities and return a IENumerable colection:
public IEnumerable<PedidosList> Pedidos_Listar(string sComprobante, Clientes MyCliente = null, DateTime? dDesde = null, DateTime? dHasta = null, bool bCumplidos = false)
{
using (var context = new OhmioEntities())
{
IEnumerable<PedidosList> query =
from Pedidos in context.Pedidos
join Clientes in context.Clientes on Pedidos.ID_Cliente equals Clientes.ID_Cliente
where Pedidos.ID_Comprobante == sComprobante
select new PedidosList {ID_Pedido = Pedidos.ID_Pedido, Fecha=Pedidos.Fecha, Aprobado=Pedidos.Aprobado, Bruto=Pedidos.Bruto, Cliente=Clientes.RazonFantasia,
FechaEntrega=Pedidos.FechaEntrega, Neto=Pedidos.Neto, Numero=Pedidos.Numero, Observaciones=Pedidos.Observaciones, Entregado=Pedidos.Entregado, ID_Cliente=Pedidos.ID_Cliente };
if (MyCliente != null) query = query.Where(i => i.ID_Cliente == MyCliente.ID_Cliente);
if (MyCliente != null) query = query.Where(i => i.ID_Cliente == MyCliente.ID_Cliente);
if (dDesde != null && dHasta != null) query = query.Where(i => i.Fecha >= dDesde && i.Fecha <= dHasta);
if (bCumplidos == false) query = query.Where(i => i.Entregado == false);
return query.ToList();
}
}
So my questions: Is this the best way to achive this? Can i make an optional filter adding a new where on a field that is on Pedidos but not on PedidosList? Example: i have to add the field ID_Cliente to PedidosList ONLY so i can filter even if i don't want it on PedidosList. Thanks!
I'm super late answering this, but I just came across it. I wouldn't be particularly uncomfortable with what you've done, I think it's mostly a matter of personal preference and what you consider readable. But if I were to write it, I would probably do something like this.
public IEnumerable<PedidosList> Pedidos_Listar(string sComprobante, Clientes MyCliente = null, DateTime? dDesde = null, DateTime? dHasta = null, bool bCumplidos = false)
{
using (var context = new OhmioEntities())
{
return from Pedidos in context.Pedidos
join Clientes in context.Clientes on Pedidos.ID_Cliente equals Clientes.ID_Cliente
where (Pedidos.ID_Comprobante == sComprobante) &&
(MyCliente == null || Pedidos.ID_Cliente == MyCliente.ID_Cliente) &&
(dDesde == null || dHasta == null || Pedidos.Fecha >= dDesde && Pedidos.Fecha <= dHasta) &&
(bCumplidos || Pedidosi.Entregado)
select new PedidosList
{
ID_Pedido = Pedidos.ID_Pedido,
Fecha = Pedidos.Fecha,
Aprobado = Pedidos.Aprobado,
Bruto = Pedidos.Bruto,
Cliente = Clientes.RazonFantasia,
FechaEntrega = Pedidos.FechaEntrega,
Neto = Pedidos.Neto,
Numero = Pedidos.Numero,
Observaciones = Pedidos.Observaciones,
Entregado = Pedidos.Entregado,
ID_Cliente = Pedidos.ID_Cliente
};
}
}
The one thing I would be pretty tempted to change on what you did, though, is that I wouldn't include your ToList() at the end. That negates some of the benefits of using IEnumerable<T>.
I'm using multiple mapping for a current query and now I need to map another object on the initial query.
For example:
public class Part {
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address {
public int Id { get; set; }
public string Street { get; set; }
public SiteOu Ou { get; set; }
}
public class SiteOu
public int Id { get; set; }
public string Name { get; set; }
}
Dapper:
connection.Query<Part, Address, Part>(sql, (part, address) => {
part.Address = address;
});
How do I get the Address class to have the SiteOu information?
This example isn't what I'm actually doing because I've actually got
Query<T1,T2,T3,T4,T5,TResult>();
I'm doing 1 select and 5 joins in my query. So hopefully I don't need more overloads of Query.
Dapper allows you to map a single row to multiple objects, so you can just map SiteOu as part of the same query.
[Test]
public void TestSplitOn()
{
var conn = new SqlConnection(#"Data Source=.\SQLEXPRESS;Integrated Security=true;Initial Catalog=db");
conn.Open();
const string sql = "select Id = 1, Name = 'My Part', " +
"Id = 2, Street = 'My Street', " +
"Id = 3, Name = 'My Site'";
var result = conn.Query<Part, Address, SiteOu, Part>(sql, (part, address, siteOu) =>
{
part.Address = address;
address.Ou = siteOu;
return part;
},
commandType: CommandType.Text
).FirstOrDefault();
Assert.That(result, Is.Not.Null);
Assert.That(result.Address, Is.Not.Null);
Assert.That(result.Address.Ou, Is.Not.Null);
}
Important Note: Dapper assumes your Id columns are named "Id" or "id", if your primary key is different or you would like to split the wide row at point other than "Id", use the optional 'splitOn' parameter.
If you have more that 5 types to map, another out of the box option is to use QueryMultiple extension. Here is an example from the Dapper docs.
var sql =
#"
select * from Customers where CustomerId = #id
select * from Orders where CustomerId = #id
select * from Returns where CustomerId = #id";
using (var multi = connection.QueryMultiple(sql, new {id=selectedId}))
{
var customer = multi.Read<Customer>().Single();
var orders = multi.Read<Order>().ToList();
var returns = multi.Read<Return>().ToList();
...
}
Also check out this thread.
Keep running into "When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id" error for the below code-block:
var accounts = DbConnection.Query<Account, Branch, Application, Account>(
"select Accounts.*, SplitAccount = '', Branches.*, SplitBranch = '', Applications.*" +
" from Accounts" +
" join Branches" +
" on Accounts.BranchId = Branches.BranchId" +
" join Applications" +
" on Accounts.ApplicationId = Applications.ApplicationId" +
" where Accounts.AccountId <> 0",
(account, branch, application) =>
{
account.Branch = branch;
account.Application = application;
return account;
}, splitOn : "SplitAccount, SplitBranch"
).AsQueryable();
I'm using SplitAccount and SplitBranch for splitOn as a workaround.
Em I missing something?
Thanks
Edit:
I have cleaned up my test a little, below is a light version of classes and a new query:
public class AccountLight
{
public int AccountId { get; set; }
public string AccountNumber { get; set; }
public BranchLight Branch { get; set; }
public ApplicationLight Application { get; set; }
}
public class BranchLight
{
public int BranchId { get; set; }
public string BranchNumber { get; set; }
}
public class ApplicationLight
{
public int ApplicationId { get; set; }
public string ApplicationCode { get; set; }
}
var accounts2 = DbConnection.Query<AccountLight, BranchLight, ApplicationLight, AccountLight>(
"select Accounts.AccountId, Accounts.AccountNumber," +
" Branches.BranchId, Branches.BranchNumber," +
" Applications.ApplicationId, Applications.ApplicationCode" +
" from Accounts" +
" inner join Branches" +
" on Accounts.BranchId = Branches.BranchId" +
" inner join Applications" +
" on Accounts.ApplicationId = Applications.ApplicationId" +
" where Accounts.AccountId <> 0",
(account, brach, application) =>
{
account.Branch = brach;
account.Application = application;
return account;
},
commandType: CommandType.Text,
splitOn: "AccountId, BranchId"
).AsQueryable();
After few hours of debugging Dapper's source code, I finally found the issue and it is quite interesting one.
When multiple splitOn fields are supplied, Dapper does a split based on comma, e.g. var splits = splitOn.Split(',').ToArray(). Then it loops through all record-set fields and split’s them up into objects based on the above array; pretty strait forward.
Now the fun part: When I supplied my splitOn fields, I had an extra SPACE after the comma, e.g. “AccountId, BranchId” and that little space was the cause. After Split(), BranchId field contained an extra space and failed to match with ANY fields in the record-set.
There are two ways around this:
Do not use extra spaces after commas; which I personally addicted
to; an old habit from SQL.
Modify Dapper’s GenerateDeserializers
method and change: var currentSplit = splits[splitIndex] to var
currentSplit = splits[splitIndex].Trim(), or something similar; that is what I did for my local copy.
Here is code snapshot:
private static Func<IDataReader, object>[] GenerateDeserializers(Type[] types, string splitOn, IDataReader reader)
{
int current = 0;
var splits = splitOn.Split(',').ToArray();
var splitIndex = 0;
Func<Type, int> nextSplit = type =>
{
var currentSplit = splits[splitIndex].Trim();
if (splits.Length > splitIndex + 1)
{
splitIndex++;
}
Update:
The above fix got merged: https://github.com/SamSaffron/dapper-dot-net/commit/399db17e5aa6f1eefaf8fdccff827020be8e6cbb