NHibernate Fluent Mapping results in Invalid index n for this SqlParameterCollection with Count=n - nhibernate

I have a class TrainingSpecialty which is mapped as follows:
public TrainingSpecialityMap()
{
Table("TrainingSpecialty");
Id(o => o.Id).GeneratedBy.Native();
DiscriminateSubClassesOnColumn(CandidateTrainingDatabase.TrainingSpecialtyTable.IsCombinedColumn)
.Formula(
string.Format("case when {0} is null then 0 else {0} end", CandidateTrainingDatabase.TrainingSpecialtyTable.IsCombinedColumn));
Map(o => o.Code);
Map(o => o.Name).Not.Nullable();
Map(o => o.TrainingProgramType, "ProgramType").Not.Nullable();
HasMany(o => o.Forms)
.Not.KeyNullable()
.KeyColumn("TrainingSpecialtyID").Cascade
.SaveUpdate();
Map(o => o.ProcedureTitle).Nullable();
HasMany(o => o.Procedures);
}
I'm attempting to create a new Form, add it to the TrainingSpecialty.Forms list and then update the TrainingSpecialty. The purpose of this is to add the new form. (Note that Form does NOT have a navigation property back to the TrainingSpecialty. Without changing the Form class, there is no simple way to just add the form with a reference to the TrainingSpecialty, which is why I'm updating the TrainingSpecialty.)
The first error I'm getting upon Updating the TrainingSpecialty is Error dehydrating property value for Domain.EvaluationForm.Domain.TrainingSpecialty.FormsBackref.
The inner exception is Invalid index 3 for this SqlParameterCollection with Count=3.
There should always be one previously existing form and I'm now attempting to add a second. I've looked for fields that are duplicately mapped (as per others who have had this issue).
How is this mapping off?

It was a duplicate mapping, just like so many others like this. In this case the Form mapping someone had added a reference back to the Training Specialty.

Related

Can I transform the value of my primary key in a specific one-to-many relationship using Fluent API?

I'm trying to manually scaffold part of an existing database schema in EF Core 2.2. The database in question is part of a 3rd party ERP software, and I have zero control over the design of the database itself, so I'm trying to work with what I am given. So far it's going okay, however I am hitting a snag due to a questionable database design choice by the ERP vendor.
Consider the following one-to-many relationship I'm trying to build in my OnModelCreating() method within my DbContext (via Fluent API):
modelBuilder.Entity<SalesOrderHeader>(s => {
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.SalesOrderNumber)
.HasForeignKey(e => e.RelatedPO_SONumber);
});
This is almost what I need, however, in the WorkOrders table "RelatedPO_SONumber" is of type string, where "SalesOrderNumber" in SalesOrderHeaders table is of type int. Further, what's stored in "RelatedPO_SONumber" is a 0-padded string (always 8 characters long) of "SalesOrderNumber" (eg SalesOrderHeader.SalesOrderNumber = 1234567, WorkOrder.RelatedPO_SONumber = "01234567").
Here's what I tried and might help illustrate what I'm trying to do via format specifier (throws ArgumentException "The expression should represent a simple property access: 't => t.MyProperty'"):
modelBuilder.Entity<SalesOrderHeader>(s => {
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.SalesOrderNumber.ToString("D8"))
.HasForeignKey(e => e.RelatedPO_SONumber);
});
I tried an alternative approach which was to make another property in my SalesOrderHeader entity definition, and using that as the argument for HasPrincipalKey():
// within SalesOrderHeader
[NotMapped]
public string ZeroPaddedSONumber => SalesOrderNumber.ToString("D8");
// within DbContext OnModelCreating()
modelBuilder.Entity<SalesOrderHeader>(s => {
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.ZeroPaddedSONumber)
.HasForeignKey(e => e.RelatedPO_SONumber);
});
This resulted in:
"InvalidOperationException: No backing field could be found for property 'ZeroPaddedSONumber' of entity type 'SalesOrderHeader' and the property does not have a setter."
Is there a way to pass anything other than a strict property reference, but rather a transformed "version" of that property?"
Thanks in advance.
EDIT: Following a suggestion in a comment, I tried this:
modelBuilder.Entity<SalesOrderHeader>(s => {
s.Property(e => e.SalesOrderNumber)
.HasConversion(n => n.ToString("D8"), s => int.Parse(s));
s.HasMany<WorkOrderHeader>(e => e.WorkOrders)
.WithOne()
.HasPrincipalKey(e => e.SalesOrderNumber)
.HasForeignKey(e => e.RelatedPO_SONumber);
});
This doesn't seem to work either, I get the following exception (I get the same exception if I omit the conversion):
The types of the properties specified for the foreign key {'RelatedPO_SONumber'} on entity type 'WorkOrderHeader' do not match the types of the properties in the principal key {'SalesOrderNumber'} on entity type 'SalesOrderHeader'.

By code mapping of many-to-many with OrderBy

I'm using by code mappings and trying to map a manytomany. This works fine but I need OrderBy for the child collection items. I noticed this has been omitted (it does exist in the HBM mappings). e.g.
public class Mapping : EntityMapper<Category>
{
public Mapping()
{
Set(x => x.Items, m =>
{
m.Table("ItemCategories");
m.Key(k => k.Column("CategoryId"));
m.Inverse(true);
m.Cascade(Cascade.None);
}, col => col.ManyToMany(m =>
{
m.Columns(x => x.Name("ItemId"));
//m.OrderBy("Score desc"); // missing in Nh4.x?
}));
}
}
Is there a workaround for this? I tried following the suggestion in this article whereby I can set the property before the session factory is built but it has no effect. e.g.
cfg.GetCollectionMapping(typeof(Category).FullName + ".Items").ManyToManyOrdering = "Score desc";
cfg.BuildSessionFactory();
Am I doing something wrong or is OrderBy on manytomany not supported in Nh4?
Also, is it possible to restrict the maximum number of items retrieved in the collection?
Replaced the many to many with one to many and introduced an entity that represents the relationship (followed advice from this article).
This has the upside of allowing you to map the order-by column as well as other columns, and also solved the issue of restricting the number of items in the collection by using the one-to-many's Where() and Filter() clauses.

Nhibernate ByCode Mapping -- How to avoid a null Many-To-One Object Map?

I'm mapping a legacy database and previously users were not required to select a country when they signed up and later added it.
So there are some users who have a CountryID that is NULL. So when my many to one mapping (ByCode) tries to load up the corresponding country details, it finds none, so the user's "country" object is null.
When I try to display user.Country.Name for example, I get a object reference not found.
I could hack this by making a country with an id of -1 called Undisclosed and then change all the null country ids to -1, but that's cheating to solve a common problem I would think.
So my question is how can I map so that if the CountryID is not found, that user gets a new Country object.
My mapping in the user object is:
ManyToOne<Country>(x => x.Country, m =>
{
m.Column("CountryID");
m.Lazy(LazyRelation.NoLazy);
m.NotNullable(false);
m.Fetch(FetchKind.Join);
});
My CountryMap is pretty basic...
public CountryMap()
{
Id(p => p.ID, m => m.Generator(Generators.Identity));
Property(p => p.Code);
Property(p => p.Name);
}
Coding too late... brain freeze...
I simply modified the Country property on the User POCO to return a new Country() if the country was NULL.
Hope this helps someone, but if admins delete the question I won't be offended. :) :) :)

second level cache with formula property

Using nHib 3.2, I'm interested in caching all my entity's properties, except for a formula property which I would like to be calculated with each Get.
Is this possible?
here's my mapping:
public TransactionNHibernateMapping()
{
Table("TransactionInfo");
Id(transaction => transaction.TransactionId, m => m.Generator(Generators.GuidComb));
Cache(c =>
{
c.Include(CacheInclude.All);
c.Usage(CacheUsage.ReadWrite);
});
Property(transaction => transaction.HighestSeverity, m => m.Formula("(Select max(LogEntryInfo.Severity) from LogEntryInfo where LogEntryInfo.TransactionId = TransactionId)"));
}
now, although 2nd level cache is enabled, this entity is always re-loaded from the database.
If I remove the HighestSeverity property mapping, caching works as expected.
I've tried playing with the Generated option of the formula field, but that didn't seem to help.
any other ideas?

NHibernate Criteria: howto exclude certain mapped properties/collections?

Here's my (simplified) model: Ticket -> Customer Callback (s)
I have my Ticket mapped so that when it's loaded, the Callbacks are as well.
base.HasMany<TechSupportCallback>(x => x.Callbacks)
.KeyColumn(Fields.TRACKED_ITEM_ID)
.Not.LazyLoad()
.Inverse()
.Cache.ReadWrite();
This is not lazy loading because otherwise I'll get 'no session to load entities' when the web service tries to serialize (and load) the proxy. (Using repositories to fetch data.)
It's also bi-directional .. (in the CallbackMap)
base.References(x => x.Ticket)
.Column(Fields.TRACKED_ITEM_ID)
.Not.Nullable();
Now .. we need to show an agent a list of their callbacks - JUST their callbacks.
-- When I query using Criteria for the Callbacks, I cannot prevent the Ticket (and subsequently it's entire graph, including other collections) from being loaded. I had previously tried to set FetchMode.Lazy, then iterate each resulting Callback and set Ticket to null, but that seems to be ignored.
// open session & transaction in using (..)
var query = session.CreateCriteria<TechSupportCallback>()
.SetCacheable(true)
.SetCacheRegion("CallbacksByUserAndGroups")
.SetFetchMode("Ticket", FetchMode.Lazy) // <-- but this doesn't work!
.SetMaxResults(AegisDataContext.Current.GetMaxRecordCount())
;
rValue = query.List<TechSupportCallback>();
rvalue.ForEach(x => x.Ticket = null;); // <-- since this is already retrieved, doing this merely prevents it from going back across the wire
tx.Commit();
// usings end (..)
Should I be doing this with a projection instead?
The problem with that .. is I've not been able to find an example of projections being used to populate an entity, or a list of them -- only to be used as a subquery on a child entity or something similar to restrict a list of parent entities.
I could really use some guidance on this.
[EDIT]
I tried using a projection as suggested but:
I'm getting the following: (this was because of a bug, and so I've since stopped using the cache and it works. http://nhibernate.jira.com/browse/NH-1090)
System.InvalidCastException occurred
Message=Unable to cast object of type 'AEGISweb.Data.Entities.TechSupportCallback' to type 'System.Object[]'.
Source=NHibernate
StackTrace:
at NHibernate.Cache.StandardQueryCache.Put(QueryKey key, ICacheAssembler[] returnTypes, IList result, Boolean isNaturalKeyLookup, ISessionImplementor session)
InnerException:
at
rValue = query.List<TechSupportCallback>();
with the projection list defined like
// only return the properties we want!
.SetProjection(Projections.ProjectionList()
.Add(Projections.Alias(Projections.Id(), ex.NameOf(x => x.ID))) //
.Add(Projections.Alias(Projections.Property(ex.NameOf(x => x.ContactID)), ex.NameOf(x => x.ContactID)))
// ...
)
.SetResultTra...;
rValue = query.List<TechSupportCallback>();
mapped like
public TechSupportCallbackMap()
{
base.Cache.ReadWrite();
base.Not.LazyLoad();
base.Table("TS_CALLBACKS");
base.Id(x => x.ID, Fields.ID)
.GeneratedBy.Sequence("SEQ_TS_CALLBACKS");
base.References(x => x.Ticket)
.Column(Fields.TRACKED_ITEM_ID)
.Not.Nullable();
base.Map(x => x.TrackedItemID, Fields.TRACKED_ITEM_ID)
.Not.Insert()
.Not.Update()
.Generated.Always()
;
// ...
}
This sounds like it's a job exactly for projections.
var query = session.CreateCriteria<TechSupportCallback>()
.SetCacheable(true)
.SetCacheRegion("CallbacksByUserAndGroups")
.SetFetchMode("Ticket", FetchMode.Lazy)
.SetMaxResults(AegisDataContext.Current.GetMaxRecordCount())
.SetProjection(Projections.ProjectionList().
.Add(Projections.Alias(Projections.Id(), "Id")
.Add(Projections.Alias(Projections.Property("Prop"), "Prop")))
.SetResultTransformer(Transformers.AliasToBean<TechSupportCallback>())
;
Simply list all the properties you want to include and exclude the Ticket entity. You can even create a POCO class simply for encapsulating the results of this query. Rather than using an existing entity class.