Limit child entities without limiting parent entities - NHibernate - c#

I'm trying to limit the result set of a mapped collection.
Here is a simple model:
public class Table1 {
public virtual long Id { get; set; }
public virtual IList<Table2> Table2s { get; set; }
}
public class Table2 {
public virtual long Id { get; set; }
public virtual long Table1Id { get; set; }
public virtual Table1 Table1 { get; set; }
public virtual string Field { get; set; }
}
public class Table1Map : ClassMap<Table1> {
public Table1Map () {
Table("Table1");
Id(x => x.Id).Column("Id").Not.Nullable().CustomType("Int64").GeneratedBy.Native();
HasMany<Table2>(x => x.Table2s).Inverse().Not.LazyLoad().KeyColumns.Add("Table1Id").Fetch.Join();
}
}
public class Table2Map : ClassMap<Table2> {
public Table2Map () {
Table("Table2");
Id(x => x.Id).Column("Id").Not.Nullable().CustomType("Int64").GeneratedBy.Native();
Map(x => x.Table1Id).Column("Table1Id").Not.Nullable().CustomType("Int64");
Map(x => x.Field).Column("Field").Not.Nullable().CustomType("AnsiString").Length(25);
References<Table1>(x => x.Table1, "Table1Id").Cascade.None();
}
}
I want to select all Table1s. I also want to select all Table2s that meet a certain criteria (Table2.Field = 'value'), but I don't want to limit my Table1s, so select null Table2s if they don't meet the criteria. If I want to do this in SQL I'd do the following:
SELECT *
FROM
Table1
LEFT OUTER JOIN Table2 ON Table1.Id = Table2.Table1Id
WHERE
Table2.Field = 'value' or Table2.Field IS NULL
How should I structure my NHibernate query to achieve the desired result? I'd like a list of Table1s, and within each Table1 I'd like either an empty list of Table2s (because no Table2s met the criteria), or a list of Table2s that met the creteria.
I'm trying something like the following, but this will obviously not work:
List<Table1> result = new List<Table1>();
IQueryable<Table1> query = session.Query<Table1>();
if (value != null) {
query = query.Where(x => x.Table2s.Field == value);
}
query = query.OrderBy(x => x.Id);
result = query.ToList();

I think this is not possible the way you do this. Hibernate loads the complete entity with all its properties (if not lazyloading is activated).
What should hibernate do, if you save such a loaded entity of type table1 without all table2's?
You should create some kind of viewobject (dvo) that contains the relevant parts of table1 and a list of table2 childs that fit the criteria. The select could possible be done by projection.

There is pretty good documentation - 16.4. Associations
http://nhibernate.info/doc/nh/en/index.html#queryqueryover-associations
The QueryOver syntax would look like this
IQueryOver<Table1, Table2> myQuery =
session.QueryOver<Table1>()
.Left.JoinQueryOver<Table2>(t => t.Table2s)
.Where(
Restrictions.Or(
Restrictions.On<Table2>((t2) => t2.ID).IsNull,
Restrictions.On<Table2>((t2) => t2.Field).IsLike("value")
)
);
var list = myQuery.List<Table1>();
then the list will return the collection of all combinations meeting the criteria. (Later order by or distinct or other porjections could be added...)

Related

Linq method - Return rows only found in another table

I want to return the Tags from the Tag table that are only found in the TagRecipe table. How can I do this?
var dataTags = await _context.Tags
.Include(tc => tc.TagCategory)
.ToListAsync();
public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<TagRecipe> TagRecipes { get; set; }
public int TagCategoryID { get; set; }
public TagCategory TagCategory { get; set; }
}
public class TagRecipe
{
public int TagId { get; set; }
public int RecipeId { get; set; }
public Tag Tag { get; set; }
public Recipe Recipe { get; set; }
}
Thank you
Try this
var dataTags = await _context.TagRecipe
.Include(tc => tc.Tag.TagCategory)
.Select(i=> i.Tag)
.ToListAsync();
or you can use this syntax if you like it more
var dataTags = await _context.TagRecipe
.Include(t => t.Tag)
.ThenInclude(tc => tc.TagCategory)
.Select(i=> i.Tag)
.ToListAsync();
An alternative starting at table Tags using Join that will return a result without duplicates.
var dataTags = db.Tags
.Join(db.TagRecipes, tag => tag.Id, tagRecipe => tagRecipe.TagId, (tag, tagRecipe) => tag)
.Include(tag => tag.TagCategory)
.ToLookup(tag => tag.Id) // client-side from here
.Select(grouping => grouping.First()) // to make distinct
.ToList();
Will generate a straight-forward SQL
SELECT "t"."Id", "t"."Name", "t"."TagCategoryId", "t1"."Id", "t1"."Name"
FROM "Tags" AS "t"
INNER JOIN "TagRecipes" AS "t0" ON "t"."Id" = "t0"."TagId"
INNER JOIN "TagCategories" AS "t1" ON "t"."TagCategoryId" = "t1"."Id"
It is possible to use .Distinct in the above expression for removing duplicates instead of using grouping, but that will create a more complex SQL.
Table TagRecipes seems to be a join table in a many-to-many between table Tags and table Recipes. The latter is not included in the question, but I added it during my tests.
Please note that in EF Core 5, many-to-many relations may be created without an entity class for the join table.

Left outer join using LINQ Query Syntax EF Core C#

I have a question in regards with the below,
Left outer join of two tables who are not connected through Foreign Key.
Order by the results matched in second table.
I would like this to be done in LINQ Query method syntax as I am adding lots of conditions depending on the input provided along with skip and limit.
If we have below Product and Favorite tables
So the output that I would like to have is:
meaning with the favorites as part of first set and which are not favorites should be behind them. Below are the tries that I did.
I am able to join the tables get the output but not sure how I can make sure that in the first page I get all the favs.
This answer was very near to what I thought but it gets the result and then does the ordering which will not be possible in my case as I am doing pagination and using IQueryable to get less data.
Group Join and Orderby while maintaining previous query
Open to any solutions to achieve the same.
[Table("Product")]
public class ProductModel
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ProductId { get; set; }
public string ProductName {get; set;}
public bool IsFavorite { get; set; }
}
[Table("UserFavorite")]
public class UserFavoriteModel
{
[Required]
public Guid UserId { get; set; }
[Required]
public Guid Identifier { get; set; }
[Required]
public FavoriteType Type { get; set; }
}
// Gets products
private async Task<List<ProductModel>> GetProductsAsync(
Guid categoryId,
Guid subCategoryId,
int from,
int limit)
{
var query = _context.Products.AsQueryable();
if (!string.IsNullOrEmpty(categoryId))
query = query.Where(product => product.CategoryId == categoryId);
if (!string.IsNullOrEmpty(subCategoryId))
query = query.Where(product => product.SubCategoryId == subCategoryId);
query = query.Skip(from).Take(limit);
var products = await query.ToListAsync();
query = query.GroupJoin(
_context.Favorites.AsNoTracking()
.Where(favorite => favorite.Type == FavoriteType.FASHION)
// This user Id will come from context just adding for overall picture.
.Where(favorite => favorite.UserId == userId),
//This orderby if I add will not make any difference.
//.OrderByDescending(favorite => favorite.Identifier),
v => v.ProductId,
f => f.Identifier,
(product, fav) => new { product, fav }).
SelectMany(x => x.Fav.DefaultIfEmpty(),
(x, y) => SetFavorite(x.Project, y));
}
private static ProductModel SetFavorite(ProductModel v, UserFavoriteModel si)
{
v.IsFavorite = (si != null);
return v;
}
I would do something like this:
var query =
_context.Products.AsQueryable().Select(p => new ProductModel {
ProductId = p.ProductId,
ProductName = p.ProductName,
IsFavorite =
_context.Favorites.Any(f =>
f.Identifier = p.ProductId &&
f.Type == FavoriteType.FASHION &&
f.UserId == userId
)
}).OrderByDescending(favorite => favorite.Identifier);

Get Table A record if Table B has a match in a search

I have two tables
CREATE TABLE RetailGroup(
Id int IDENTITY(1,1),
GroupName nvarchar(50),
)
CREATE TABLE RetailStore(
Id int IDENTITY(1,1),
StoreName nvarchar(100),
RetailGroupId int
)
Where RetailGroupId in RetailStore is referencing RetailGroup ID. I am trying to create a search function where I can search for both RetailGroup and RetailsStores. If I get a matching RetailStore I want to return the Group it is tied to and the matching Store record. If I get a matching Group, I want the group record but no retail stores.
I tried to do it the following way:
public List<RetailGroup> SearchGroupsAndStores(string value)
{
return _context.RetailGroups.Where(group => group.GroupName.Contains(value)).Include(group => group.RetailStores.Where(store => store.StoreName.Contains(value))).ToList();
}
But this is wrong because include should not be used for selection.
Here is my entity framework model for groups
public class RetailGroup
{
[Key]
public int Id { set; get; }
[MaxLength(50)]
public String GroupName { set; get; }
//Relations
public ICollection<RetailStore> RetailStores { set; get; }
}
And here is the one for the store
public class RetailStore
{
[Key]
public int Id { get; set; }
[MaxLength(100)]
public string StoreName { get; set; }
[ForeignKey("RetailGroup")]
public int RetailGroupId { get; set; }
//Relations
public RetailGroup RetailGroup { get; set; }
public ICollection<EGPLicense> Licenses { get; set; }
}
How do I create my LINQ to get the results I am looking for ?
The query returning the desired result with projection is not hard:
var dbQuery = _context.RetailGroups
.Select(rg => new
{
Group = rg,
Stores = rg.RetailStores.Where(rs => rs.StoreName.Contains(value))
})
.Where(r => r.Group.GroupName.Contains(value) || r.Stores.Any());
The problem is that you want the result to be contained in the entity class, and EF6 neither supports projecting to entity types nor filtered includes.
To overcome these limitations, you can switch to LINQ to Objects context by using AsEnumerable() method, which at that point will effectively execute the database query, and then use delegate block to extract the entity instance from the anonymous type projection, bind the filtered collection to it and return it:
var result = dbQuery
.AsEnumerable()
.Select(r =>
{
r.Group.RetailStores = r.Stores.ToList();
return r.Group;
})
.ToList();
Try using an OR condition to filter both the group name and the store name.
return _context.RetailGroups
.Where(group => group.GroupName.Contains(value) || group.RetailStores.Any(store => store.StoreName.Contains(value)))
.Include(group => group.RetailStores.Where(store => store.StoreName.Contains(value)))
.ToList();
Another option would be doing 2 searches, one for the groups and other for the stores. The problem here would be geting a unique set of groups from both results.
List<RetailGroup> retailGroups = new List<RetailGroup>();
var groupSearchResults = _context.RetailGroups
.Where(group => group.GroupName.Contains(value))
.Include(group => group.RetailStores.Where(store => store.StoreName.Contains(value)))
.ToList();
var storeSearchResults = _context.RetailStores
.Where(store => store.StoreName.Contains(value))
.Select(store => store.RetailGroup)
.ToList();
retailGroups.AddRange(groupSearchResults);
retailGroups.AddRange(storeSearchResults);
// Remove duplicates by ID
retailGroups = retailGroups
.GroupBy(group => group.Id)
.Select(group => group.First());
Either use OR condition and have the search in one statement :
public List<RetailGroup> SearchGroupsAndStores(string value)
{
return _context.RetailGroups
.Where(rg => rg.GroupName.Contains(value) || rg.RetailStores.Any(rs => rs.StoreName.Contains(value)))
.Include(rg => rg.RetailStores.Where(rs => rs.StoreName.Contains(value)).ToList())
.ToList();
}
Or you can split the search, first look for RetailGroups then search RetailStores and return their RetailGroup :
public List<RetailGroup> SearchGroupsAndStores(string value)
{
List<RetailGroup> searchResults = new List<RetailGroup>();
searchResults.AddRange(_context.RetailGroups.Where(rg => rg.GroupName.Contains(value)).ToList());
searchResults.AddRange(_context.RetailStores.Where(rs => rs.StoreName.Contains(value)).Select(rs => rs.RetailGroup).ToList());
}

Select categories with subcategories [duplicate]

I have the following entity:
public class Item
{
public int Id { get; set; }
public int? ParentId { get; set; }
public Item Parent { get; set; }
public List<Item> Children { get; set; }
public double PropertyA { get; set; }
public double PropertyB { get; set; }
...
}
Now I want to query the database and retrieve data of all the nested children.
I could achieve this by using Eager Loading with Include():
var allItems = dbContext.Items
.Include(x => Children)
.ToList();
But instead of Eager Loading, I want to do the following projection:
public class Projection
{
public int Id { get; set; }
public List<Projection> Children { get; set; }
public double PropertyA { get; set; }
}
Is it possible to retrieve only the desired data with a single select?
We are using Entity Framework 6.1.3.
Edit:
This is what I have tried so far.
I really don't know how to tell EF to map all child Projection the same way than their parents.
An unhandled exception of type 'System.NotSupportedException' occurred in EntityFramework.SqlServer.dll
Additional information: The type 'Projection' appears in two structurally incompatible initializations within a single LINQ to Entities query. A type can be initialized in two places in the same query, but only if the same properties are set in both places and those properties are set in the same order.
var allItems = dbContext.Items
.Select(x => new Projection
{
Id = x.Id,
PropertyA = x.PropertyA,
Children = x.Children.Select(c => new Projection()
{
Id = c.Id,
PropertyA = c.PropertyA,
Children = ???
})
})
.ToList();
Generally speaking, you can't load a recursive structure of unknown unlimited depth in a single SQL query, unless you bulk-load all potentially relevant data irregardless whether they belong to the requested structure.
So if you just want to limit the loaded columns (exclude PropertyB) but its ok to load all rows, the result could look something like the following:
var parentGroups = dbContext.Items.ToLookup(x => x.ParentId, x => new Projection
{
Id = x.Id,
PropertyA = x.PropertyA
});
// fix up children
foreach (var item in parentGroups.SelectMany(x => x))
{
item.Children = parentGroups[item.Id].ToList();
}
If you want to limit the number of loaded rows, you have to accept multiple db queries in order to load child entries. Loading a single child collection could look like this for example
entry.Children = dbContext.Items
.Where(x => x.ParentId == entry.Id)
.Select(... /* projection*/)
.ToList()
I see only a way with first mapping to anonymous type, like this:
var allItems = dbContext.Items
.Select(x => new {
Id = x.Id,
PropertyA = x.PropertyA,
Children = x.Children.Select(c => new {
Id = c.Id,
PropertyA = c.PropertyA,
})
})
.AsEnumerable()
.Select(x => new Projection() {
Id = x.Id,
PropertyA = x.PropertyA,
Children = x.Children.Select(c => new Projection {
Id = c.Id,
PropertyA = c.PropertyA
}).ToList()
}).ToList();
A bit more code but will get the desired result (in one database query).
Let's say we have the following self-referencing table:
public class Person
{
public Person()
{
Childern= new HashSet<Person>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int? ParentId { get; set; }
[StringLength(50)]
public string Name{ get; set; }
public virtual Person Parent { get; set; }
public virtual ICollection<Person> Children { get; set; }
}
And for some point of time you need to get all grandsons for specific persons.
So, first of all I will create stored procedure(using code-first migration) to get all persons in the hierarchy for those specific persons:
public override void Up()
{
Sql(#"CREATE TYPE IdsList AS TABLE
(
Id Int
)
GO
Create Procedure getChildIds(
#IdsList dbo.IdsList ReadOnly
)
As
Begin
WITH RecursiveCTE AS
(
SELECT Id
FROM dbo.Persons
WHERE ParentId in (Select * from #IdsList)
UNION ALL
SELECT t.Id
FROM dbo.Persons t
INNER JOIN RecursiveCTE cte ON t.ParentId = cte.Id
)
SELECT Id From RecursiveCTE
End");
}
public override void Down()
{
Sql(#" Drop Procedure getChildIds
Go
Drop Type IdsList
");
}
After that you can use Entity Framework to load the ids(you could modify stored procedure to return persons instead of only returning ids) of persons under the passed persons(ex grandfather) :
var dataTable = new DataTable();
dataTable.TableName = "idsList";
dataTable.Columns.Add("Id", typeof(int));
//here you add the ids of root persons you would like to get all persons under them
dataTable.Rows.Add(1);
dataTable.Rows.Add(2);
//here we are creating the input parameter(which is array of ids)
SqlParameter idsList = new SqlParameter("idsList", SqlDbType.Structured);
idsList.TypeName = dataTable.TableName;
idsList.Value = dataTable;
//executing stored procedure
var ids= dbContext.Database.SqlQuery<int>("exec getChildIds #idsList", idsList).ToList();
I hope my answer will help others to load hierarchical data for specific entities using entity framework.

Entity Framework - Selective Condition on Included Navigation Property

Assume I have these simplified EF generated entities...
public class PurchaseOrder
{
public int POID {get;set;}
public int OrderID {get;set;}
public int VendorID {get;set;}
public IEnumerable<Order> Orders {get;set;}
}
public class Order
{
public int OrderID {get;set;}
public decimal Price {get;set;}
public IEnumerable<Item> Items {get;set;}
}
public class Item
{
public int OrderID {get; set;}
public string SKU {get;set;}
public int VendorID {get;set;}
public Order Order {get;set;}
}
Business Logic:
An order can have multiple POs, one for each distinct vendor on the order (vendors are determined at the Item level).
How Can I selectively Include Child Entities?
When querying for POs, I want to automatically include child entites for Order and Item.
I accomplish this, using Include()...
Context.PurchaseOrders.Include("Orders.Items");
This does it's job and pulls back related entities, but, I only want to include Item entities whose VendorID matches the VendorID of the PurchaseOrder entity.
With traditional SQL, I'd just include that in the JOIN condition, but EF builds those internally.
What LINQ magic can I use tell EF to apply the condition, without manually creating the JOINs between the entities?
You can't selectively pull back certain child entities that match a certain condition. The best you can do is manually filter out the relevant orders yourself.
public class PurchaseOrder
{
public int POID {get;set;}
public int OrderID {get;set;}
public int VendorID {get;set;}
public IEnumerable<Order> Orders {get;set;}
public IEnumerable<Order> MatchingOrders {
get {
return this.Orders.Where(o => o.VendorId == this.VendorId);
}
}
}
You can't. EF doesn't allow conditions for eager loading. You must either use multiple queries like:
var pos = from p in context.PurchaseOrders.Include("Order")
where ...
select p;
var items = from i in context.Items
join o in context.Orders on new { i.OrderId, i.VendorId}
equals new { o.OrderId, o.PurchaseOrder.VendorId }
where // same condition for PurchaseOrders
select i;
Or you can use projection in single query:
var data = from o in context.Orders
where ...
select new
{
Order = o,
PurchaseOrder = o.PurchaseOrder,
Items = o.Items.Where(i => i.VendorId == o.PurchaseOrder.VendorId)
};
You could use the IQueryable-Extensions here:
https://github.com/thiscode/DynamicSelectExtensions
The Extension builds dynamically an anonymous type. This will be used for projection as described by #Ladislav-Mrnka.
Then you can do this:
var query = query.SelectIncluding( new List<Expression<Func<T,object>>>>(){
//Example how to retrieve only the newest history entry
x => x.HistoryEntries.OrderByDescending(x => x.Timestamp).Take(1),
//Example how to order related entities
x => x.OtherEntities.OrderBy(y => y.Something).ThenBy(y => y.SomeOtherThing),
//Example how to retrieve entities one level deeper
x => x.CollectionWithRelations.Select(x => x.EntityCollectionOnSecondLevel),
//Of course you can order or subquery the deeper level
//Here you should use SelectMany, to flatten the query
x => x.CollectionWithRelations.SelectMany(x => x.EntityCollectionOnSecondLevel.OrderBy(y => y.Something).ThenBy(y => y.SomeOtherThing)),
});

Categories

Resources