RavenDb Strange Behaviour with objects with equality members - ravendb

I got to the bottom of a strange bug (in my code ) when I was trying to store a list of objects to ravendb. The problem was that the object to be stored has equality members generated by resharper. The object in question is as follows (note that I have commented out the equality members to solve the issue) -
//[DataContract(Namespace = "")]
//[KnownType(typeof(IApplicationEntity))]
public class ApplicationEntity: IApplicationEntity
{
public ApplicationEntity()
{
}
public ApplicationEntity(string processName)
{
ProcessName = processName;
Id = "Processes/" + ProcessName;
}
public ApplicationEntity(string key, string processName)
{
ProcessName = processName;
Key = key;
Id = string.Format("Processes/{0}_{1}", Key, ProcessName);
}
//[DataMember]
public string Id { get; set; }
//[DataMember]
public string Key { get; set; }
//[DataMember]
public string ProcessName { get; set; }
//[DataMember]
public string ProcessDescription { get; set; }
/// <summary>
/// used to generate sequential unique activity ID generation only.
/// </summary>
//[DataMember]
public string ActivityCount { get; set; }
//public bool Equals(ApplicationEntity other)
//{
// if (ReferenceEquals(null, other)) return false;
// if (ReferenceEquals(this, other)) return true;
// return Equals(other.ProcessName, ProcessName);
//}
//public override bool Equals(object obj)
//{
// if (ReferenceEquals(null, obj)) return false;
// if (ReferenceEquals(this, obj)) return true;
// if (obj.GetType() != typeof (ApplicationEntity)) return false;
// return Equals((ApplicationEntity) obj);
//}
//public override int GetHashCode()
//{
// return (ProcessName != null ? ProcessName.GetHashCode() : 0);
//}
}
Now if I stored the object with equality members implement then the following code produces strange results -
int count = 0;
using (var session = _store.OpenSession(_databaseName))
{
foreach (var applicationEntity in _listOfApplications)
{
var entity = new ApplicationEntity(count.ToString(), applicationEntity.ProcessName);
//ravenRepositoryCachable.Add(entity);
session.Store(entity);
count++;
}
session.SaveChanges();
}
The strange behaviour is that I would expect the Key field to be incrementing to 400 as the list has 400 members , but instead the Key for the first 10 object stored is correct i.e 0 to 9. but the 11th one started from 0 again and so on.
but if I comment the equality members off(as in the code snippet above) then this problem disappears.
Also If i add the objects one at a time as opposed to batch the problem disappears -
int count = 0;
foreach (var applicationEntity in _listOfApplications)
{
using (var session = _store.OpenSession(_databaseName))
{
var entity = new ApplicationEntity(count.ToString(), applicationEntity.ProcessName);
//ravenRepositoryCachable.Add(entity);
session.Store(entity);
session.SaveChanges();
count++;
}
}
I know I have resolved the issue but I do not understand what happened here and why only the key field was effected? This is a undefined behaviour and I am worried as bit as code is supposed to be deployed in production!! It is clear that the equality members are not to be defined. I need to get to the bottom of it , is it a bug?

No, I don't think there is such a bug in RavenDB. Instead, I guess there is another issue with your code, because this test (that is based on your example above) works for me:
public class EqualityMembersInSessionCache
{
public class ApplicationEntity
{
public ApplicationEntity(string key, string processName)
{
ProcessName = processName;
Key = key;
Id = string.Format("Processes/{0}_{1}", Key, ProcessName);
}
public string Id { get; set; }
public string Key { get; set; }
public string ProcessName { get; set; }
public bool Equals(ApplicationEntity other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Equals(other.ProcessName, ProcessName);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof(ApplicationEntity)) return false;
return Equals((ApplicationEntity)obj);
}
public override int GetHashCode()
{
return (ProcessName != null ? ProcessName.GetHashCode() : 0);
}
}
[Fact]
public void Has_correct_behaviour()
{
var randomNames = new List<string>();
for (int c = 1; c < 100; c++)
{
randomNames.Add(string.Format("test{0}", c));
}
using (var store = new EmbeddableDocumentStore { RunInMemory = true })
{
int count = 0;
using (var session = store.OpenSession())
{
foreach (var name in randomNames)
{
session.Store(new ApplicationEntity(count.ToString(), name));
count++;
}
session.SaveChanges();
}
using (var session = store.OpenSession())
{
var results = session.Query<ApplicationEntity>()
.Customize(x => x.WaitForNonStaleResultsAsOfLastWrite())
.ToList();
Assert.NotEmpty(results);
Assert.Equal(99, results.Count);
for (int c = 0; c < 99; c++)
{
Assert.Equal(c, int.Parse(results[c].Key));
}
}
}
}
}
If you want us to investigate further on this, please provide a failing test.

Related

How can I count total item in an object/record and count only the value = true

var obj = (from r in db.Module_Completion
.Where(r => r.star_id == User.Identity.Name)
select new
{
r.adv_stud_success,
r.important_policies,
r.technology,
r.finances,
r.resources,
r.student_life,
r.next_steps
}).FirstOrDefault();
obj = {true, false, false , true,...};
This is only one record that I query from database by using Linq
The reason is I would like to display Finsished 2 of 7 , that is why I need to count items in a record.
Thank you in advance!
public partial class Module_Completion
{
public int module_completionId { get; set; }
[StringLength(10)]
public string star_id { get; set; }
public bool adv_stud_success { get; set; }
public bool important_policies { get; set; }
public bool technology { get; set; }
public bool finances { get; set; }
public bool resources { get; set; }
public bool student_life { get; set; }
public bool next_steps { get; set; }
}
After I can store numbers in variable, then I would like to display like this"Finished 2 of 7"
So Every ModuleCompletion contains several Boolean properties. If the value of such a property is true, you consider the item to be "finished".
You want to count the number of finished items in a ModuleCompletion.
The proper method would be to add a property to class ModuleCompletion.
public int FinishedCount => ...
If you can't do this, for instance because ModuleCompletion represents the columns of a table in a database, then consider to add an extension method. If you are not familiar with extension methods, read Extension Methods demystified
public static int CountFinished(this ModuleCompletion moduleCompletion)
{
// TODO: implement
}
Usage would be:
ModuleCompletion x = ...
int finished = x.CountFinished();
This would work if your ModuleCompletion is in local process. However, if you want to do this database side (as queryable), this won't work: your database wouldn't know method CountFinished.
If you want to let the database do this, you could change your Select method:
int finishedCount = db.ModuleCompletions
.Where(completion => completion.star_id == User.Identity.Name)
.Select(completion =>
completion.adv_stud_success ? 1 : 0 +
completion.important_policies ? 1 : 0 +
completion.technology : 1 : 0 +
...)
.FirstOrDefault();
This looks rather ugly. Luckily we can create an extension method of IQueryable<ModuleCompletion>, such that you can use it as any other LINQ method:
public static IQueryable<int> ToFinishedCount(this IQueryable<ModuleCompletion> moduleCompletions)
{
return moduleCompletions.Select(completion =>
completion.adv_stud_success ? 1 : 0 +
completion.important_policies ? 1 : 0 +
completion.technology : 1 : 0 +
...);
}
This query can be translated into a language that your DBMS understands. Usage would be much easier to understand, much easier to reuse, unit test and modify:
int finishedCount = db.ModuleCompletions
.Where(completion => completion.star_id == User.Identity.Name)
.ToFinishedCount()
.FirstOrDefault();
My solution involves 2 parts first. Create a ViewModel to get the number of completed object that associates with the class that goes:
public class ModuleCompletesVM
{
public Module_Completion Module_Completion { get; set; }
public int Completed { get
{
return (Convert.ToInt32(Module_Completion.stud_success) +
Convert.ToInt32(Module_Completion.important_policies) +
Convert.ToInt32(Module_Completion.technology) +
Convert.ToInt32(Module_Completion.finances) +
Convert.ToInt32(Module_Completion.resources) +
Convert.ToInt32(Module_Completion.next_steps) +
Convert.ToInt32(Module_Completion.student_life));
}
}
}
Note that this View Model would calculate the number of completed subject of each Module_Completion Class and then from your controller. You just need to do something like this
var model= from s in db.Module_Completion
select new ModuleCompletesVM
{
Module_Completion = s
};
You can also achieve this by creating a function in SQL database as well. Another piece of advice is that, do consider stop using underscrore in your data table and data field. Follow the latest convention, the next person who take over your code will appreciate that.
I have an approach where you can have the total finished as property. Here is an example
public partial class Module_Completion
{
private bool _adv_stud_success;
private bool _important_policies;
private bool _technology;
private bool _finances;
private bool _resources;
private bool _student_life;
private bool _next_steps;
public int module_completionId { get; set; }
[StringLength(10)]
public string star_id { get; set; }
public bool adv_stud_success
{
get { return _adv_stud_success; }
set {
_adv_stud_success = value;
if (_adv_stud_success)
{
total_finsihed += 1;
}
}
}
public bool important_policies
{
get { return _important_policies; }
set
{
_important_policies = value;
if (_important_policies)
{
total_finsihed += 1;
}
}
}
public bool technology
{
get { return _technology; }
set
{
_technology = value;
if (_technology)
{
total_finsihed += 1;
}
}
}
public bool finances
{
get { return _finances; }
set
{
_finances = value;
if (_finances)
{
total_finsihed += 1;
}
}
}
public bool resources
{
get { return _resources; }
set
{
_resources = value;
if (_resources)
{
total_finsihed += 1;
}
}
}
public bool student_life
{
get { return _student_life; }
set
{
_student_life = value;
if (_student_life)
{
total_finsihed += 1;
}
}
}
public bool next_steps
{
get { return _next_steps; }
set
{
_next_steps = value;
if (_next_steps)
{
total_finsihed += 1;
}
}
}
/* This property is storing the finished count */
/* using System.ComponentModel.DataAnnotations.Schema; */
[NotMapped]
public int total_finsihed { get; set; }
}
After that you can do the following
var obj = (from r in db.Module_Completion
.Where(r => r.star_id == User.Identity.Name)
select new
{
r.total_finsihed
}).FirstOrDefault();
/* if the technology, finance, resources are true then */
/* obj = {3}; */

automatically expand related entity with OData controller

I have these classes:
public class Items
{
[Key]
public Guid Id { get; set; }
public string ItemCode { get; set; }
public decimal SalesPriceExcl { get; set; }
public decimal SalesPriceIncl { get; set; }
public virtual ICollection<ItemPrice> SalesPrices { get; set; }
public Items()
{
SalesPrices = new HashSet<App4Sales_ItemPrice>();
}
}
public class ItemPrice
{
[Key, Column(Order = 0), ForeignKey("Items")]
public Guid Id { get; set; }
public virtual Items Items { get; set; }
[Key, Column(Order=1)]
public Guid PriceList { get; set; }
public decimal PriceExcl { get; set; }
public decimal PriceIncl { get; set; }
public decimal VatPercentage { get; set; }
}
I want to query the Items and automatically get the ItemPrice collection.
I've created an OData V3 controller:
// GET: odata/Items
//[Queryable]
public IQueryable<Items> GetItems(ODataQueryOptions opts)
{
SelectExpandQueryOption expandOpts = new SelectExpandQueryOption(null, "SalesPrices", opts.Context);
Request.SetSelectExpandClause(expandOpts.SelectExpandClause);
return expandOpts.ApplyTo(db.Items.AsQueryable(), new ODataQuerySettings()) as IQueryable<Items>;
}
But I get the error:
"Cannot serialize null feed"
Yes, some Items have no ItemPrice list.
Can I get past this error, or can I do something different?
Kind regards
Jeroen
I found the underlying error is:
Unable to cast object of type
'System.Data.Entity.Infrastructure.DbQuery1[System.Web.Http.OData.Query.Expressions.SelectExpandBinder+SelectAllAndExpand1[.Models.Items]]'
to type '.Models.Items'.
I've solved it after I came across this post: http://www.jauernig-it.de/intercepting-and-post-processing-odata-queries-on-the-server/
This is my controller now:
SelectExpandQueryOption expandOpts = new SelectExpandQueryOption(null, "SalesPrices", opts.Context);
Request.SetSelectExpandClause(expandOpts.SelectExpandClause);
var result = expandOpts.ApplyTo(db.Items.AsQueryable(), new ODataQuerySettings());
var resultList = new List<Items>();
foreach (var item in result)
{
if (item is Items)
{
resultList.Add((Items)item);
}
else if (item.GetType().Name == "SelectAllAndExpand`1")
{
var entityProperty = item.GetType().GetProperty("Instance");
resultList.Add((Items)entityProperty.GetValue(item));
}
}
return resultList.AsQueryable();
Jeroen
GetItems([FromODataUri] ODataQueryOptions queryOptions)
expanding on Jeroen's post. Anytime a select or expand is involved, OData wraps the results in a SelectAll or SelectSome object; so, we need to unwrap the values rather than do an direct cast.
public static class ODataQueryOptionsExtensions
{
public static IEnumerable<T> ApplyODataOptions<T>(this IQueryable<T> query, ODataQueryOptions options) where T : class, new()
{
if (options == null)
{
return query;
}
var queryable = options.ApplyTo(query);
if (queryable is IQueryable<T> queriableEntity)
{
return queriableEntity.AsEnumerable();
}
return UnwrapAll<T>(queryable).ToList();
}
public static IEnumerable<T> UnwrapAll<T>(this IQueryable queryable) where T : class, new()
{
foreach (var item in queryable)
{
yield return Unwrap<T>(item);
}
}
public static T Unwrap<T>(object item) where T : class, new()
{
var instanceProp = item.GetType().GetProperty("Instance");
var value = (T)instanceProp.GetValue(item);
if (value != null)
{
return value;
}
value = new T();
var containerProp = item.GetType().GetProperty("Container");
var container = containerProp.GetValue(item);
if (container == null)
{
return (T)null;
}
var containerType = container.GetType();
var containerItem = container;
var allNull = true;
for (var i = 0; containerItem != null; i++)
{
var containerItemType = containerItem.GetType();
var containerItemValue = containerItemType.GetProperty("Value").GetValue(containerItem);
if (containerItemValue == null)
{
containerItem = containerType.GetProperty($"Next{i}")?.GetValue(container);
continue;
}
var containerItemName = containerItemType.GetProperty("Name").GetValue(containerItem) as string;
var expandedProp = typeof(T).GetProperty(containerItemName);
if (expandedProp.SetMethod == null)
{
containerItem = containerType.GetProperty($"Next{i}")?.GetValue(container);
continue;
}
if (containerItemValue.GetType() != typeof(string) && containerItemValue is IEnumerable containerValues)
{
var listType = typeof(List<>).MakeGenericType(expandedProp.PropertyType.GenericTypeArguments[0]);
var expandedList = (IList)Activator.CreateInstance(listType);
foreach (var expandedItem in containerValues)
{
var expandedInstanceProp = expandedItem.GetType().GetProperty("Instance");
var expandedValue = expandedInstanceProp.GetValue(expandedItem);
expandedList.Add(expandedValue);
}
expandedProp.SetValue(value, expandedList);
allNull = false;
}
else
{
var expandedInstanceProp = containerItemValue.GetType().GetProperty("Instance");
if (expandedInstanceProp == null)
{
expandedProp.SetValue(value, containerItemValue);
allNull = false;
}
else
{
var expandedValue = expandedInstanceProp.GetValue(containerItemValue);
if (expandedValue != null)
{
expandedProp.SetValue(value, expandedValue);
allNull = false;
}
else
{
var t = containerItemValue.GetType().GenericTypeArguments[0];
var wrapInfo = typeof(ODataQueryOptionsExtensions).GetMethod(nameof(Unwrap));
var wrapT = wrapInfo.MakeGenericMethod(t);
expandedValue = wrapT.Invoke(null, new[] { containerItemValue });
if (expandedValue != null)
{
expandedProp.SetValue(value, expandedValue);
allNull = false;
}
}
}
}
containerItem = containerType.GetProperty($"Next{i}")?.GetValue(container);
}
if (allNull)
{
return (T)null;
}
return value;
}
}

After successfully adding child entity with parent reference, child does not show under parent resource

I have two entities, Shelf and Book. A Shelf can have multiple Books (the relationship is bi-directional). I've exposed both of these as JpaRepositories.
Here's the issue:
I create a shelf by posting { "name":"sci-fi" } to /shelves.(success)
I create a book for that shelf by posting { "name":"mybook", "shelf":"localhost:8080/shelves/1" } to /books. (success)
When I get the book I just created at /books/1, it has the correct link to the parent shelf.
But when I go to shelves/1/books, I get an empty result, { }!
Any ideas what I might be missing?
Right now I've constructed a workaround by explicitly adding the book to its shelf in a beforeCreate event, but it seems like this should be totally unnecessary. (It does fix the problem, however.)
#HandleBeforeCreate(Book.class)
public void handleCreate(Book book) {
// why is this necessary?
book.getShelf().getBooks().add(book);
}
Here are the entity classes:
#Entity
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
#ManyToOne
private Shelf shelf;
public Shelf getShelf() {
return shelf;
}
public void setShelf(Shelf shelf) {
this.shelf = shelf;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Book other = (Book) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
#Entity
public class Shelf {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
#OneToMany
private List<Book> books = new ArrayList<Book>();
public List<Book> getBooks() {
return books;
}
public void setBooks(List<Book> books) {
this.books = books;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Shelf other = (Shelf) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
I'm using Spring Boot 1.1.8.
In your Shelf entity, add the mappedBy = "self" property to the #OneToMany annotation in books:
#OneToMany(mappedBy = "self")
private List<Book> books = new ArrayList<Book>();
This will auto populate the list of books with the books whose reference to self matches.
I think you need to update shelf to add new book you created as a last step. This is because you are using bi-directional relationship and shelf has no idea about books till you update it with the books it should hold.

NHibernate: IUserType not working

I have this class that implements IUserType:
public class StringToIntType : IUserType
{
/// <summary>
/// mutable object = an object whose state CAN be modified after it is created
/// </summary>
public bool IsMutable
{
get { return false; }
}
public Type ReturnedType
{
get { return typeof(StringToIntType); }
}
public SqlType[] SqlTypes
{
get { return new[] { NHibernateUtil.String.SqlType }; }
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var obj = NHibernateUtil.String.NullSafeGet(rs, names[0]);
if (obj == null) return null;
var s = (string)obj;
int i;
if (Int32.TryParse(s, out i))
return i;
return -1;
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
if (value == null)
{
((IDataParameter)cmd.Parameters[index]).Value = DBNull.Value;
}
else
{
var i = (int)value;
((IDataParameter)cmd.Parameters[index]).Value = i.ToString();
}
}
public object DeepCopy(object value)
{
return value;
}
public object Replace(object original, object target, object owner)
{
return original;
}
public object Assemble(object cached, object owner)
{
return cached;
}
public object Disassemble(object value)
{
return value;
}
public new bool Equals(object x, object y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
return x.Equals(y);
}
public int GetHashCode(object x)
{
return x == null ? typeof(int).GetHashCode() + 473 : x.GetHashCode();
}
}
My mapping:
public BarausLangMap()
{
Table("BARAUSLANG");
Id(x => x.ula).CustomType<StringToIntType>();
Map(x => x.bezeichnung);
Map(x => x.sprache);
Map(x => x.la);
Where("la = 'SPE'");
}
My properties:
public virtual int ula { get; set; }
public virtual String bezeichnung { get; set; }
public virtual Int32? sprache { get; set; }
public virtual String la { get; set; }
Problem: When I do
var b = session.Get<BarausLang>(5);
It says
{NHibernate.TypeMismatchException: Provided id of the wrong type.
Expected: MobileServiceServer.Models.StringToIntType, got System.Int32
What is the problem? I thought nHibernate would call StringToIntType implicitely to convert from int to string and vice versa. I think that is the whole point. I thought StringToIntType was only for the mapping? How should I use it then?
You are correct, ReturnedType should return the type that NullSafeGet will return. The example code you linked to is incorrect, ReturnedType should return typeof(bool).
Also, getting the Equals method right is very important and I recommend a small change to your code:
public new bool Equals(object x, object y)
{
if (ReferenceEquals(x, y)) return true;
var xString = x as string;
var yString = y as string;
if (xString == null || yString == null) return false;
return xString.Equals(yString);
}
I MAY have found the problem:
public Type ReturnedType
above returns StringToIntType, and i THINK it should be int.
However, on:
http://lostechies.com/rayhouston/2008/03/23/mapping-strings-to-booleans-using-nhibernate-s-iusertype/
the method returns the type that implements IUserType.
Please confirm.

Querying UserType's in NHibernate

I have the following scenario:
Let's say that my "Product" table in this legacy database has a "Categories" column of type string. This column stores the category ID's separated by some sort of ascii character. For instance: "|1|" (for category 1), "|1|2|3|" (for categories 1, 2, and 3), etc.
Instead of exposing a string property for that, I want to expose an IEnumerable, so that users of my Product class don't have to worry about parsing those values.
I'm creating a SelectedCatories type that's simply an IEnumerable, and my Product class looks like this:
public class Product
{
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
public virtual bool Discontinued { get; set; }
public virtual SelectedCategories Categories { get; set; }
}
I then created a SelectedCategoriesUserType class like so:
public class SeletedCategoriesUserType : IUserType
{
static readonly SqlType[] _sqlTypes = {NHibernateUtil.String.SqlType};
public bool Equals(object x, object y)
{
// Fix this to check for Categories...
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
return x.Equals(y);
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
object obj = NHibernateUtil.String.NullSafeGet(rs, names[0]);
if (obj == null) return null;
string[] stringCategories = obj.ToString().Split(new[] {'|'}, StringSplitOptions.RemoveEmptyEntries);
var categories = new Categories();
return
new SelectedCategories(
stringCategories.Select(
stringCategory => categories.Single(cat => cat.Id == int.Parse(stringCategory)))
.ToList());
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
if (value == null)
{
((IDataParameter) cmd.Parameters[index]).Value = DBNull.Value;
}
else
{
var theCategories = (SelectedCategories) value;
var builder = new StringBuilder();
builder.Append("|");
theCategories.ForEach(i => builder.AppendFormat("{0}|", i.Id.ToString()));
((IDataParameter) cmd.Parameters[index]).Value = builder.ToString();
}
}
public object DeepCopy(object value)
{
return value;
}
public object Replace(object original, object target, object owner)
{
throw new NotImplementedException();
}
public object Assemble(object cached, object owner)
{
throw new NotImplementedException();
}
public object Disassemble(object value)
{
throw new NotImplementedException();
}
public SqlType[] SqlTypes
{
get { return _sqlTypes; }
}
public Type ReturnedType
{
get { return typeof (SelectedCategories); }
}
public bool IsMutable
{
get { return false; }
}
}
I then want to build a query that gives me back any product that belongs in a specific category (say, category 2), matching both "|2|", and "|1|2|3|".
Right now, my naive implementation that barely makes my test pass looks like this:
public IEnumerable<Product> GetByCategory(Category category)
{
using (ISession session = NHibernateHelper.OpenSession())
{
return session
.CreateSQLQuery("select * from product where categories LIKE :category")
.AddEntity(typeof(Product))
.SetString("category", string.Format("%|{0}|%", category.Id))
.List()
.Cast<Product>();
}
}
My question is: what's the proper way to right that query?
A different way to do that ICriteria query would be this...
return Session
.CreateCriteria(typeof(Product), "product")
.Add(Expression.Sql(
"{alias}.categories LIKE ?",
string.Format("%|{0}|%", category.Id),
NHibernateUtil.String))
.List<Product>();
However, you may want to think about setting up a many-to-many table between Product and Category and setting up a collection of Categories in the Product class. You can still keep your field of concatenated Category Ids (I assume it's needed for legacy purposes), but tie it to the collection with something like this.
public virtual ISet<Category> Categories { get; private set; }
public virtual string CategoriesString
{
get { return string.Join("|", Categories.Select(c => c.Id.ToString()).ToArray()); }
}
Doing something like this will let you set foreign keys on your tables, and make the queries a bit easier to construct.
return Session
.CreateCriteria(typeof(Product), "product")
.CreateCriteria("product.Categories", "category")
.Add(Restrictions.Eq("category.Id", category.Id))
.List<Product>();