I'm doing the below join, there are many bookingActions records, but I want there to only be one BookingAction record per booking record. I want the BookingAction record that has the highest primary key value.
How would I do this?
var bookingLocationsQuery = (
from
booking in session.Query<Booking>()
join
bookingActions in session.Query<BookingAction>() on booking.Id equals bookingActions.bookingId
where
(booking.bookingAdminID == userId)
select new { booking, bookingActions }
);
A couple of suggestions. First, you should be leveraging NHibernate's many-to-one to do the join for you instead of doing it manually. It looks like you currently have something like this...
public class BookingAction
{
// ... other properties ...
public virtual int bookingId { get; set; }
}
<class name="BookingAction">
<!-- ... other properties ... -->
<property name="bookingId" />
</class>
Don't do that. Instead, you should have:
public class BookingAction
{
// ... other properties ...
public virtual Booking Booking { get; set; }
}
<class name="BookingAction">
<!-- ... other properties ... -->
<many-to-one name="Booking" column="bookingId" />
</class>
Similar advice for Booking.bookingAdminID. It should be a many-to-one to User, not just a simple property.
Second, after you make those changes, you should be able to accomplish your goal with a query like this:
var subquery = session.Query<BookingAction>()
.Where(a => a.Booking.Admin.Id == userId)
.GroupBy(a => a.Booking.Id)
.Select(g => g.Max(a => a.Id));
var bookingActions = session.Query<BookingAction>()
.Fetch(a => a.Booking)
.Where(a => subquery.Contains(a.Id));
Sorry about switching it to the chained extension method syntax - that's easier for me to work with. It's exactly equivalent to the from ... select syntax in execution.
Try using the Max() method, for sample:
var bookingLocation = session.Query<Booking>()
.Where(booking => booking.bookingAdminID == userId)
.Max(x => booking.bookingAdminID);
Related
I'm using LINQ to execute a query on a List type variable with a large amount of data (over a million). For performance purposes I'm using IEnumerable to store the results but when I try to access it there is a slight delay.
Specifically I want to see if the query produced any results, but when I use the .Count() or .Any() functions the performance drops.
I read that for IEnumerable types the execution of the query happens at the time of need, hence the delay. Is there a way to see if the IEnumerable has elements inside it without having that much delay?
This is what I'm trying to run.
IEnumerable<Entity> matchingEntities = entities.Where(e => e.Names.Any(n => myEntity.Names.Any(entityName => entityName.CompareNameObjects(n))));
and here are my classes
public class Entity
{
public string EntityIdentifier { get; set; }
public List<Name> Names { get; set; }
}
public class Name
{
public string FullName { get; set; }
public string NameType { get; set; }
public bool CompareNameObjects(Name name2)
{
return FullName == name2.FullName &&
NameType == name2.NameType;
}
}
entities is a list of all my objects and I want to check if myEntity has any Names identical with another entity in the set.
EDITED:
The data structure is similar to the 2 classes (Entity and Name). The entities are created by selecting all the entities, along with their names, from the database in XML format and then I convert the XML to a List as such:
List<Entity> entities = new List<Entity>();
using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["myCS"].ConnectionString))
{
conn.Open();
SqlCommand cmd = new SqlCommand("GetAllEntities", conn);
cmd.CommandType = CommandType.StoredProcedure;
string entitiesXml = "";
using (SqlDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
entitiesXml += rdr["XmlString"].ToString();
}
}
using (TextReader reader = new StringReader(entitiesXml))
entities = (Entity)xmlSerializer.Deserialize(reader);
conn.Close();
}
GetAllEntities (Stored Procedure):
declare #xmlString nvarchar(max) =(
select e.EntityIdentifier,
(
select n.[Full Name] as 'FullName',
n.[Name Type] as 'NameType'
from tblNames n
where e.EntityID=n.[Entity_ID]
for xml path('Name'), type
)
from tblEntities e
order by e.EntityID
for xml path('Entity')
)
select #xmlString as XmlString
Basically, you should avoid getting all data from your database then filter it with C# code. It consumes a lot of effort.
However, for quick solution, you can improve performance by preparing your conditions in a Dictionary form firstly.
// Let's say you have myEntity here
var myEntity = new Entity();
var entities = new List<Entity>();
// You should prepare the list of name that you wanna to find before you do it so that you don't have to make it repeatedly for every iteration
var names = myEntity.Names.Select(p=> p.FullName + p.NameType ).ToDictionary(p=>p, p=>p);
IEnumerable<Entity> matchingEntities = entities.Where(e => e.Names.Any(n => names.ContainsKey(n.FullName + n.NameType)));
This is just an example that may give you more idea. You can improve much more. I hope it can help you
I'm using Entity Framework Core 2.1.4 with Oracle 11 database and Devart.Data.Oracle.EFCore provider.
Database first approach.
I want to get from sequence value for ID column (primary key) on inserting without setting this explicitly every time.
So, based on similar infos with SQL Server, I did it as following:
Entity
public class Foo
{
public int Id { get; set; }
public double Value { get; set; }
}
Mapping (OnModelCreating method)
modelBuilder.HasSequence<int>("SEQ_FOOS", schema: "SCHEMA")
.StartsAt(1)
.IncrementsBy(1);
modelBuilder.Entity<Foo>(entity =>
{
entity.ForOracleToTable("FOOS");
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).ForOracleHasColumnName("ID").IsRequired().ForOracleHasDefaultValueSql("SELECT SEQ_FOO.NEXTVAL FROM DUAL");
entity.Property(e => e.Value).HasColumnName("VALUE");
});
Adding value:
using (var dbContext = new FooDbContext())
{
var foo = new Foo()
{
Value = 5
};
dbContext.Foos.Add(foo);
dbContext.SaveChanges();
}
On SaveChanges:
OracleException: ORA-01400: cannot insert NULL into ("SCHEMA"."FOOS"."ID")
I also logged EF query. As you can see, there is no ID column in insert:
INSERT INTO SCHEMA.FOOS (VALUE)
VALUES (:p0)
I was trying to use simply SEQ_FOO.NEXTVAL instead of full select or default EF methods (like HasDefaultValueSql) but nothing worked. Even if I type:
ForOracleHasDefaultValueSql("asdasd");
There is no errors with this - only the same exception as above. It seems like EF never call that SQL.
Am I missing something important? Or maybe it's internal Devart problem?
Ok, I have solution. It seems we need to use ValueGenerator. My implementation below.
Mapping
entity.Property(e => e.Id)
.ForOracleHasColumnName("ID")
.IsRequired()
.ValueGeneratedOnAdd()
.HasValueGenerator((_, __) => new SequenceValueGenerator(_defaultSchema, "SEQ_FOOS"));
SequenceValueGenerator (please note that ValueGenerator is EF Core type)
internal class SequenceValueGenerator : ValueGenerator<int>
{
private string _schema;
private string _sequenceName;
public SequenceValueGenerator(string schema, string sequenceName)
{
_schema = schema;
_sequenceName = sequenceName;
}
public override bool GeneratesTemporaryValues => false;
public override int Next(EntityEntry entry)
{
using (var command = entry.Context.Database.GetDbConnection().CreateCommand())
{
command.CommandText = $"SELECT {_schema}.{_sequenceName}.NEXTVAL FROM DUAL";
entry.Context.Database.OpenConnection();
using (var reader = command.ExecuteReader())
{
reader.Read();
return reader.GetInt32(0);
}
}
}
}
It seems to work as I needed.
Mapping:
private void FooMapping(ModelBuilder modelBuilder)
{
//modelBuilder.HasSequence<int>("SEQ_FOOS", schema: "SCHEMA")
// .StartsAt(1)
// .IncrementsBy(1);
modelBuilder.Entity<Foo>(entity =>
{
entity.ForOracleToTable("FOOS");
entity.HasKey(e => e.Id);
//entity.Property(e => e.Id).ForOracleHasColumnName("ID").IsRequired().ForOracleHasDefaultValueSql("SELECT SEQ_FOO.NEXTVAL FROM DUAL");
entity.Property(e => e.Value).HasColumnName("VALUE");
});
}
Code:
// https://www.devart.com/dotconnect/oracle/docs/?dbmonitor.html
var monitor = new OracleMonitor() { IsActive = true };
using (var dbContext = new FooModel())
{
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
var foo = new Foo()
{
Value = 5
};
dbContext.Foos.Add(foo);
dbContext.SaveChanges();
}
Check SQL generated in dbMonitor. Is that what you need?
Did not figure this out. I have similar problem on Oracle 18C - I need to fill PK in the table, PK is a NUMBER, not IDENTITY (this is obviously a defect and will be changed later on, but now I have to deal with that since I don't have rights to change DB structure, however I need to prepare CRUD demo). I don't want to use some C# value generator, but instead - DB remedy. So I tried to use the following (but it did not work - the expression is ignored):
b.HasKey(x => x.Id);
b.Property(x => x.Id).HasColumnName("C_LICENCE").IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("select round(dbms_random.value(100000, 999999)) from dual");
I suspect it's probably because int primary column is never null :) But anyway, I need to somehow force it to be generated via SQL always.
I have a table (or entity) named Cases. There is another table CaseStatus_Lookup and the primary key of this table is a foreign key in the Cases table.
What I want to do is: For every status type I want the number of count of cases. For e.g. if status = in progress , I want to know how many cases are in that status.
one other thing: I also want to filter the Cases based on UserID.
I tried several ways in LINQ but could not get vary far. I was wondering if someone could help.
try Linq .GroupBy
am assuming your entity structure
suppose your Case Entity is like
public class Case
{
public int Id{get;set;}
public int CaseStatusId{get;set;}
public int UserId{get;set;}
//navigational fields
public virtual CaseStatus CaseStatus {get;set;}
}
and suppose your CaseStatus entity is like:
public class CaseStatus
{
public int Id{get;set;}
public string Name{get;set;}
//navigational fields..
public virtual ICollection<Case> Cases{get;set;}
}
then you can do this:
using (myDbContext db = new myDbContext())
{
var query = db.Cases.GroupBy(case => case.CaseStatus.Name)
.Select(group =>
new {
Name = group.Key,
Cases= group.OrderBy(x => x.Id),
Count= group.Count()
}
).ToList();
//query will give you count of cases grouped by CaseStatus.
}
similarly you can further filter your result based on userId.
Start to explore about Linq .GroupBy
You need a function that returns the sum and takes the status as parameter :- something like below.
MyCaseStatusEnum caseStatus; //Pass your required status
int caseCount = myCases
.Where(r => r.Status == caseStatus)
.GroupBy(p => p.Status)
.Select(q => q.Count()).FirstOrDefault<int>();
I am trying to query a one to many relationship but cannot figure out how to do this. The problem I have is that the ID of the field I want to filter by lives in the join table (not the main table)...
Its probably easier to illustrate rather than explain!!
The two classes I have are
public class DbUserClient
{
public virtual string UserId { get; set; }
public virtual int ClientId { get; set; }
public virtual DateTime AssignedOn { get; set; }
public virtual DateTime? ClearedOn { get; set; }
// navigation properties
public virtual DbUser User { get; set; }
public virtual DbClient Client { get; set; }
}
and
public class DbClient
{
public virtual int ClientId {get;set;}
public virtual string EntityName { get; set; }
public virtual bool Deleted { get; set; }
// navigation properties
public ICollection<DbUserClient> UserClients { get; set; }
}
In the program I have a repository that exposes the Clients i.e.
public ObservableCollection<DbClient> Clients
{
get { return context.Clients.Local; }
}
I am binding to this which is why I am keen on querying via the Client as this will refresh my "Local" collection. However I can't seem to figure out a way to include the UserClients as well as add the "where" clause.
I have tried something like
context.Clients.Include(c => c.UserClients.Where(uc => uc.UserId == "ME"));
But this results in the following exception
"The Include path expression must refer to a navigation property defined on the type. Use dotted paths for reference navigation properties and the Select operator for collection navigation properties.
Parameter name: path"
This works but unfortunately will not update my "Local" collection
from c in context.Clients
from uc in c.UserClients
where uc.ClientId == uc.ClientId && uc.UserId == "ME"
select new { c.ClientId, c.EntityName, uc.AssignedOn };
Any suggestions on where I have gone wrong?
Cheers
Abs
EDIT I : looking at the SQL Profiler the above query generates the following SQL
SELECT
[Extent1].[ClientId] AS [ClientId],
[Extent1].[EntityName] AS [EntityName],
[Extent2].[AssignedOn] AS [AssignedOn]
FROM [dbo].[Client] AS [Extent1]
INNER JOIN [dbo].[UserClient] AS [Extent2] ON [Extent1].[ClientId] = [Extent2]. [ClientId]
WHERE ([Extent2].[ClientId] = [Extent2].[ClientId]) AND (N'ME' = [Extent2].[UserId])
This is pretty simple and more or less along the lines of what I would have written myself if I was handcrafting the SQL
However although the suggested expression below works and as you pointed out populates the Local cache
context.Clients
.Where(c => c.UserClients.Any(uc => uc.UserId == userId))
.Select(c => new { DbClient = c, DbUser = c.UserClients.Where(uc => uc.UserId == userId).FirstOrDefault() }).ToList();
it produces the following SQL. This looks alot more complicated than it needs to be and I am assuming will have performance implications
exec sp_executesql N'SELECT
[Filter2].[ClientId] AS [ClientId],
[Filter2].[EntityName] AS [EntityName],
[Filter2].[Deleted] AS [Deleted],
[Limit1].[UserId] AS [UserId],
[Limit1].[ClientId] AS [ClientId1],
[Limit1].[AssignedOn] AS [AssignedOn],
[Limit1].[ClearedOn] AS [ClearedOn]
FROM (SELECT [Extent1].[ClientId] AS [ClientId], [Extent1].[EntityName] AS [EntityName], [Extent1].[Deleted] AS [Deleted]
FROM [dbo].[Client] AS [Extent1]
WHERE EXISTS (SELECT
1 AS [C1]
FROM [dbo].[UserClient] AS [Extent2]
WHERE ([Extent1].[ClientId] = [Extent2].[ClientId]) AND ([Extent2].[UserId] = #p__linq__0)
) ) AS [Filter2]
OUTER APPLY (SELECT TOP (1)
[Extent3].[UserId] AS [UserId],
[Extent3].[ClientId] AS [ClientId],
[Extent3].[AssignedOn] AS [AssignedOn],
[Extent3].[ClearedOn] AS [ClearedOn]
FROM [dbo].[UserClient] AS [Extent3]
WHERE ([Filter2].[ClientId] = [Extent3].[ClientId]) AND ([Extent3].[UserId] = #p__linq__1) ) AS [Limit1]',N'#p__linq__0 nvarchar(4000),#p__linq__1 nvarchar(4000)',#p__linq__0=N'ME',#p__linq__1=N'ME'
EDIT II : After playing around some more, I have found a solution that seems to fulfill my requirement. Looking at the SQL Profiler, I am happy with the generated SQL. This is similar to that of my orginal query.
exec sp_executesql N'SELECT
[Extent1].[ClientId] AS [ClientId],
[Extent1].[EntityName] AS [EntityName],
[Extent1].[Deleted] AS [Deleted],
[Extent2].[UserId] AS [UserId],
[Extent2].[ClientId] AS [ClientId1],
[Extent2].[AssignedOn] AS [AssignedOn],
[Extent2].[ClearedOn] AS [ClearedOn]
FROM [dbo].[Client] AS [Extent1]
INNER JOIN [dbo].[UserClient] AS [Extent2] ON [Extent1].[ClientId] = [Extent2].[ClientId]
WHERE [Extent2].[UserId] = #p__linq__0',N'#p__linq__0 nvarchar(4000)',#p__linq__0=N'ME'
I am assuming that there is no lazy loading involved here. If someone could confirm I would be grateful
context.Clients.Join
(
context.UserClients,
c => c.ClientId,
uc => uc.ClientId,
(user, usrclient) => new { DbClient = user, DbUserClient = usrclient }
).Where(uc => uc.DbUserClient.UserId == userId).Load();
You can load the clients which have at least one user with UserId = "ME":
var clients = context.Clients
.Where(c => c.UserClients.Any(uc => uc.UserId == "ME"))
.ToList();
This loads the correct clients but no user is included.
If you include the users...
var clients = context.Clients.Include(c => c.UserClients)
.Where(c => c.UserClients.Any(uc => uc.UserId == "ME"))
.ToList();
... you'll get the correctly filtered clients but it will include all users, not only the user "ME".
In order to get the users filtered as well your last approach, the projection, is the best way:
var clientsWithUser = context.Clients
.Where(c => c.UserClients.Any(uc => uc.UserId == "ME"))
.Select(c => new
{
Client = c,
User = c.UserClients.Where(uc => uc.UserId == "ME").FirstOrDefault()
})
.ToList();
This should also update the Local collection because you are loading full entities (Client and User) in the anonymous object list.
Edit
The last query in your question is fine, although it's not really the EF way to write a Join manually when you have navigation properties. The SQL and query result is most likely identical to:
context.UserClients.Include(uc => uc.Client)
.Where(uc => uc.UserId == userId)
.Load();
The Include in this query should translate into the same INNER JOIN that your hand-written LINQ Join produces.
I have a problem with NHibernate not using my mappings configuration for eager loading a collection when I get something using HQL or Linq (Session.Query). Session.Get and Session.QueryOver is working like expected.
I'm using NHibernate 3.2. Here's the mapping of a collection in my Product mapping.
<bag name="OrderItems" inverse="true" cascade="none" lazy="false" fetch="join">
<key column="order_id" />
<one-to-many class="OrderItem" />
</bag>
and from the other side the mapping looks like this:
<many-to-one name="Product" class="Product" column="product_id" not-null="true" />
I have 4 Tests, 2 are successfull and 2 are not. They use Session.SessionFactory.Statistics to keep track of CollectionFetchCount (was OrderItems selected in 1 joined query or in a separate). The intent is to have OrderItems selected and loaded when selecting the product as OrderItems are almost always accessed as well.
LastCreated is a simple reference to the last product inserted into the DB.
[Test] /* Success */
public void Accessing_Collection_Using_Session_Get_Results_In_1_Select()
{
// Get by Id
var product = Session.Get<Product>(LastCreated.Id);
var count = product.OrderItems.Count;
Assert.AreEqual(0,statistics.CollectionFetchCount,"Product collectionfetchcount using Get");
}
[Test] /* Success */
public void Accessing_Collection_Using_Session_QueryOver_Results_In_1_Select()
{
// Get by Id
var product = Session.QueryOver<Product>().SingleOrDefault();
var count = product.OrderItems.Count;
Assert.AreEqual(0, statistics.CollectionFetchCount, "Product collectionfetchcount using QueryOver");
}
[Test] /* Fail */
public void Accessing_Collection_Using_Session_Query_Results_In_1_Select()
{
// Get by IQueryable and Linq
var product = Session.Query<Product>().Single(x => x.Id == LastCreated.Id);
var count = product.OrderItems.Count;
Assert.AreEqual(0, statistics.CollectionFetchCount, "Product collectionfetchcount using Linq");
}
[Test] /* Fail */
public void Accessing_Collection_Using_HQL_Results_In_1_Select()
{
// Get by IQueryable and Linq
var product = Session.CreateQuery("from Product where Id = :id")
.SetParameter("id",LastCreated.Id)
.UniqueResult<Product>();
var count = product.OrderItems.Count;
Assert.AreEqual(0, statistics.CollectionFetchCount, "Product collectionfetchcount using HQL");
}
Is this intended behaviour or am I doing something wrong?
HQL queries will not respect a fetch="join" set in mapping. This is because they are freeform queries, making it impossible for NH to guess how to transform them to add the join.
Linq is implemented as a wrapper for HQL, QueryOver is a wrapper for Criteria; that's why you see the different behaviors.
If you need eager loads in Linq/HQL, you will have to make them explicit in the query (using join fetch and Fetch()/FetchMany()