I have a simple application, that registers a collection of money from batches of sold tickets from
drivers (which domain expert calls settlements). I try to use DDD approach and EF Core to handle this small app (call it a playground for using EF Core in DDD). I have basically 3 tables in SQL Server (simplified to the absolute minimum):
Table: Collection
-------------+---------
Column | Type
-------------+---------
CollectionId | int (PK)
IsCancelled | bit
Table: Settlement
-------------+---------
Column | Type
-------------+---------
SettlementId | int (PK)
CollectionId | int (FK)
Number | int
Table: CollectionSettlementJoin
-------------+---------
Column | Type
-------------+---------
SettlementId | int (PK)(FK)
CollectionId | int (PK)(FK)
I know there seems to be a redundant join table (since I have the CollectionId on the Settlement table), but it seems to be a design requirement, that I will explain in a moment. So each collection has at least 1 or more settlements. I have 2 domain entities which actually correspond with the tables - my aggregate root Collection and attached to it Settlements property that contains list of Settlement entities.
The extra table is used for auditing purposes as actually does not take real part in the domain. It is populated by a trigger on Settlement.CollectionId update (for non-nulls). Each collection can be cancelled within 5 minutes of its creation by the creator or anytime by a superuser. When a collection is cancelled, I want to reset Settlement.CollectionId to null (when that happens data in CollectionSettlementJoin stays and I can always get back what settlements were cancelled).
My current setup is working fine when comes to creating a collection. The selected settlements are added, saved and successfully persisted in my database. The problem starts when I want to cancel a collection. I get from the database the collection with attached settlements . But when I remove the settlements from my aggregate root, dbContext.SaveChanges() does not persist the changes (does not set Settlement.CollectionId to null).
Here is my setup:
public class Collection : EntityBase, IAggregateRoot
{
private Collection() { }
public Collection(List<Settlement> settlements)
{
_settlements = settlements;
}
private bool _isCancelled;
public bool IsCancelled => _isCancelled;
private List<Settlement> _settlements;
public IReadOnlyCollection<Settlement> Settlements => _settlements.AsReadOnly();
public void CancelCollection()
{
if (_isCancelled != true)
{
_isCancelled = true;
_settlements.Clear();
}
}
}
public class Settlement : EntityBase
{
private Settlement() { }
public Collection? Collection { get;set; }
public int? CollectionId { get; internal set; }
}
public class CollectionEntityTypeConfiguration : IEntityTypeConfiguration<Collection>
{
public void Configure(EntityTypeBuilder<Collection> builder)
{
builder.ToTable("Collection");
builder.HasKey(s => s.Id);
builder.Property(s => s.Id).HasColumnName("CollectionId").UseIdentityColumn();
builder.Property(s => s.IsCancelled).HasDefaultValueSql("(0)");
builder.HasMany(s => s.Settlements)
.WithOne(c => c.Collection)
.HasForeignKey(s => s.CollectionId);
}
}
public class SettlementEntityTypeConfiguration : IEntityTypeConfiguration<Settlement>
{
public void Configure(EntityTypeBuilder<Settlement> builder)
{
builder.ToTable("Settlement");
builder.HasKey(s => s.Id);
builder.Property(s => s.Id).HasColumnName("SettlementId").ValueGeneratedNever();
builder.Property(s => s.CollectionId);
builder.HasOne(s => s.Collection)
.WithMany(c => c.Settlements)
.HasForeignKey(s => s.CollectionId);
}
}
Context (not much here)
public class SettlementCollectionContext: DbContext
{
public SettlementCollectionContext(
DbContextOptions<SettlementCollectionContext> options) : base(options)
{
ChangeTracker.StateChanged += ChangeTracker_StateChanged;
}
private void ChangeTracker_StateChanged(object sender, EntityStateChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine($"Entity {e.Entry.Entity.GetType().Name} has changed.");
}
public DbSet<Collection> Collections { get; set; }
public DbSet<Settlement> Settlements { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new SettlementEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new CollectionEntityTypeConfiguration());
}
}
And finally my command:
var collection = await _dbContext.Collections
.Include(c => c.Settlements)
.Where(c => c.Id == collectionId)
.SingleOrDefaultAsync();
if (!collection!.IsCancelled)
{
collection.CancelCollection();
}
_dbContext.Update(collection); //without this the change tracker does not register the change
_dbContext.SaveChanges();
I know that the ChangeTracker registeres this change, becuase I added in my context an even handler to ChangeTracker.StateChanged and during debugging I noticed it register that collection has changed (although not the settlement). I also tried to reset Settlement.CollectionId property to null in Collection.CancelCollection() method, but this did not help either. I must be missing something.
So in the end nothing is wrong with the setup. Actually my unit of work was a problem, because it was saving incorrect context, so obviously the changes were never persisted.
Related
This is a tale of optional owned entities and foreign keys.
I'm working with EF 5 (code first) and I do this :
public class Parent {
public Guid Id { get; private set; }
public OwnedType1? Owned1 { get; private set; }
public OwnedType2? Owned2 { get; private set; }
public Parent(Guid id, OwnedType1? owned1, OwnedType2? owned2) {
Id = id; Owned1 = owned1; Owned2 = owned2;
}
}
public class OwnedType1 {
public Guid? OptionalExternalId { get; private set; }
public OwnedType1 (Guid? optionalExternalId) {
OptionalExternalId = optionalExternalId;
}
}
public class OwnedType2 {
public Guid? OptionalExternalId { get; private set; }
public OwnedType2 (Guid? optionalExternalId) {
OptionalExternalId = optionalExternalId;
}
}
public class Shared {
public Guid Id { get; private set; }
public Shared (Guid id) {
Id = id;
}
}
Now, the configuration :
//-------- for Parent ------------
public void Configure(EntityTypeBuilder<Parent> builder) {
builder
.ToTable("Parents")
.HasKey(p => p.Id);
builder
.OwnsOne(p => p.Owned1)
.HasOne<Shared>()
.WithMany()
.HasForeignKey(x => x.OptionalExternalId);
builder
.OwnsOne(p => p.Owned2)
.HasOne<Shared>()
.WithMany()
.HasForeignKey(x => x.OptionalExternalId);
}
//-------- for OwnedType1 ------------
// (there's no builder as they're owned and EntityTypeBuilder<Parent> is enough)
//-------- for OwnedType2 ------------
// (there's no builder as they're owned and EntityTypeBuilder<Parent> is enough)
//-------- for Shared ---------------
public void Configure(EntityTypeBuilder<Shared> builder) {
builder
.ToTable("Shareds")
.HasKey(p => p.Id);
}
Side note : If you're wondering why OwnedType1 and OwnedType2 don't each have a property called 'ParentId', it's because it's created implicitly by the "OwnsOne".
My problem is this :
When I create a new Migration, then OwnedType1 works like a charm, but for OwnedType2 (which is quasi-identical), I get his error :
The property 'OptionalExternalId' cannot be added to the type
'MyNameSpace.OwnedType2' because no property type was specified and
there is no corresponding CLR property or field. To add a shadow state
property, the property type must be specified.
I don't understand what it's complaining about. And why it's complaining only for one of them.
I know that you probably can't work it out with this simplified version of my schema, but what I'm asking is what you think it might be (follow your guts of EF guru) :
Some missing constructor?
Incorrect visibility on one of the fields?
Bad navigation definition?
A typo?
Something tricky (like : If you're going to have TWO different entity classes having a one-to-many relation with Shared, then they can't use the same name for external key. Or I need to use a composite key. Or whatnot).
It was a configuration issue that had nothing to do with Owned entities. Another case of "EF error message is obscure but issue is somewhere there in plain sight".
Unfortunately I don't remember how I fixed it. But it was along the lines of "Need an extra constructor with all the paramaters" or "one of the fields had a different name in the constructor parameters" or one of those classic EF mishaps.
I am implementing an api using .net 5.
I have a student class which have a property with address type(value object according to ddd).
public class Student
{
public long Id{ get; private set; }
public string FirstName { get; private set; }
public string LastName { get; private set; }
public Address Address { get; private set; }
}
public class Address
{
public string City { get; private set; }
public string Road { get; private set; }
}
I am using fluent api to configure the database using ef core 5.
class StudentConfiguration : IEntityTypeConfiguration<Student>
{
public void Configure(EntityTypeBuilder<Student> builder)
{
builder.ToTable("Students");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).ValueGeneratedNever().IsRequired();
builder.Property(x => x.FirstName).HasMaxLength(25).IsRequired();
builder.Property(x => x.LastName).HasMaxLength(50).IsRequired();
builder.OwnsOne(x => x.Address, x =>
{
x.Property(pp => pp.City)
.IsRequired()
.HasColumnName("City")
.HasMaxLength(20);
x.Property(pp => pp.Road)
.IsRequired()
.HasColumnName("Road")
.HasMaxLength(40);
});
}
}
As a result I have one table with columns Id,Fistname,lastname,city,road.
Now I am trying to update only the city and the road(for example a student change house)
but I have different exceptions and I don't know how to update only these 2 columns
public async Task UpdateAddress(Student student)
{
//Firts try
//var studentEntry = context.Entry(student);
//studentEntry.Property(x => x.Address.City).IsModified = true;
//studentEntry.Property(x => x.Address.Road).IsModified = true;
//**Exception** 'The expression 'x => x.Address.City' is not a valid member access expression. The expression should represent a simple property or field access: 't => t.MyProperty'. (Parameter 'memberAccessExpression')'
//Second try
//var studentEntry = context.Entry(student.Address);
//studentEntry.Property(x => x.City).IsModified = true;
//studentEntry.Property(x => x.Road).IsModified = true;
//**Exception** 'Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
//the only method that works is Update but this update the whole object
context.Update(student);
await context.SaveChangesAsync();
}
EDIT
public class StudentDbContext : DbContext
{
public DbSet<Student> Students { get; set; }
public StudentDbContext(DbContextOptions<StudentDbContext> options) : base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
base.OnModelCreating(modelBuilder);
}
}
How can update only these two properties of an owned entity?
Address is owned entity type, hence Student.Address property by EF Core terminology is not a property, but reference navigation property, thus should be accessed via Reference method rather than Property method (none of them supports property path). Then you can use the returned tracking entry to access its members.
To force updating just some properties of the Student.Address, first attach the Student entity instance (to tell EF that it is existing)
var studentEntry = context.Attach(student);
and then use something like this
var addressEntry = studentEntry.Reference(e => e.Address);
addressEntry.TargetEntry.Property(e => e.City).IsModified = true;
addressEntry.TargetEntry.Property(e => e.Road).IsModified = true;
Since your queries are not tracked by EntityFramework (because you set that in your DBContext configuration) you can try the following flow:
Firstly get student from DB by its ID and then change only those two properties you want to change on that entity directly.
public async Task UpdateAddress(Student student)
{
// Get existing student by its ID from the database
var existingStudent = await context.Students
.Include(x => x.Address)
.SingleOrDefaultAsync(x => x.Id == student.Id);
// To prevent null reference exception
if (existingStudent is null)
return; // Exception maybe? Depends on your app flow
// Edit address value object with the method available in your entity, since you're using DDD approach
existingStudent.ChangeAddress(student.Address);
// Since your query are not tracked you need to explicity tell EF that this entry is being modified
context.Entry(existingStudent).State = EntityState.Modified;
// EF will save only two properties in that case
await context.SaveChangesAsync();
}
Than in your Student entity class add the following method to provide ability to change its address:
public void ChangeAddress(Address newAddress)
{
// Some additional conditions to check the newAddress for nulls, valid values, etc.
Address = newAddress;
}
You can treat your Address as a ValueObject and replace it with the new Address VO.
I have a one-to-many relationship that am trying to update but get the error
Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
UPDATED
The update method will work for as long as the beneficiaries collection is not being changed or updated.
The code looks like
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
private readonly List<Beneficiary> _beneficiaries;
public IEnumerable<Beneficiary> Beneficiaries => _beneficiaries;
public void AddBeneficiary(string name)
{
var beneficiary = new Beneficiary(Id, name);
_beneficiaries.Add(beneficiary);
}
}
public sealed class Beneficiary
{
public int Id { get; set; }
public int EmployeeId { get; private set; }
public string Name { get; private set; }
public Beneficiary(int employeeId, string name)
{
Name = name;
EmployeeId = employeeId;
}
}
//Extracted from repo method
public void Update(TEntity entity)
{
if (entity != null)
_dbSet.Update(entity);
}
// Extracted Actual update from application service
if (incomingEmployee.Beneficiaries.Any())
{
if (employeeFromStore.Beneficiaries.Any())
{
employeeFromStore.Beneficiaries.ToList()
.ForEach(existingBeneficiary =>
employeeFromStore.RemoveBeneficiary(existingBeneficiary)); //Method skipped for brevity
//Tried calling this too
//_employeeRepository.UnitOfWork.SaveChanges();
}
incomingEmployee.Beneficiaries.ToList().ForEach(beneficiary =>
{
employeeFromStore.AddBeneficiary(beneficiary.Name);
});
}
_employeeRepository.Update(employeeFromStore);
_employeeRepository.UnitOfWork.SaveChanges();
I feel the issue is in how the tracker treats the update for navigation properties collection but I could be wrong.
I have also attempted to use the change tracker for updates with a method like below
public void ApplyCurrentValues<TEntity>(TEntity original, TEntity current)
where TEntity : class
{
//if it is not attached, attach original and set current values
base.Entry<TEntity>(original).CurrentValues.SetValues(current);
}
Does anyone have an idea how to go about this?
I finally made headway after reading about how update works. Basically as mentioned here
, and this issue. When Update method is invoked the state of the whole entity including all reachable entities through navigation properties is marked as modified.
If you are trying to add beneficiaries to an existing employee, the SaveChanges() throws since it expects to modify a record(s) with the primary key(s) provided but finds none in the database. Update method seemed not the best candidate.
I opted to make the Update method from the repository base overridable in the EmployeeRepository as below
//Update method in the repository base
//Made method virtual to enable overriding
public virtual void Update(TEntity entity)
{
if (entity != null)
_dbSet.Update(entity);
}
//Update method from EmployeeRepository
public override void Update(Employee employee)
{
var employeeFromStore = _unitOfWork.Employees.Where(p => p.Id == employee.Id)
.Include(p => p.Beneficiaries)
.SingleOrDefault();
if (employeeFromStore != null)
{
context.Entry(employeeFromStore).CurrentValues.SetValues(employee);
//Replacing the whole collection
foreach (var beneficiary in employeeFromStore.Beneficiaries)
{
if (employee.Beneficiaries.Any())
context.Entry(beneficiary).State = EntityState.Deleted;
}
//Adding new collection
foreach (var beneficiaryToAdd in employee.Beneficiaries)
{
var newBeneficiary = new Beneficiary(beneficiaryToAdd.EmployeeId, beneficiaryToAdd.Name);
employeeFromStore.AddBeneficiary(newBeneficiary.Name);
}
}
}
I believe this answer can be improved or tweaked to suit specific situations. For instance, other than clearing the persisted collection, one can opt for an update
here is my order and orderItem classe:
public class Order : AggregateRootBase<OrderId>
{
public string CustomerName { get; private set; }
public IList<OrderItem> Items { get; set; }
public DateTime RegisterDatetime { get; private set; }
}
public class OrderItem : ValueObjectBase
{
public long Id { get; private set; }
public long OrderId { get; set; }
public long Number { get; private set; }
public long Goods { get; private set; }
public double UnitPrice { get; private set; }
}
Im using nhibernate as my orm. in mapping this code i want order to be in Orders table & orderItem to be stored in a diffrente table called OrderItems.
here is my mapping:
public class OrderMapping : ClassMapping<Order>
{
public OrderMapping()
{
Table("Orders");
Lazy(false);
ComponentAsId(a => a.EntityId, a => { a.Property(x => x.DbId, x => x.Column("Id")); });
Property(a=>a.CustomerName);
Property(a => a.RegisterDatetime);
Bag(a => a.Items,
mapper => {
mapper.Inverse(true);
mapper.Cascade(Cascade.None);
mapper.Table("OrderItems");
mapper.Key(k => k.Column(columnMapper => columnMapper.Name("OrderId")));
},
relation => { relation.OneToMany(); });
}
}
public class OrderItemMapping : ClassMapping<OrderItem>
{
public OrderItemMapping()
{
Lazy(false);
Id(a => a.Id);
Table("OrderItems");
Property(a => a.OrderId);
Property(a => a.Number);
Property(a => a.Goods);
Property(a => a.UnitPrice);
}
}
also i have created tables in database but when i insert order with 3 orderItems, it inserts order but not orderitems
thanks for your help
You have mapped the parent Order as mapper.Inverse(true); for the Items bag which tells NHibernate that you do not want the parent to map this relation.
Since the child OrderItem has no mapping to the parent, then nothing is saving the relation. When you write a collection mapping, at least one side must be inverse(false).
You have also set mapper.Cascade(Cascade.None);, which tells NHibernate that you do not want the parent Order to deal with any operations on the Items when they change in Session.
So unless you are explicitly calling Save() on each Item, then they will never be saved as it stands.
There is a trade off in NHibernate between free class layout, and optimum database performance (although very minor in this case).
If you really don't want the OrderItem to have an Order property linking back to it's parent, then you will get an additional UPDATE call to SQL whenever the parent Order of an OrderItem changes, this cost is in practice negligible if creation of OrderItems is less than ~10% of operations that you do with them.
In that case you can set inverse(false) on the OrderMapping to Items.
But my adivce would be to give OrderItem an Order field or property (you can map a private field using NHibernate!) and then give the OrderItemMapping a map back to the parent with inverse(false), so that when children are saved, they will deal with the relation. You will have to make sure that each OrderItem has it's Order field/property filled in before save though!
You might be able to wing this by using the OrderId property instead of a full reference to Order, but you'd have to look that up.
As for making them save to the DB, the easiest way is to change mapper.Cascade(Cascade.None); to mapper.Cascade(Cascade.AllDeleteOrphan); (may not be exact class name). This will make sure that whenever you modify that collection on the Order, then Save()/Update() the Order, all OrderItems in the Items collection will be updated in the DB accordingly.
You could also check out less strict Cascades or save them manually as your current setup would require.
Lastly check out bag vs set in the Nhibernate docs, I suspect you want a set here, I would only use bag with inverse(true) ever. If you use an inverse(false) bag there is a performance penalty, unless the items can be identified (your OrderItem has an Id, so it can!), but if the items can be identified, then why not just make it a set!
The only reason you'd have an inverse(false) bag with identifiable items, is if those identifiers didn't correlate to a Primary Key in the database, that's not a situation that comes up very often (or ever in my case!).
thank you #starlight54 but my problem solved like this.
i had to change my classes a little like this:
public class Order : AggregateRootBase<OrderId>
{
private readonly IList<OrderItem> _items;
public string CustomerName { get; private set; }
public DateTime RegisterDatetime { get; private set; }
public IReadOnlyCollection<OrderItem> Items => new ReadOnlyCollection<OrderItem>(_items);
}
public class OrderItem : ValueObjectBase
{
private readonly long _number;
private readonly long _goods;
private readonly double _unitPrice;
public long Number => _number;
public long Goods => _goods;
public double UnitPrice => _unitPrice;
}
notice that in order item you have to create fields like _number, _goods & ...
and this is the mapping of order:
public class OrderMapping : ClassMapping<Order>
{
public OrderMapping()
{
Table("Orders");
Lazy(false);
ComponentAsId(a => a.EntityId, a => { a.Property(x => x.DbId, x => x.Column("Id")); });
Property(a => a.CustomerName);
Property(a => a.RegisterDatetime);
IdBag(a => a.Items, map => {
map.Access(Accessor.Field);
map.Table("OrderItems");
map.Key(a => a.Column("OrderId"));
map.Id(a => {
a.Column("Id");
a.Generator(Generators.Identity);
});
}, relation => relation.Component(map => {
map.Access(Accessor.Field);
map.Property(a => a.Number, a => a.Access(Accessor.Field));
map.Property(a => a.Goods, a => a.Access(Accessor.Field));
map.Property(a => a.UnitPrice, a => a.Access(Accessor.Field));
}));
}
}
in nhibernate there is IdBag that helped me do exactly what i needed. notice than there is no need to create class mapping for orderItem class. nhibernate inserts it automatic.
but you have to create databases manually.
i hope this helps you.
I'm struggling to understand why when I remove a child Settings object from MyUser.Settings and SAVE MyUser I get SQL errors like below:
Cannot insert the value NULL into column 'MyUserId', table '###.Settings'; column does not allow nulls. UPDATE fails.
The statement has been terminated.
What I would expect to happen is that removing the item from the collection, then saving MyUser causes NHibernate to issue a DELETE command for the given child. However, what it does is UPDATE the relevant row for the Settings object, setting MyUserId to NULL - which isn't allowed as I'm using a Composite Key.
I've tried so many combinations of Inverse() and the various Cascade options but nothing seems to work. I should point out that Adding to the collection works perfectly when I save MyUser.
I'm totally baffled!
Below is pseudo code to try and explain my entities and mappings.
public class SettingType
{
public virtual int SettingTypeId { get; set; }
public virtual string Name { get; set; }
public virtual bool Active { get; set; }
}
public class Setting
{
public virtual MyUser MyUser { get; set; }
public virtual SettingType SettingType { get; set; }
public virtual DateTime Created { get; set; }
}
public class MyUser
{
public virtual int MyUserId { get; set; }
public virtual IList<Setting> Settings { get; set; }
public virtual string Email { get; set; }
public void AddSetting(SettingType settingType, DateTime now)
{
var existing = _settings.SingleOrDefault(s => s.SettingType.SettingTypeId == settingType.SettingTypeId);
if (existing != null)
{
existing.Updated = now;
}
else
{
var setting = new Setting
{
MyUser = this,
SettingType = settingType,
Created = now,
};
_settings.Add(setting);
}
}
public void RemoveSetting(SettingType settingType)
{
var existingPref = _settings.SingleOrDefault(s => s.SettingType.SettingTypeId == settingType.SettingTypeId);
if (existingPref != null)
{
_settings.Remove(existingPref);
}
}
private readonly IList<Setting> _settings = new List<Setting>();
}
And my mappings:
public class SettingTypeMap : IAutoMappingOverride<SettingType>
{
public void Override(AutoMapping<SettingType> mapping)
{
mapping.Table("SettingTypes");
mapping.Id(m => m.SettingTypeId).GeneratedBy.Identity();
mapping.Map(m => m.Name).Not.Nullable().Length(100);
mapping.Map(m => m.Active).Not.Nullable().Default("0");
}
}
public class SettingMap : IAutoMappingOverride<Setting>
{
public void Override(AutoMapping<Setting> mapping)
{
mapping.Table("Settings");
mapping.CompositeId()
.KeyReference(m => m.MyUser)
.KeyReference(m => m.SettingType);
mapping.Map(m => m.Created).Not.Nullable().Default("CURRENT_TIMESTAMP");
mapping.Map(m => m.Updated).Nullable();
}
}
public class MyUserMappingOverride : IAutoMappingOverride<MyUser>
{
public void Override(AutoMapping<MyUser> mapping)
{
mapping.Table("MyUsers");
mapping.Id(m => m.MyUserId).GeneratedBy.Identity();
mapping.Map(m => m.Email).Not.Nullable().Length(200);
mapping.HasMany(m => m.Settings).KeyColumn("MyUserId").Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
}
}
All using:
FluentNHibernate v1.3.0.733
NHibernate v3.3.1.4000
UPDATE: After a few suggestions I've tried to change the mapping for MyUser entity.
First to this:
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
This gives the error: Given key was not present in the dictionary
So tried to add second key column:
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.KeyColumn("SettingTypeId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
But this then causes odd behaviour when loading the Settings collection from the DB for a given MyUserId. Looking at the nh profiler I see a second SELECT ... FROM Settings but setting the SettingTypeId same as value for MyUserId.
Still totally baffled. Has cost me too much time so going to revert to adding a primary key id field to the Settings entity. Maybe you just can't do what I'm trying using NHibernate. In pure SQL this is simple.
You should use the Inverse mapping
mapping.HasMany(m => m.Settings)
.KeyColumn("MyUserId")
.Inverse()
.Cascade.DeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
This will allow NHibernate to ask the setting itself to be deleted. Otherwise, NHibernate firstly tries to delete the relation, and would try to delete the entity.
See: 6.4. One-To-Many Associations
Very Important Note: If the column of a
association is declared NOT NULL, NHibernate may cause constraint
violations when it creates or updates the association. To prevent this
problem, you must use a bidirectional association with the many valued
end (the set or bag) marked as inverse="true". See the discussion of
bidirectional associations later in this chapter.