NHibernate mapping error in legacy mapping - nhibernate

I've inherited a large set of NHibernate mappings that live in an existing, functional application. I've branched this application to develop some new features, and while I do so I'm also extending the testing infrastructure to allow for a more TDD-like approach. But now I've hit a wall in one of my integration tests...
I have a class with test data, which I insert prior to the integration test. In the method that inserts these, I get the following exception:
NHibernate.PropertyAccessException: Invalid Cast (check your mapping for property type mismatches); setter of Domain.Entities.Project ---> System.InvalidCastException: Unable to cast object of type 'System.Object' to type 'Domain.Entities.ProjectModules'.
and I can't figure out why. I have two Project instances that I try to persist in the database on setup, both defined like this:
new Project("2023", "projeName", "projaddr")
{
PrincipalOwner = UserOne, // UserOne and Office are other properties
Office = Office,
// I've tried just not instantiating this too - gave the same exception
ProjectModules = new ProjectModules
{
HasModuleOne = false,
HasModuleTwo = false
});
});
The (relevant part of the) Fluent NHibernate mapping looks like this:
Component(m => m.ProjectModules, c =>
{
c.LazyLoad();
c.Map(x => x.HasModuleOne)
.Column("ModuleOne").Not.Nullable().Default("0");
c.Map(x => x.HasModuleTwo)
.Column("ModuleTwo").Not.Nullable().Default("0");
});

I've solved this - for some reason, NHibernate didn't like when the component mapping was specified inline in the mapping for Projects, but if I moved the mapping to a separate class ComponentMap<T> it worked. So I changed the problematic lines to
Component(p => p.ProjectModules);
and added the following class to my mappings assembly:
public class ProjectModulesMap : ComponentMap<ProjectModules>
{
LazyLoad.Always();
Map(pm => pm.ModuleOne);
Map(pm => pm.ModuleTwo);
}
Then everything worked as I would have expected it to from the start.

Related

How to use existing data from the database in Codeception FactoryMuffin?

I'm trying to set up easy test data in my Acceptance tests:
public function shouldUseAFakeAccountHolder(AcceptanceTester $I) {
$I->have(AccountHolder::class);
// ...
}
I've copied the example code from the Codeception documentation and modified it with my entity names (as well as fixing the bugs).
<?php
public function _beforeSuite()
{
$factory = $this->getModule('DataFactory');
// let us get EntityManager from Doctrine
$em = $this->getModule('Doctrine2')->_getEntityManager();
$factory->_define(AccountHolder::class, [
'firstName' => Faker::firstName(),
// Comment out one of the below 'accountRole' lines before running:
// get existing data from the database
'accountRole' => $em->getRepository(AccountRole::class)->find(1),
// create a new row in the database
'accountRole' => 'entity|' . AccountRole::class,
]);
}
The relationship using existing data 'accountRole' => $em->getRepository(AccountRole::class)->find(1) always fails:
[Doctrine\ORM\ORMInvalidArgumentException] A new entity was found through the relationship 'HMRX\CoreBundle\Entity\AccountHolder#accountRole' that was not configured to cascade persist operations for entity: HMRX\CoreBundle\Entity\AccountRole#0000000062481e3f000000009cd58cbd. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'HMRX\CoreBundle\Entity\AccountRole#__toString()' to get a clue.
If I tell it to create a new entry in the related table 'accountRole' => 'entity|' . AccountRole::class, it works, but then it adds rows to the table when it should be using an existing row. All the role types are known beforehand, and a new random role type makes no sense because there's nothing in the code it could match to. Creating a duplicate role works, but again it makes so sense to have a separate role type for each user since roles should be shared by users.
I've had this error before in Unit tests, not Acceptance tests, when not using Faker / FactoryMuffin, and it's been to do with accessing each entity of the relationship with a different instance of EntityManager. As soon as I got both parts using the same instance, it worked. I don't see how to override the native behaviour here though.
It works (at least in Codeception 4.x) by using a callback for the existing relation:
<?php
public function _beforeSuite()
{
$factory = $this->getModule('DataFactory');
$em = $this->getModule('Doctrine2')->_getEntityManager();
$factory->_define(AccountHolder::class, [
'firstName' => Faker::firstName(),
'accountRole' => function($entity) use ($em) {
$em->getReference(AccountRole::class)->find(1);
},
]);
}
I've found it here: https://github.com/Codeception/Codeception/issues/5134#issuecomment-417453633

NCommon + Fluent NHibernate + Multiple Databases?

I'm trying to wire NCommon and NH to multiple databases via the guidance at http://codeinsanity.com (see 'Configuring NCommon for multiple database support') and it works via the fluent approach suggested, below:
var configuration = NCommon.Configure.Using(adapter).ConfigureState<DefaultStateConfiguration>();
configuration.ConfigureData<NHConfiguration>(config => config.WithSessionFactory(() => _sessionFactories[0]).WithSessionFactory(() => _sessionFactories[1]));
This works as expected but as you can see the sessionFactories are hardcoded. What I'd really like to do is something like this:
foreach(ISessionFactory sessionFactory in _sessionFactories)
{
configuration.ConfigureData<NHConfiguration>(config => config.WithSessionFactory(() => sessionFactory));
}
But this throws the following exception:
Component NCommon.Data.NHibernate.NHUnitOfWorkFactory could not be registered. There is already a component with that name. Did you want to modify the existing component instead? If not, make sure you specify a unique name.
My hope is there's a proper way to wire-up n-SessionFactories without hardcoding them - but I'm just not seeing a solution. Any advice?
I thought I got this to work by delegating the SessionFactory piece to a method, as below:
configuration.ConfigureData<NHConfiguration>(config => ConfigureSessionFactories(config));
private void ConfigureSessionFactories(NHConfiguration configuration)
{
foreach (ISessionFactory sessionFactory in _sessionFactories)
{
configuration.WithSessionFactory(() => sessionFactory);
}
}
However, that only appears to be a solution. Say you have multiple session factories, you can use all of them to query, but only the last-added session factory will hit the database and return results. Queries against the other session factories will never hit the actual database and will return zero entities.
The only way I've been able to get this to work is:
configuration.ConfigureData<NHConfiguration>(config => config.WithSessionFactory(() => _sessionFactories[0]).WithSessionFactory(() => _sessionFactories[1]));
I'd rather not hard-code that though.. I'd much rather iterate over a loop of n-session factories... does anyone have an idea of how to accomplish this?

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.

SharpArchitecture: Using FNH's ClassMaps instead of auto mapping

I need to use ClassMaps instead of auto mapping because of legacy database. But I don't see how to tune SharpArch to use them. I tried to remove AutoPersistentModelGenerator and use the following code in the InitializeNHibernateSession method:
var config = NHibernateSession.Init(webSessionStorage,
new[]{"ApplicationConfiguration.Models.dll"});
Fluently.Configure(config)
.Mappings(m =>
{
m.FluentMappings.AddFromAssemblyOf<ConfigSchema>();
});
But I always get MappingException - "No persister for: ConfigSchema" when trying to work with the ConfigSchema.
Has anyone tried to do this?
Edit:
ConfigSchema is a part of domain model.
I'm stupid. Fluently.Configure(config) generates a new config for NHibernate. So it will never be used in my scenario. All I was need is to use the following code in the AutoPersistentModelGenerator:
public AutoPersistenceModel Generate()
{
var mappings = new AutoPersistenceModel();
mappings.AddMappingsFromAssembly(typeof(ConfigVersionMap).Assembly);
return mappings;
}
I'm not all that familiar with the S#arp project, but is ConfigSchema a type from you domain model? The generic argument T to AddFromAssemblyOf<T> should be a mapped class from your domain model.

How to configure multiple mappings using FluentHibernate?

First time rocking it with NHibernate/Fluent so apologies in advance if this is a naive question. I have a set of Models I want to map. When I create my session factory I'm trying to do all mappings at once. I am not using auto-mapping (though I may if what I am trying to do ends up being more painful than it ought to be). The problem I am running into is that it seems only the top map is taking. Given the code snippet below and running a unit test that attempts to save 'bar', it fails and checking the logs I see NHibernate is trying to save a bar entity to the foo table. While I suspect it's my mappings it could be something else that I am simply overlooking.
Code that creates the session factory (note I've also tried separate calls into .Mappings):
Fluently.Configure().Database(MsSqlConfiguration.MsSql2008
.ConnectionString(c => c
.Server(#"localhost\SQLEXPRESS")
.Database("foo")
.Username("foo")
.Password("foo")))
.Mappings(m =>
{
m.FluentMappings.AddFromAssemblyOf<FooMap>()
.Conventions.Add(FluentNHibernate.Conventions.Helpers
.Table.Is(x => "foos"));
m.FluentMappings.AddFromAssemblyOf<BarMap>()
.Conventions.Add(FluentNHibernate.Conventions.Helpers
.Table.Is(x => "bars"));
})
.BuildSessionFactory();
Unit test snippet:
using (var session = Data.SessionHelper.SessionFactory.OpenSession()) {
var bar = new Bar();
session.Save(bar);
Assert.NotNull(bar.Id);
}
You're doing it wrong. :)
Firstly, m.FluentMappings.AddFromAssemblyOf<FooMap>() and m.FluentMappings.AddFromAssemblyOf<BarMap>() are doing exactly the same thing (if FooMap and BarMap are in the same assembly). Each one just tells Fluent NHibernate to scan the assembly that contains the generic type; so if both types are in the same assembly, it'll scan it twice.
Secondly, the Conventions call is not scoped to the specific assembly you call it after, it's for the whole set of mappings. So what you're doing is supplying two conventions to set the table name to an explicit value, and the second one is the last one to be applied. What you want to do is use the x parameter (which is the entity type) and create your table name from that.
What you need is something like this:
.Mappings(m =>
{
m.FluentMappings.AddFromAssemblyOf<FooMap>()
.Conventions.Add(Table.Is(x => x.Name + "s"));
})
Obviously my implementation is naive, and depending on what your table naming convention is you might want to use a pluraliser (I believe Castle has one, but it shouldn't be hard to find one with google).
You can read up more about conventions on the Fluent NHibernate wiki.
With classmap you specify the table name in the mapping. If not specified, it will be the same as the entity class name.
class FooMap : ClassMap<Foo>
{
public FooMap()
{
Table("foos");
}
}
Conventions apply to all mappings. As you added 2 table name conventions, only 1 will take effect.
Are your FooMap and BarMap in the same assembly? You only need to add each assembly once.
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<FooMap>())
Just want to share this will automatically add plural name either s, es, ies based on the end alphabet. Not so sure about grammar and some exception but this seems ok for me. Any exception use Table("foos") like #Lachlan Roche said. E.g. Customer class will have Customers table and Category class will have Categories table.
Modified #James Gregory answer:
.Mappings(m =>
{
m.FluentMappings.AddFromAssemblyOf<FooMap>()
.Conventions.Add(Table.Is(x => GetPluralName(x.Name));
})
public static string GetPluralName(string oldName)
{
// This is the very simple. Just ignore exception like days, boys, photos and other specific nouns.
if (oldName.EndsWith("y"))
return oldName.Remove(oldName.Length - 1) + "ies";
else if (oldName.EndsWith("s") || oldName.EndsWith("e") || oldName.EndsWith("h") || oldName.EndsWith("z") || oldName.EndsWith("o")) // Sibilant consonant or "o"
return oldName + "es";
return oldName + "s";
}