We have an object model represented roughly by the following
interfaces:
/// <summary>
/// Metadata of a version of an exported document.
/// </summary>
public interface IExport
{
Guid Id { get; }
IList<IExportRequest> Requests { get; }
IExportRequest CurrentRequest { get; }
...
}
/// <summary>
/// Represents a user's request to export a version of a document.
/// </summary>
public interface IExportRequest
{
int Id { get; }
IExport Export { get; }
IList<IExportSendRequest> SendAttempts { get; }
IExportSendRequest LastSendAttempt { get; }
DateTime CreationDate { get; }
...
}
/// <summary>
/// Represents a single attempt to send a version of a document.
/// </summary>
public interface IExportSendRequest
{
int Id { get; }
IExportRequest ExportRequest { get; }
DateTime CreationDate { get; }
...
}
The DB schema is approximately as follows:
CREATE TABLE [t_exports] (
[id] [uniqueidentifier] NOT NULL PRIMARY KEY,
[currentRequestId] [int] NULL,
...
)
CREATE TABLE [t_export_requests] (
[id] [int] NOT NULL PRIMARY KEY,
[exportId] [uniqueidentifier] NOT NULL,
[lastSendAttemptId] [int] NULL,
[creationDate] [datetime] NOT NULL,
...
)
CREATE TABLE [t_export_send_requests] (
[id] [int] NOT NULL PRIMARY KEY,
[exportRequestId] [int] NOT NULL,
[creationDate] [datetime] NOT NULL,
...
)
GO
The relationship between IExportRequest and IExportSendRequest is
basically the same as between IExport and IExportRequest: one-to-many,
plus an extra property pointing to the most recent entry.
This is a refactor of a legacy schema, hence the Guid ID for the root
entity. It is desirable to code against interfaces for similar
reasons. The 'currentRequestId' and 'lastSendAttemptId' fields are
required because another system (actually a separate part of the same
one) needs to be able to run a query like this:
SELECT ... FROM t_exports E
LEFT JOIN t_export_requests R ON E.currentRequestId = R.id
LEFT JOIN t_export_send_requests S ON R.lastSendAttemptId = S.id
WHERE ...
In practice, the current request will reside in the Requests
collection, which is sorted by CreationDate to put the newest request
first.
There are two major use cases for the object model:
1) Loading a single IExport and its current IExportRequest, but not
the Requests collection.
2) Loading a single IExport, all associated IExportRequests, and all
associated IExportSendRequests.
The second case is less common and may involve a very large number of
objects, so optimising for the first case by lazy-loading the
collection properties would be preferred. This means that the 'most
recent' properties cannot be defined in terms of the collections. The
logic that creates new ExportRequests is inside a method on Export and
does something like:
var request = new ExportRequest(this); // constructor sets ExportId =
export.Id, among other things
CurrentRequest = request;
The request should implicitly become part of the Requests collection,
without actually trying to load it. The previous value of
CurrentRequest should remain associated with the Export entity. The
Export is registered with the NHibernate session shortly after
construction, but we're relying on saves cascading through
CurrentRequest and LastSendAttempt to pick up the other two entity
types.
We're using the Fluent NHibernate automapper, which doesn't like code-
to-interface except when the concrete classes are fully proxyable
(ours aren't). Our best shot at the mapping overrides so far are:
public class ExportMappingOverride : IAutoMappingOverride<Export>
{
public void Override(AutoMapping<Export> mapping)
{
mapping.HasMany<ExportRequest>(s =>
s.Requests).Inverse().AsBag().OrderBy("creationDate DESC").LazyLoad();
mapping.HasOne<ExportRequest>(s =>
s.CurrentRequest).Cascade.SaveUpdate(); // doesn't update
currentRequestId on save
mapping.Not.LazyLoad();
}
}
public class ExportRequestMappingOverride :
IAutoMappingOverride<ExportRequest>
{
public void Override(AutoMapping<ExportRequest> mapping)
{
mapping.HasMany<ExportSendRequest>(s =>
s.SendAttempts).Inverse().AsBag().OrderBy("creationDate
DESC").LazyLoad();
mapping.HasOne<ExportSendRequest>(s =>
s.LastSendAttempt).Cascade.SaveUpdate(); // doesn't update
lastSendAttemptId on save
mapping.References<Export>(s => s.Export).Not.Nullable();
mapping.Not.LazyLoad();
}
}
public class ExportSendRequestMappingOverride :
IAutoMappingOverride<ExportSendRequest>
{
public void Override(AutoMapping<ExportSendRequest> mapping)
{
mapping.References<ExportRequest>(s =>
s.ExportRequest).Not.Nullable();
mapping.Not.LazyLoad();
}
}
This mostly works, except neither of the 'current' properties are
saved to the database and the Requests collection is always empty when
fetched. Writing the mapping files to disk yielded the following for
the Export and ExportRequest classes:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-
access="property" auto-import="true" default-cascade="none" default-
lazy="true">
<class xmlns="urn:nhibernate-mapping-2.2" lazy="false"
mutable="true" name="Export, Test, Version=7.2.0.0, Culture=neutral,
PublicKeyToken=null" table="t_exports">
<id name="Id" type="System.Guid, mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">
<column name="Id" />
<generator class="guid.comb" />
</id>
<!-- shouldn't this reference currentRequestId? -->
<one-to-one cascade="save-update" class="ExportRequest, Test,
Version=7.2.0.0, Culture=neutral, PublicKeyToken=null"
name="CurrentRequest" />
<bag inverse="true" name="Requests" mutable="true" order-
by="queuedDate">
<key>
<column name="ExportId" />
</key>
<one-to-many class="ExportRequest, Test, Version=7.2.0.0,
Culture=neutral, PublicKeyToken=null" />
</bag>
...
</class>
</hibernate-mapping>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-
access="property" auto-import="true" default-cascade="none" default-
lazy="true">
<class xmlns="urn:nhibernate-mapping-2.2" lazy="false"
mutable="true" name="ExportRequest, Test, Version=7.2.0.0,
Culture=neutral, PublicKeyToken=null" table="t_export_requests">
<id name="Id" type="System.Int32, mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">
<column name="Id" />
<generator class="hilo">
<param name="max_lo">1000</param>
</generator>
</id>
<!-- shouldn't this reference lastSendAttemptId? -->
<one-to-one cascade="save-update" class="ExportSendRequest, Test,
Version=7.2.0.0, Culture=neutral, PublicKeyToken=null"
name="LastSendAttempt" />
<bag inverse="true" lazy="true" name="SendAttempts" mutable="true"
order-by="queuedDate">
<key>
<column name="ExportRequestId" />
</key>
<one-to-many class="ExportSendRequest, Test, Version=7.2.0.0,
Culture=neutral, PublicKeyToken=null" />
</bag>
<many-to-one class="Export, Test, Version=7.2.0.0,
Culture=neutral, PublicKeyToken=null" name="Export">
<column name="ExportId" not-null="true" />
</many-to-one>
...
</class>
</hibernate-mapping>
It looks like the one-to-optional mapping is not being recognised. A
many-to-one worked for Export and ExportRequest, but it led to errors
about transient/null instances when we extended it to
ExportSendRequest. We don't really want a redundant collection of zero-
or-one Exports on the ExportRequest object either. It should not be
necessary. Nor does this explain why the Requests collection is not
being loaded, since the mappings for that look fine.
Can anyone help us solve this problem, or suggest ideas for an
alternative model/schema that might work?
Thanks,
Alex
--
You received this message because you are subscribed to the Google Groups
"nhusers" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to
[email protected].
For more options, visit this group at
http://groups.google.com/group/nhusers?hl=en.