How to retrieve entities from a join using QueryOver in NHibernate - nhibernate

I'm trying to return multiple entities from a QueryOver query. I'm doing this code in a plain text editor so there may be syntactic errors, but it should get the idea across.
public class Product
{
public virtual int ID { get; set; }
public virtual string ProductName { get; set; }
public virtual List<Category> Categories { get; set; }
public virtual List<Inventory> Inventories { get; set; }
...
}
public class Category
{
public virtual int ID { get; set; }
public virtual string CategoryName { get; set; }
public virtual string Style { get; set; }
public virtual Product Product { get; set; }
...
}
public class Inventory
{
public virtual int ID { get; set; }
public virtual List<Discount> Discounts { get; set; }
public virtual Product Product { get; set; }
public virtual bool InStock { get; set; }
...
}
public class Discount
{
public virtual int ID { get; set; }
public virtual Inventory Inventory { get; set; }
public virtual decimal DiscountAmount { get; set; }
...
}
Now my goal is to take a product ID and a couple other options to pull back a Category, Inventory, and DiscountAmount in a single query. I've gotten this to work using HQL with this query:
var query = session.CreateQuery("select category, inventory, discount.DiscountAmount"
+ " from Product product"
+ " join product.Categories category"
+ " join product.Inventories inventory"
+ " left join inventory.Discounts discount"
+ " where product.ID = :productID"
+ " and category.Style = :style"
+ " and inventory.InStock = 1");
With this query I get an list of object arrays that each have a Category entity, Inventory entity, and a DiscountAmount decimal. My goal is to use a QueryOver query to do this same query with no magic strings, but I can't get it to work. Here's what I've tried so far:
Product productAlias = null;
Category categoryAlias = null;
...
var query = session.QueryOver<Product>(() => productAlias)
.Where(() => productAlias.ID == productID)
.JoinAlias(() => productAlias.Categories, () => categoryAlias)
...
.Select(Projections.Property(() => categoryAlias.ID),
Projections.Property(() => discountAlias.Inventory),
Projections.Property(() => discount.DiscountAmount));
This query only pulls back the ID for Category, and while it does pull the full Inventory entity back it uses a full additional database query to grab it.
...
.Select(Projections.Property(() => categoryAlias),
Projections.Property(() => inventoryAlias),
Projections.Property(() => discountAlias.DiscountAmount));
This query throws a runtime exception of "Could not resolve property: categoryAlias of : Product".
...
.Select(Projections.Property(() => categoryAlias.ID).WithAlias(() => ReturnClass.Category),
Projections.Property(() => inventoryAlias.ID).WithAlias(() => ReturnClass.Inventory),
Projections.Property(() => discountAlias.DiscountAmount).WithAlias(() => ReturnClass.DiscountAmount))
.TransformUsing(Transformers.AliasToBean<ReturnClass>());
This query throws a runtime exception of "Object of type Int32 cannot be converted to type Category".
...
.Select(Projections.Property(() => categoryAlias.ID)
.TransformUsing(Transformers.AliasToBean<Category>())
This query returns a default Category entity.
So is there any way to mimic the HQL query using the QueryOver API, or is my only option to choose between HQL or making multiple queries?
Edit: To be more clear, I really want to avoid magic strings as much as possible, so I'd really prefer strongly typed QueryOver queries. Currently I'm using a QueryOver query that returns the IDs for the Category and Inventory entities and then querying for them separately, but since I have to hit those tables in the first query anyway I'd rather return them all at once.
Edit 2: The exact SQL I'm trying to achieve is
Select Category.ID, Category.CategoryName, Category.Style, (other Category columns),
Inventory.ID, Inventory.InStock, (other Inventory columns),
Discount.DiscountAmount
From Products as Product
Inner join Categories as Category ...
Where Product.ID = #productID
And Category.Style = #style
And ...

I think you should use Fetch instead of JoinAlias:
.Fetch(product => product.Categories).Eager
and don't use select: .List<Product>() // then by LINQ you can get Categories and Inventories from Product

So here is an example of "eager" loading all of the subcategories for my categories.
No N+1 when iterating over the categories collection. Future is the key here.
Category catalias = null;
var subCategories =_session.QueryOver<Category>().JoinQueryOver(x => x.SubCategories, () => catalias, JoinType.LeftOuterJoin).
Future<Category>();
var categories = _session.QueryOver<Category>().Where(x => x.ParentCategoryId == null).Future<Category>();

Related

EF Core One to One - result is returned only for one record

I am facing this problem for a several hours, could someone take a look what I am doing wrong?
Relationship One to One returns value only for one record, if there would be three Products with idCategory = 2 only one of them will have a Category. I have even tried using CategoryId instead of idCategory, still does not work.
Using EF Core 3.1.5 and .NET Core 3.1, downgrading versions do not work as well.
In Product class:
public int idCategory { get; set; }
[ForeignKey("idCategory")]
public virtual Category Category { get; set; }
In Category class:
public virtual Product Product { get; set; }
Also I could not add to DB two records in Products with same idCategory so I inserted to OnModelCreating:
builder.Entity<Product>().HasIndex(e => e.idCategory).IsUnique(false);
And Fluent API:
builder.Entity<Category>()
.HasOne<Product>(b => b.Product)
.WithOne(i => i.Category)
.HasForeignKey<Product>(b => b.idCategory);
In Controller:
[HttpGet]
[Route("products")]
public IEnumerable<Product> GetProducts()
{
return products.GetProducts();
}
In ProductRepository:
public IEnumerable<Product> GetProducts()
{
return context.Products.Include(x => x.Category)
.Where(x => x.Category.IsActive == true && x.IsActive == true).ToList();
}
In ConfigureServices:
services.AddControllers().AddNewtonsoftJson(options =>
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
Result:
JSON Result - Photo
^ Same thing happends with lazy loading, but then 1st record has a value for category, and 2nd not
Are you sure isn't One to Many relationship ?
If I understand correctly you just want return the category of a product, why didn't you add this property "CategoryId" in class "Product" and create a other class "Categories" with all Categories.
Something like this :
Class Product
public virtual Product Product { get; set; }
public Categories CategoryId { get; set; }
Class Categories
public int Id { get; set; }
public ICollection<Product> Product {get; set;}
Like this you'll can add a category for each products recorded.
You can check the ASP Net Core 3.1 documentation about Access Data :
https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/complex-data-model?view=aspnetcore-3.1

Linq Select statement inside Order By

Is it possible to write an linq select inside order by something like :
ReservationDTO obj = new ReservationDTO();
obj.bookroomview = obj.bookroomview.GroupBy(a => a.RoomFloor)
.Select(a => obj.bookroomview
.Select(x => new { Amount = a.Select(b => b.ReservationRoomID).Count(), Name = a.Key })
.OrderBy(x => x.Amount)
).ToList().AsQueryable();
My aim is to select all obj.roombookview items but sort them according to Count calculation. If it is possible how can i write it?
My view class :
public partial class Book_Room_View
{
public Nullable<int> ReservationID { get; set; }
public Nullable<System.DateTime> StartDate { get; set; }
public Nullable<System.DateTime> EndDate { get; set; }
public Nullable<int> ReservationRoomID { get; set; }
public string RoomName { get; set; }
}
if you were to order the groups by reservations, I guess this is the proper way doing it, you don't need to count specific attribute as I guess you are trying, just order by the count of the value list.
obj.bookroomview.GroupBy(a => a.RoomFloor)
.OrderBy(gr => gr.Count())
.Select(gr => new { Amount = gr.Count(), Name = gr.Key })
.AsQueryable();

How to create custom navigation property on EF Core

I m using .NET Core 2.0. I wrote, a lot for navigation property I know that does not support automatic lazy loading on EF Core at this time. I m using Microsoft approach to create navigation property. I`m trying to create a many-to-many relationship.
First create manually mapping table that is also included in ApplicationDbContext like new DbSet
public class ProductCategory
{
[Key]
public int ProductId { get; set; }
[ForeignKey("ProductId")]
public virtual Product Product { get; set; }
[Key]
public int CategoryId { get; set; }
[ForeignKey("CategoryId")]
public virtual Category Category { get; set; }
}
public class Product
{
public int Id { get; set; }
public virtual ICollection<ProductCategory> ProductCategory { get; set;
}
public class Category
{
public int Id { get; set; }
public virtual ICollection<ProductCategory> ProductCategory { get; set;
}
In OnModelCreating class
builder.Entity<ProductCategory>()
.HasKey(x => new { x.CategoryId, x.ProductId });
builder.Entity<ProductCategory>()
.HasOne(x => x.Product)
.WithMany(x => x.ProductCategory)
.HasForeignKey(x => x.ProductId);
builder.Entity<ProductCategory>()
.HasOne(x => x.Category)
.WithMany(x => x.ProductCategory)
.HasForeignKey(x => x.CategoryId);
When adding a new object in mapping table.
var productCategory = new ProductCategory
{
CategoryId = 1,
ProductId = 1
};
db.ProductCategory.Add(productCategory);
db.SaveChanges();
Item is added successfully, after that try to access Product or Category to test navigation property but receives only the current class in the mapping table.You can see the example when I`m trying to access product from category class:
model.Categories = this._categoryService
.All()
.Include(x => x.ProductCategory)
.ToList();
Product class is null?
It's because including navigation property automatically includes the inverse navigation property, but nothing more. You need to specifically ask for that using ThenInclude.
Assuming All method returns IQueryable<Category>, something like this:
model.Categories = this._categoryService
.All()
.Include(x => x.ProductCategory)
.ThenInclude(x => x.Product)
.ToList();
Note that this will also automatically include (load) the ProductCategory collection property of the Product entity.

NHibernate Future<T> analyse

I have a code to query/paginate a ProductPrice list... My ProductPrice Object has a Product...
The code works fine...
But looking at log4net I have 2 SELECT happening...
Is that right?
My code :
var query = Session.QueryOver<ProductPrice>();
Product product = null;
query.JoinQueryOver(mg => mg.Product, () => product);
query.WhereRestrictionOn(() => product.Name).IsLike("Asics", MatchMode.Anywhere)
.OrderBy(() => product.Name);
var rowCountQuery = query.ToRowCountQuery();
totalCount = rowCountQuery.FutureValue<int>().Value;
var firstResult = pageIndex * pageSize;
ProductViewModel productViewModel = null;
var productsViewModel = query
.SelectList(l => l
.Select(() => product.Id).WithAlias(() => productViewModel.Id)
.Select(() => product.Name).WithAlias(() => productViewModel.Name)
.Select(mg => mg.Price).WithAlias(() => productViewModel.Price))
.TransformUsing(Transformers.AliasToBean<ProductViewModel>())
.Skip(firstResult)
.Take(pageSize)
.Future<ProductViewModel>();
edited
ProductPrice:
public class ProductPrice : Entity
{
public virtual string Sku { get; set; }
public virtual decimal Price { get; set; }
public virtual Product Product { get; set; }
...
}
Product:
public class ProductPrice : Entity
{
public virtual string Name { get; set; }
public virtual IList<ProductPrice> Prices { get; set; }
...
}
The mapping is generated by Fluent NHibernate...
Thanks
You're doing the ".Value" too soon to get the row count. You should keep it like:
var rowCountQuery = query.ToRowCountQuery();
var rowCount = rowCountQuery.FutureValue<int>();
This way the query is not really executed, just deferred.
After the main query, which seems ok, you may now really fetch the row count integer, and both queries should be sent at the same time to the database:
totalCount = rowCount.Value;

get property count with entity using nhibernate

can I am hoping someone can point me to the right direction on how to get count of a property and the entity using a single trip to sql.
public class Category
{
public virtual int Id { get; private set; }
public virtual string Description { get; set; }
public virtual IList<Article> Articles { get; set; }
public virtual int ArticlesCount { get; set; }
public Category()
{
Articles=new List<Article>();
}
public virtual void AddArticle(Article article)
{
article.Category = this;
Articles.Add(article);
}
public virtual void RemoveArticle(Article article)
{
Articles.Remove(article);
}
}
public class CategoryMap:ClassMap<Category>
{
public CategoryMap()
{
Table("Categories");
Id(x => x.Id).GeneratedBy.Identity();
Map(x => x.Description);
HasMany(x => x.Articles).KeyColumn("CategoryId").Fetch.Join();
Cache.ReadWrite();
}
}
My goal is to get the all Categories and the count of the associated articles if there is any.
I have tried this
ICriteria crit = session.CreateCriteria(typeof(Category));
crit.SetProjection(Projections.ProjectionList()
.Add(Projections.Property("Description"), "Description")
.Add(Projections.Count("Articles"), "ArticlesCount"));
crit.SetResultTransformer(Transformers.AliasToBean (typeof(Category)));
var aa=crit.List();
unfortunately the generated sql shows the count of the Category table not the Articles list.
Thanks
You could use a multi-query, multiple sql statements but it is one trip to the database.
Here is an example from the nhibernate documentation:
https://www.hibernate.org/hib_docs/nhibernate/1.2/reference/en/html/performance.html
IMultiQuery multiQuery = s.CreateMultiQuery()
.Add(s.CreateQuery("from Item i where i.Id > ?")
.SetInt32(0, 50).SetFirstResult(10))
.Add(s.CreateQuery("select count(*) from Item i where i.Id > ?")
.SetInt32(0, 50));
IList results = multiQuery.List();
IList items = (IList)results[0];
long count = (long)((IList)results[1])[0];
Maybe not exactly what you were thinking.