I have a chat object with participants as a list. I need to enhance the participants' records with further metadata (eg. fullname, company name, instead of just the user ids).
UPDATE:
ChatLastMessage_Index: is queried only by passing an ExternalChatId or a participantId (hence find a specific chat or all the chats for a specific participant)
Profile_Index: is queried passing a person's name
The goal here would be to decorate the chat's participants with the persons' metadata.
Currently the index used to retrieve a chat and its last message is:
public class ChatLastMessage_Index : AbstractIndexCreationTask<ChatMessage, ChatLastMessage_Index.Result>
{
public class Result
{
public string Id { get; set; }
public string ExternalChatId { get; set; }
public List<Participant> Participants { get; set; }
// last message's information:
public string LastMessageUuid { get; set; }
}
public ChatLastMessage_Index()
{
Map = messages => from message in messages
let chat = LoadDocument<Chat>(message.ChatId)
select new Result
{
Id = chat.Id,
ExternalChatId = chat.ExternalChatId,
Participants = chat.Participants,
LastMessageUuid = message.Id,
LastMessageCreatedBy = message.CreatedBy
};
Reduce = results => from result in results
group result by result.ExternalChatId into g
select new Result
{
Id = g.First().Id,
ExternalChatId = g.First().ExternalChatId,
Participants = g.First().Participants,
LastMessageUuid = g.OrderByDescending(m => m.LastMessageCreatedAt).First().LastMessageUuid
};
}
}
As we have another index (Profile_Index) that retrieves all the participants metadata needed, I was wondering whether would it be possible to re use its results inside the ChatLastMessage_Index in order to avoid having to copy/paste its logic.
Below the second index used to retrieve the participants metadata:
public class Profile_Index : AbstractMultiMapIndexCreationTask<PersonProfile_Index.ReducedResult>
{
public class ReducedResult
{
public string Id { get; set; }
public string Name { get; set; }
public string JobTitle { get; set; }
public string ProfileImageUrl { get; set; }
public string OrganizationId { get; set; }
public string OrganizationName { get; set; }
public string OrgCountryName { get; set; }
public List<LanguageProficiency> Languages { get; set; }
}
public PersonProfile_Index()
{
AddMap<Person>(
persons => from person in persons
select new ReducedResult
{
Id = person.Id,
Name = person.Name,
Languages = person.Languages,
ProfileImageUrl = person.ProfileImage.Url
JobTitle = null,
OrganizationId = null,
OrganizationName = null,
OrgCountryName = null
});
AddMap<Employment>(
employments => from empl in employments
let org = LoadDocument<Organization>(empl.OrganizationId)
let countryName = LoadDocument<Country>(org.CountryOfResidenceId).Name
select new ReducedResult
{
Id = empl.PersonId,
Name = null,
Languages = null,
ProfileImageUrl = null,
JobTitle = empl.JobTitle,
OrganizationId = org.Id,
OrganizationName = org.Name,
OrgCountryName = countryName
});
Reduce = results => from result in results
group result by result.Id into g
select new ReducedResult
{
Id = g.Key,
Name = g.Select(x => x.Name).FirstOrDefault(p => p != null),
JobTitle = g.Select(x => x.JobTitle).FirstOrDefault(p => p != null),
Languages = g.Select(x => x.Languages).FirstOrDefault(p => p != null),
ProfileImageUrl = g.Select(x => x.ProfileImageUrl).FirstOrDefault(p => p != null),
OrganizationId = g.Select(x => x.OrganizationId).FirstOrDefault(p => p != null),
OrganizationName = g.Select(x => x.OrganizationName).FirstOrDefault(p => p != null),
OrgCountryName = g.Select(x => x.OrgCountryName).FirstOrDefault(p => p != null)
};
}
}
Related
I have a many to many relationship:
A post can have many tags
A tag can have many posts
Models:
public class Post
{
public virtual string Title { get; set; }
public virtual string Content{ get; set; }
public virtual User User { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
}
public class Tag
{
public virtual string Title { get; set; }
public virtual string Description { get; set; }
public virtual User User { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
I want to count all posts that belong to multiple tags but I don't know how to do this in NHibernate. I am not sure if this is the best way to do this but I used this query in MS SQL:
SELECT COUNT(*)
FROM
(
SELECT Posts.Id FROM Posts
INNER JOIN Users ON Posts.UserId=Users.Id
LEFT JOIN TagsPosts ON Posts.Id=TagsPosts.PostId
LEFT JOIN Tags ON TagsPosts.TagId=Tags.Id
WHERE Users.Username='mr.nuub' AND (Tags.Title in ('c#', 'asp.net-mvc'))
GROUP BY Posts.Id
HAVING COUNT(Posts.Id)=2
)t
But NHibernate does not allow subqueries in the from clause. It would be great if someone could show me how to do this in HQL.
I found a way of how to get this result without a sub query and this works with nHibernate Linq. It was actually not that easy because of the subset of linq expressions which are supported by nHibernate... but anyways
query:
var searchTags = new[] { "C#", "C++" };
var result = session.Query<Post>()
.Select(p => new {
Id = p.Id,
Count = p.Tags.Where(t => searchTags.Contains(t.Title)).Count()
})
.Where(s => s.Count >= 2)
.Count();
It produces the following sql statment:
select cast(count(*) as INT) as col_0_0_
from Posts post0_
where (
select cast(count(*) as INT)
from PostsToTags tags1_, Tags tag2_
where post0_.Id=tags1_.Post_id
and tags1_.Tag_id=tag2_.Id
and (tag2_.Title='C#' or tag2_.Title='C++'))>=2
you should be able to build your user restriction into this, I hope.
The following is my test setup and random data which got generated
public class Post
{
public Post()
{
Tags = new List<Tag>();
}
public virtual void AddTag(Tag tag)
{
this.Tags.Add(tag);
tag.Posts.Add(this);
}
public virtual string Title { get; set; }
public virtual string Content { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
public virtual int Id { get; set; }
}
public class PostMap : ClassMap<Post>
{
public PostMap()
{
Table("Posts");
Id(p => p.Id).GeneratedBy.Native();
Map(p => p.Content);
Map(p => p.Title);
HasManyToMany<Tag>(map => map.Tags).Cascade.All();
}
}
public class Tag
{
public Tag()
{
Posts = new List<Post>();
}
public virtual string Title { get; set; }
public virtual string Description { get; set; }
public virtual ICollection<Post> Posts { get; set; }
public virtual int Id { get; set; }
}
public class TagMap : ClassMap<Tag>
{
public TagMap()
{
Table("Tags");
Id(p => p.Id).GeneratedBy.Native();
Map(p => p.Description);
Map(p => p.Title);
HasManyToMany<Post>(map => map.Posts).LazyLoad().Inverse();
}
}
test run:
var sessionFactory = Fluently.Configure()
.Database(FluentNHibernate.Cfg.Db.MsSqlConfiguration.MsSql2012
.ConnectionString(#"Server=.\SQLExpress;Database=TestDB;Trusted_Connection=True;")
.ShowSql)
.Mappings(m => m.FluentMappings
.AddFromAssemblyOf<PostMap>())
.ExposeConfiguration(cfg => new SchemaUpdate(cfg).Execute(false, true))
.BuildSessionFactory();
using (var session = sessionFactory.OpenSession())
{
var t1 = new Tag() { Title = "C#", Description = "C#" };
session.Save(t1);
var t2 = new Tag() { Title = "C++", Description = "C/C++" };
session.Save(t2);
var t3 = new Tag() { Title = ".Net", Description = "Net" };
session.Save(t3);
var t4 = new Tag() { Title = "Java", Description = "Java" };
session.Save(t4);
var t5 = new Tag() { Title = "lol", Description = "lol" };
session.Save(t5);
var t6 = new Tag() { Title = "rofl", Description = "rofl" };
session.Save(t6);
var tags = session.Query<Tag>().ToList();
var r = new Random();
for (int i = 0; i < 1000; i++)
{
var post = new Post()
{
Title = "Title" + i,
Content = "Something awesome" + i,
};
var manyTags = r.Next(1, 3);
while (post.Tags.Count() < manyTags)
{
var index = r.Next(0, 6);
if (!post.Tags.Contains(tags[index]))
{
post.AddTag(tags[index]);
}
}
session.Save(post);
}
session.Flush();
/* query test */
var searchTags = new[] { "C#", "C++" };
var result = session.Query<Post>()
.Select(p => new {
Id = p.Id,
Count = p.Tags.Where(t => searchTags.Contains(t.Title)).Count()
})
.Where(s => s.Count >= 2)
.Count();
var resultOriginal = session.CreateQuery(#"
SELECT COUNT(*)
FROM
(
SELECT count(Posts.Id)P FROM Posts
LEFT JOIN PostsToTags ON Posts.Id=PostsToTags.Post_id
LEFT JOIN Tags ON PostsToTags.Tag_id=Tags.Id
WHERE Tags.Title in ('c#', 'C++')
GROUP BY Posts.Id
HAVING COUNT(Posts.Id)>=2
)t
").List()[0];
var isEqual = result == (int)resultOriginal;
}
As you can see at the end I do test against your original query (without the users) and it is actually the same count.
In HQL:
var hql = "select count(p) from Post p where p in " +
"(select t.Post from Tag t group by t.Post having count(t.Post) > 1)";
var result = session.Query(hql).UniqueResult<long>();
You can add additional criteria to the subquery if you need to specify tags or other criteria.
Edit : In the future I should read the questions until the last words. I would have seen in HQL...
After some seach, realizing that RowCount removes any grouping in the query ( https://stackoverflow.com/a/8034921/1236044 ). I found a solution using QueryOver and SubQuery which I post here as information.
I find this solution interesting as it offers some modularity, and seprates the counting from the subquery itself, which can be reused as it is.
var searchTags = new[] { "tag1", "tag3" };
var userNames = new[] { "mr.nuub" };
Tag tagAlias = null;
Post postAlias = null;
User userAlias = null;
var postsSubquery =
QueryOver.Of<Post>(() => postAlias)
.JoinAlias(() => postAlias.Tags, () => tagAlias)
.JoinAlias(() => postAlias.User, () => userAlias)
.WhereRestrictionOn(() => tagAlias.Title).IsIn(searchTags)
.AndRestrictionOn(() => userAlias.UserName).IsIn(userNames)
.Where(Restrictions.Gt(Projections.Count<Post>(p => tagAlias.Title), 1));
var numberOfPosts = session.QueryOver<Post>()
.WithSubquery.WhereProperty(p => p.Id).In(postsSubquery.Select(Projections.Group<Post>(p => p.Id)))
.RowCount();
Hope this will help
Can't believe I've found no answer to this but how can you do a query like
SELECT LTRIM(RTRIM("ColumnName")) FROM ....
in NHibernate
thanks
Having an example of Bank as POCO:
public class Bank
{
public virtual int ID { get; set; }
public virtual string City { get; set; }
public virtual string Street { get; set; }
}
There is a syntax for the LTRIM(RTRIM...
Bank bank = null;
var session = ...;
var query = session.QueryOver<BankAddress>()
.SelectList(l => l
// properties ID, City
.Select(c => c.ID).WithAlias(() => bank.ID)
.Select(c => c.City).WithAlias(() => bank.City)
// projection Street
.Select(Projections.SqlProjection(
" LTRIM(RTRIM({alias}.Street)) as Street" // applying LTRIM(RTRIM
, new string[] { "Street" }
, new IType[] { NHibernate.NHibernateUtil.String }
))
.TransformUsing(Transformers.AliasToBean<Bank>())
;
var list = query.List<Bank>();
Either I'm having a mental block, or its not that straightforward.
I have 2 classes, something like that:
public class House
{
public string Id { get; set; }
public string City { get; set; }
public string HouseNumber { get; set; }
}
public class Person
{
public string Id { get; set; }
public string HouseId { get; set; }
public string Name { get; set; }
}
Now I want a list of all people living in a given city, in a flattened model ({City, HouseNumber, PersonName}).
I can't figure out a way on how to map that.. If I had a City in a Person class that would be easy, but I don't, and it doesn't make sense there, imo.
Help ?
Edit:
I came up with this index, which actually works with in-memory list, but Raven returns nothing :(
public class PeopleLocations : AbstractMultiMapIndexCreationTask<PeopleLocations.EntryLocation>
{
public class PeopleLocation
{
public string PersonId { get; set; }
public string HouseId { get; set; }
public string City { get; set; }
}
public PeopleLocations()
{
this.AddMap<House>(venues => venues.Select(x => new
{
x.City,
HouseId = x.Id,
PersonId = (string)null
}));
this.AddMap<Person>(people => people.Select(x => new
{
City = (string)null,
HouseId = x.HouseId,
PersonId = x.Id
}));
this.Reduce = results => results.GroupBy(x => x.HouseId)
.Select(x => new
{
HouseId = x.Key,
People = x.Select(e => e.PersonId),
City = x.FirstOrDefault(y => y.City != null).City,
})
.SelectMany(x =>
x.People.Select(person => new PeopleLocation
{
PersonId = person,
HouseId = x.HouseId,
City = x.City,
})
)
.Select(x => new { PersonId = x.PersonId, x.City, x.HouseId });
}
}
You can do this with a MultiMap Index - but there's a great new feature in RavenDB 2.0 called Indexing Related Documents that is much easier.
Map = people => from person in people
let house = LoadDocument<House>(person.HouseId)
select new
{
house.City,
house.HouseNumber,
PersonName = person.Name,
}
Say I have a User class like this:
public class User
{
public string Id {get; set;}
public string Name {get; set;}
}
Each User can be either a Mentor, a Mentee or both. This is represented by a Relationship class:
public class Relationship
{
public string MentorId {get; set;} // This is a User.Id
public string MenteeId {get; set;} // This is another User.Id
}
Now I would like to generate a report that lists all of my Users and contains a field called Mentor Count and another field called Mentee Count. To achieve this I have created a UserReportDTO class to hold my report data.
public class UserReportDTO
{
public string Name {get; set;}
public string MentorCount {get; set;}
public string MenteeCount {get; set;}
}
I then query my RavenDB to get a list of all the Users and transform this into a list of UserReportDTO instances.
UserService
public List<UserReportDTO> GetReportChunk(
IDocumentSession db,
int skip = 0,
int take = 1024)
{
return db.Query<User>()
.OrderBy(u => u.Id)
.Skip(skip)
.Take(take)
.ToList()
.Select(user =>
new UserReportDTO
{
Name = user.Name,
MentorCount = // What goes here?
MenteeCount = // What goes here?
})
.ToList();
}
As you can see, I am struggling to work out the best way to retrieve the MentorCount and MenteeCount values. I have written some Map/Reduce Indexes that I think should be doing the job but I am unsure how to use them to achieve the result I want.
Question
What is the best way to include multiple aggregate fields into a single query?
EDIT 1
#Matt Johnson: I have implemented your index (see end) and now have a working Report Query which, in case anybody is interested, looks like this:
Working User Report Query
public List<UserDTO> GetReportChunk(IDocumentSession db, Claim claim, int skip = 0, int take = 1024)
{
var results = new List<UserDTO>();
db.Query<RavenIndexes.Users_WithRelationships.Result, RavenIndexes.Users_WithRelationships>()
.Include(o => o.UserId)
.Where(x => x.Claims.Any(c => c == claim.ToString()))
.OrderBy(x => x.UserId)
.Skip(skip)
.Take(take)
.ToList()
.ForEach(p =>
{
var user = db.Load<User>(p.UserId);
results.Add(new UserDTO
{
UserName = user.UserName,
Email = user.Email,
// Lots of other User properties
MentorCount = p.MentorCount.ToString(),
MenteeCount = p.MenteeCount.ToString()
});
});
return results;
}
MultiMap Index
public class Users_WithRelationships :
AbstractMultiMapIndexCreationTask<Users_WithRelationships.Result>
{
public class Result
{
public string UserId { get; set; }
public string[] Claims { get; set; }
public int MentorCount { get; set; }
public int MenteeCount { get; set; }
}
public Users_WithRelationships()
{
AddMap<User>(users => users.Select(user => new
{
UserId = user.Id,
user.Claims,
MentorCount = 0,
MenteeCount = 0
}));
AddMap<Relationship>(relationships => relationships.Select(relationship => new
{
UserId = relationship.MentorId,
Claims = (string[]) null,
MentorCount = 0,
MenteeCount = 1
}));
AddMap<Relationship>(relationships => relationships.Select(relationship => new
{
UserId = relationship.MenteeId,
Claims = (string[]) null,
MentorCount = 1,
MenteeCount = 0
}));
Reduce = results => results.GroupBy(result => result.UserId).Select(g => new
{
UserId = g.Key,
Claims = g.Select(x => x.Claims).FirstOrDefault(x => x != null),
MentorCount = g.Sum(x => x.MentorCount),
MenteeCount = g.Sum(x => x.MenteeCount)
});
}
}
You might be better served with a model that already has your relationship data kept with the user. This might look something like:
public class User
{
public string Id { get; set; }
public string Name { get; set; }
public string[] MentorUserIds { get; set; }
public string[] MenteeUserIds { get; set; }
}
However, if you want to stick with the model you described, the solution is to get rid of the multiple separate indexes and create a single multi-map index that has the data you need.
public class Users_WithRelationships
: AbstractMultiMapIndexCreationTask<Users_WithRelationships.Result>
{
public class Result
{
public string UserId { get; set; }
public string Name { get; set; }
public int MentorCount { get; set; }
public int MenteeCount { get; set; }
}
public Users_WithRelationships()
{
AddMap<User>(users => from user in users
select new
{
UserId = user.Id,
Name = user.Name,
MentorCount = 0,
MenteeCount = 0
});
AddMap<Relationship>(relationships => from relationship in relationships
select new
{
UserId = relationship.MentorId,
Name = (string)null,
MentorCount = 1,
MenteeCount = 0
});
AddMap<Relationship>(relationships => from relationship in relationships
select new
{
UserId = relationship.MenteeId,
Name = (string)null,
MentorCount = 0,
MenteeCount = 1
});
Reduce = results =>
from result in results
group result by result.UserId
into g
select new
{
UserId = g.Key,
Name = g.Select(x => x.Name).FirstOrDefault(x => x != null),
MentorCount = g.Sum(x => x.MentorCount),
MenteeCount = g.Sum(x => x.MenteeCount)
};
}
}
Then you can update your GetReportChunk method to query against the one index if you still want to project a custom DTO.
return db.Query<Users_WithRelationships.Result, Users_WithRelationships>()
.OrderBy(x => x.UserId)
.Skip(skip)
.Take(take)
.Select(x =>
new UserReportDTO
{
Name = x.Name,
MentorCount = x.MentorCount,
MenteeCount = x.MenteeCount,
})
.ToList();
In RavenDb I have a simple multimap index which looks like below:
public class MessageOutboxIndex : AbstractMultiMapIndexCreationTask<MessageOutboxIndex.ReduceResult>
{
public class ReduceResult
{
public string Id { get; set; }
public string FromAccountId { get; set; }
public Core.Enums.Message.MessageStatus OutboxStatus { get; set; }
public string ToAccountId { get; set; }
public string ToArtistName { get; set; }
public DateTimeOffset DateSent { get; set; }
public string Subject { get; set; }
}
public MessageOutboxIndex()
{
AddMap<Message>(messages => from msg in messages
select new
{
Id = msg.Id,
FromAccountId = msg.FromAccountId,
OutboxStatus = msg.OutboxStatus,
ToAccountId = (string)null,
ToArtistName = (string)null,
DateSent = msg.DateSent,
Subject = msg.Subject
});
AddMap<MessageRecipient>(recipients => from recipient in recipients
select new
{
Id = recipient.MessageId,
FromAccountId = (string)null,
OutboxStatus = (object)null,
ToAccountId = recipient.ToAccountId,
ToArtistName = recipient.ToArtistName,
DateSent = DateTimeOffset.MinValue,
Subject = (string)null
});
Reduce = results => from result in results
group result by result.Id
into g
select new
{
Id = g.Key,
FromAccountId = g.Select(x => x.FromAccountId).Where(x => x != null).FirstOrDefault(),
OutboxStatus = g.Select(x => x.OutboxStatus).Where(x => x != null).FirstOrDefault(),
ToAccountId = g.Select(x => x.ToAccountId).Where(x => x != null).FirstOrDefault(),
ToArtistName = g.Select(x => x.ToArtistName).Where(x => x != null).FirstOrDefault(),
DateSent = g.Max(x => (DateTimeOffset)x.DateSent),
Subject = g.Select(x => x.Subject).Where(x => x != null).FirstOrDefault()
};
}
}
However, I now want to only return specific messages from this index by Id using this example query below:
var messages = _documentSession.Query<MessageOutboxIndex.ReduceResult, MessageOutboxIndex>()
.Where(x => x.Id.In(new string[2] {"messages/1", "messages/2"}))
.ToList();
This fails with the following error:
System.ArgumentException: The field '__document_id' is not indexed, cannot query on fields that are not indexed
Querying by Id works for normal indexes, but not multimap indexes. Is this a bug?
Paul
Paul,
We auto translate the Id field to the __document_id, because we don't know that we are querying a m/r index that doesn't have a __document_id.
Your workaround is the way to go, yes.