I have these entities:
public class Post {
public virtual int Id { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Tag {
public virtual int Id { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Comment {
public virtual int Id { get; set; }
public virtual Post Post { get; set; }
}
I want to load a Post by it's related Tags and related Comments's count by LINQ. I use this:
var posts = session
.Query<Post>()
.OrderBy(t => t.Id)
.Select(t => new {
Post = t,
CommentsCount = t.Comments.Count(),
Tags = t.Tags
}).ToList();
Do you think it is enough? or do you have any suggestion that may be better than my code? thanks.
This highly depends on your mapping and what you want to do with the fetched result. Imho eager loading Tags would speed up performance (Select N+1) if you want to access the tags list (assuming that the tags are mapped as lazy).
var posts = session
.Query<Post>()
.Fetch(t => t.Tags)
.OrderBy(t => t.Id)
.Select(t => new {
Post = t,
CommentsCount = t.Comments.Count(),
Tags = t.Tags
}).ToList();
http://ayende.com/blog/1328/combating-the-select-n-1-problem-in-nhibernate
Related
After trying numerous things, I am still unable to figure out the right way to query the following relationship using Fluent NHibernate.
This would otherwise have been an easier task if I had to write SQL queries. Hope to get some better advice to avoid N+1 issue and unoptimized auto-generated SQL queries.
I have the following relationship which goes something as below :
Tags can have media content (Images of various predefined sizes, videos, documents etc.) associated with it.
- 1 Tags can have multiple Media Items mapped to it (Lets's say images with dimension 32x32, 64x64, 600x100, 0 or more Videos)
- Every media item is mapped to a media description which helps in identifying the size and type of the media
- The same media item can be used by a different tag. Example, having a generic image for all tags which do not have any icons.
Entities:
Media
public class Media:IEntity
{
private ICollection<TagMedia> _tagMedia;
public virtual int Id { get; set; }
public virtual string FilePath { get; set; }
public virtual MediaType MediaType { get; set; }
public virtual ICollection<TagMedia> TagMedia
{
get { return _tagMedia?? (_tagMedia= new List<TagMedia>()); }
protected set { _tagMedia= value; }
}
}
Tag
public class Tag:IEntity
{
private ICollection<TagMedia> _tagMedia;
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ICollection<TagMedia> TagMedia
{
get { return _tagMedia?? (_tagMedia= new List<TagMedia>()); }
set { _tagMedia= value; }
}
}
TagMedia
public class TagMedia :IEntity
{
public virtual int Id { get; set; }
public virtual Media Media { get; set; }
public virtual Tag Tag { get; set; }
public virtual DateTime AddedOn { get; set; }
}
MediaType
public class MediaType:IEntity
{
public virtual int Id { get; set; }
public virtual string Description { get; set; }
}
Mappings
MediaMapping
public class MediaMapping : IAutoMappingOverride<Media>
{
public void Override(AutoMapping<Media> mapping)
{
mapping.Map(c => c.FileName).CustomSqlType("varchar(60)").Not.Nullable();
}
}
TagMapping
public class TagMapping : IAutoMappingOverride<Tag>
{
public void Override(AutoMapping<Tag> mapping)
{
mapping.HasMany<TagMedia>(c => c.TagMedia)
.KeyColumn("TagId")
.Cascade.SaveUpdate()
.BatchSize(25);
mapping.BatchSize(25);
mapping.DynamicUpdate();
mapping.DynamicInsert();
}
}
TagMediaMapping
public class TagMediaMapping : IAutoMappingOverride<TagMedia>
{
public void Override(AutoMapping<TagMedia> mapping)
{
mapping.Map(c=>c.AddedOn);
}
}
Query:
The following query gets the cartesian product of all media mapped to the tag and does not eliminate records other than "Icon-16x16". I expect the ORM to return no more than one row.
Any help would be highly appreciated.
_session.Query<Tag>()
.FetchMany(x => x.TagMedia)
.ThenFetch(x => x.Media)
.ThenFetch(x=>x.MediaType)
.Where(c => c.Id == id
&& c.TaxonomyMedia.Any(x=>x.Media.MediaType.Description== "Icon-16x16"))
.SingleOrDefault();
Generated SQL:
exec sp_executesql N'select *
from [Tag] Tag0_ left outer join [TagMedia] Tagme1_ on Tag0_.TagId=Tagme1_.TagId
left outer join [Media] media2_ on Tagme1_.MediaId=media2_.MediaId
left outer join [MediaType] mediatype3_ on media2_.MediaTypeId=mediatype3_.MediaTypeId
where Tag0_.TagId=#p0
and (exists (select Tagme4_.TagMediaId
from [TagMedia] Tagme4_ inner join [Media] media5_ on Tagme4_.MediaId=media5_.MediaId inner join [MediaType] mediatype6_ on media5_.MediaTypeId=mediatype6_.MediaTypeId where Tag0_.TagId=Tagme4_.TagId and mediatype6_.MediaTypeDescription=#p1))',N'#p0 int,#p1 nvarchar(4000)',#p0=102,#p1=N'Icon-16x16'
go
the SingleOrDefault function is a LINQ function and not implemented by NHibernate. Use .Take(1)
I think the solution is:
_session.Query<Tag>()
.FetchMany(x => x.TagMedia)
.ThenFetch(x => x.Media)
.ThenFetch(x=>x.MediaType)
.Where(c => c.Id == id
&& c.TaxonomyMedia.Any(x=>x.Media.MediaType.Description== "Icon-16x16"))
.Take(1)
.SingleOrDefault();
I'm having trouble querying RavenDB with even the simplest of queries, probably I'm doing something wrong, but after a few hours I just can't see it anymore. I've googled almost anything I can think of..
I have these entities:
public class User
{
public string Id { get; set; }
public string DisplayName { get; set; }
public string RealName { get; set; }
public string Email { get; set; }
public string PictureUri { get; set; }
public List<Comment> Comments { get; set; }
public List<Role> Roles { get; set; }
}
public class NewsItem
{
public string Id { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public DateTime Created { get; set; }
public string UserId { get; set; }
public List<Tag> Tags { get; set; }
public List<Comment> Comments { get; set; }
public List<WebImage> Images { get; set; }
}
I want to query these so I get a list of newsItems, but with the user information alongside it. So I read the docs and tried the LoadDocument feature, the index:
public class NewsItemIndexWithComments : AbstractIndexCreationTask<NewsItem, NewsItemIndexWithComments.Result>
{
public class Result
{
public string AuthorName { get; set; }
}
public NewsItemIndexWithComments()
{
Map = newsItems => from newsItem in newsItems
select new
{
AuthorName = LoadDocument<User>(newsItem.UserId).DisplayName
};
}
}
Which I try to use like:
var result = _documentSession.Query<NewsItemIndexWithComments.Result, NewsItemIndexWithComments>().AsProjection<NewsItemIndexWithComments.Result>().ToList();
Now I get the number of documents in my list, but the AuthorName is always null. If I don't use the AsProjection method, I won't get any results. Can anyone show me a proper example on which I can experiment further?
Thanks.
_ edit:
That helped a lot, thanks :) Now for step two, I'm sorry if I'm being a bit newbish, but you'll have to start somewhere. In the newsitems there are comments, in these comments there is another reference to the userid. You can probably guess what I want to do: I want the user info for the comments with the comments as well.
new Index:
public class NewsItemIndexWithComments : AbstractIndexCreationTask<NewsItem, NewsItemIndexWithComments.Result>
{
public class Result : NewsItem
{
public string AuthorName { get; set; }
public string AuthorId { get; set; }
}
public NewsItemIndexWithComments()
{
Map = newsItems => from newsItem in newsItems
let user = LoadDocument<User>(newsItem.UserId)
select new
{
AuthorName = user.DisplayName,
AuthorId = user.Id,
};
Store(x => x.AuthorName, FieldStorage.Yes);
Store(x => x.AuthorId, FieldStorage.Yes);
}
}
Comment class:
public class Comment
{
public string Id { get; set; }
public string Text { get; set; }
public string UserId { get; set; }
public string User { get; set; }
public DateTime Created { get; set; }
}
How can I query the comments and expand the results for that? Or is it better to create a new index just for the comments and get the user info analog to the solution above?
You're almost there, you just need to store the field you are projecting. Add this to the index constructor, after the map.
Store(x=> x.AuthorName, FieldStorage.Yes);
This is because you want it returned and available for AsProjection to find. If you just wanted to use the author name in a where or orderby, you wouldn't need it.
If you just want to include the comments in your AsProjection, you can simply index the entire object along.
Note that indexing a custom object will mean that you're not able to query on it using a .Where(). RavenDB can only query on flattened results (ints, decimals, strings, dates).
In order to, for instance, query on the title, you will need to create a seperate Property public string Title { get; set; } and map it with Title = newsItem.Title.
public class NewsItemIndexWithComments : AbstractIndexCreationTask<NewsItem, NewsItemIndexWithComments.Result>
{
public class Result : NewsItem
{
public string AuthorName { get; set; }
public string AuthorId { get; set; }
public List<Comment> Comments { get; set; }
}
public NewsItemIndexWithComments()
{
Map = newsItems => from newsItem in newsItems
let user = LoadDocument<User>(newsItem.UserId)
select new
{
AuthorName = user.DisplayName,
AuthorId = user.Id,
Comments = newsItem.Comments.
};
Store(x => x.AuthorName, FieldStorage.Yes);
Store(x => x.AuthorId, FieldStorage.Yes);
Store(x => x.Comments, FieldStorage.Yes);
}
}
I'm new to RavenDB and I have a question about joining two documents with Raven 2.0
I found this page http://daniellang.net/joining-documents-in-ravendb-2-0/ that helped me in finding a solution to join two documents.
Please see my code first (it compiles)
internal class Program
{
private static void Main(string[] args)
{
using (var store = new EmbeddableDocumentStore {DataDirectory = #"C:\temp\ravendata"}.Initialize())
{
using (var session = store.OpenSession())
{
var products = session.Query<Product, UserProducts>()
.AsProjection<UserProductProjection>()
.ToList();
}
}
}
}
public class Product
{
public string Id { get; set; }
public string Name { get; set; }
public string UserId { get; set; }
}
public class User
{
public string Id { get; set; }
public string Name { get; set; }
}
public class UserProductProjection
{
public string Id { get; set; }
public string UserName { get; set; }
public string ProductName { get; set; }
public string ProductId { get; set; }
}
internal class UserProducts : AbstractIndexCreationTask<Product, UserProductProjection>
{
public UserProducts()
{
Map = products => from product in products
select new
{
UserName = LoadDocument<User>(product.UserId).Name,
ProductName = product.Name,
ProductId = product.Id
};
Index(projection => projection.ProductId, FieldIndexing.Analyzed);
Index(projection => projection.ProductName, FieldIndexing.Analyzed);
Store(projection => projection.UserName, FieldStorage.Yes);
}
}
Unfortunately it doesn't work :(
Raven.Database.Exceptions.IndexDoesNotExistsException was unhandled
HResult=-2146233088
Message=Could not find index named: UserProducts
Source=Raven.Database
StackTrace:
at Raven.Database.DocumentDatabase.<>c__DisplayClass9a.<Query>b__90(IStorageActionsAccessor actions) in c:\Builds\RavenDB-Stable\Raven.Database\DocumentDatabase.cs:line 1100
....
I really have NO clue at all!! Google doesn't help me on this subject as well, because it is still pretty new as I found out.
If someone has a hint or a solution I would be very grateful.
While you have defined the index you haven't created it in ravendb.
See Defining a static index but basically you need....
IndexCreation.CreateIndexes(typeof(UserProducts).Assembly, documentStore);
I am new to asp.net mvc and i have problems that i think i would solve them very easy with asp.net web forms. However the project have to be in mvc, so here is the problem.
I got X tables
table1 Users
int user_ID
string username
table2 Friends
int friendshipID
int user_ID
int friend_ID
In table 2, user_ID represents the current user that is logged in. friend_ID represents ids from his friends. Its one to many relationship.
Now what i want to do, is, in user/details/ID view, show all friends of that user.
The query that i want to make is: first select the friend_IDs from table2 where user_ID=id(from querystring), then select every username from table1 where user_ID = friend_ID.
I think this is really easy in SQL, but dont know how to do it with the mvc syntax.
The controller:
//
// GET: /User/Details/5
public ViewResult Details(int id)
{
User user = db.Users.Find(id);
return View(user);
}
The view:
#model Social2.Models.User
<div class="display-label">Friends</div>
<div class="display-field">
#foreach (var friend in #Model.Friends)
{
#friend.User.username;
}
</div>
The view returns wrong results.
Models
public partial class User
{
public User()
{
this.Albums = new HashSet<Album>();
this.Friends = new HashSet<Friend>();
this.Messages = new HashSet<Message>();
this.Posts = new HashSet<Post>();
this.Groups = new HashSet<Group>();
}
public int user_ID { get; set; }
public System.Guid user_UniqueID { get; set; }
public string username { get; set; }
public virtual ICollection<Album> Albums { get; set; }
public virtual aspnet_Users aspnet_Users { get; set; }
public virtual ICollection<Friend> Friends { get; set; }
public virtual ICollection<Message> Messages { get; set; }
public virtual ICollection<Post> Posts { get; set; }
public virtual ICollection<Group> Groups { get; set; }
}
and from friends table
public partial class Friend
{
public int friendship_ID { get; set; }
public int user_fr_ID { get; set; }
public int friend_ID { get; set; }
public virtual User User { get; set; }
}
also the context
public partial class ASPNETDBEntities : DbContext
{
public ASPNETDBEntities()
: base("name=ASPNETDBEntities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public DbSet<Album> Albums { get; set; }
public DbSet<aspnet_Users> aspnet_Users { get; set; }
public DbSet<Friend> Friends { get; set; }
public DbSet<Group> Groups { get; set; }
public DbSet<Message> Messages { get; set; }
public DbSet<Picture> Pictures { get; set; }
public DbSet<Post> Posts { get; set; }
public DbSet<sysdiagram> sysdiagrams { get; set; }
public DbSet<User> Users { get; set; }
}
As the Friends list property is virtual it will not be included with your query. Try using below query to include the Friends.
public ViewResult Details(int id)
{
User user = db.Users.Include("Friends").FirstOrDefault(u => u.user_ID == id);
//Also for each friend get the User:
foreach (var friend in user.Friends.ToList())
{
friend.User = db.Users.Find(friend.friend_ID);
}
return View(user);
}
View:
<table>
#foreach (var friend in #Model.Friends)
{
<tr>
#Html.DisplayFor(modelItem => friend.User.username)
</tr>
}
</table>
Your model classes doesn't appear to be following the convention for the Entity keys. The fields "user_ID" and "friendship_ID" should be UserId and FriendId. Or if you want to key them like that annotate them with [key] attribute.
Make the ViewModel class of your own. Retrieve the data from database and build the model class object. Pass this model class to view i.e. create your view based on this model class.
I have these entities:
public class BlogPost {
public virtual int Id { get; set; }
public virtual IList<Keyword> Keywords { get; set; }
public virtual IList<BlogComment> Comments { get; set; }
}
public class BlogComment {
public virtual int Id { get; set; }
public virtual BlogPost Post { get; set; }
}
public class Keyword {
public virtual int Id { get; set; }
public virtual IList<BlogPost> BlogPosts { get; set; }
}
I want to load a paged-list of BlogPosts by their Keywords and comments-count. So I try this:
var entities = session.Query<BlogPost>()
.Where(t => t.Published)
.FetchMany(t => t.Keywords)
.OrderByDescending(t => t.UpdatedAt)
.Skip((pageNumber - 1) * pageSize).Take(pageSize)
.Select(t => new {
CommentsCount = t.Comments.Count(),
Post = t
})
.ToList();
But the folowing error occurs:
Specified method is not supported.
And when I remove .Skip((pageNumber - 1) * pageSize).Take(pageSize) it works! e.g.
var entities = session.Query<BlogPost>()
.Where(t => t.Published)
.FetchMany(t => t.Keywords)
.OrderByDescending(t => t.UpdatedAt)
// remove the below line
//.Skip((pageNumber - 1) * pageSize).Take(pageSize)
.Select(t => new {
CommentsCount = t.Comments.Count(),
Post = t
})
.ToList();
Have you any idea please to take a number of rows by including Keywords? Thanks for any suggestion.
I'm using NHibernate 3.2 mapping by code.
The problem is that the nhibernate linq provider isn't fully implemented yet.
You could move the skip / take calls to be after the ToList() but then you're going to be filtering on the entire result set rather than querying specifically for the records matching that range.
Alternatively you could use the QueryOver<> api which has proper support for Take and Skip as per this answer: https://stackoverflow.com/a/5073510/493
This should now be supported in 3.3.3.GA
http://sourceforge.net/p/nhibernate/news/2013/03/nhiberate-333ga-released/