insert record doesn't save related entity when using related fields

Posts   
1  /  2
 
    
neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 11-Jun-2012 13:40:36   

3.1.11.706 Final (SD.LLBLGen.Pro.DQE.SqlServer.NET20.dll) 3.1.12.0222 (SD.LLBLGen.Pro.ORMSupportClasses.NET20.dll 3.1.12.216 (SD.LLBLGen.Pro.LinqSupportClasses.NET35) DotNet 4.0 vs2010 project Adapter template

Using a DevExpress ASPxGridView and an LLBLGenProdataSource2 on an entity collection where one entity has the fields of the other entity available by setting this up in the LLB gui works fine with editing but not with a new record for me at the moment. I am using a single row, single cell approach to creating a form style control for adding and editing an entity.

I get the data like this:

        private void PerformSelect(object sender, PerformSelectEventArgs2 e)
        {
            using (var adapter = new AdapterFactory().New())
            {
                var metaData = new LinqMetaData(adapter);
                var model = new Model(metaData);
                var entities = model.CreateLinq<RegulationBaseEntity>();
                foreach (var entity in entities.Take(1))
                {
                    e.ContainedCollection.Add(entity.RegulationText.First(a=>a.LanguageCode == "en"));
                }
            }
        }

i.e. the RegulationBaseEntity fields are also accessible on the RegulationTextEntity:

So, binding the grid columns to the fields makes for an easy update of either or both tables depending on which fields the user changes. This is magic and just what I need.

However, on trying to insert a record, only the RegulationTextEntity is in the e.UoW which means the RegulationBaseEntity isn't inserted.

I think this might be because the functionality to insert when databinding takes just a new entity as specified in the LLB DS:

<llblgenpro:LLBLGenProDataSource2 ID="LLBLGenProDataSource21" runat="server"
    AdapterTypeName="Enhesa.BusinessObjects.Adapter.DatabaseSpecific.DataAccessAdapter, Enhesa.BusinessObjects.Adapter"
    DataContainerType="EntityCollection"
    LivePersistence="false"
    EntityFactoryTypeName="Enhesa.BusinessObjects.Adapter.FactoryClasses.RegulationTextEntityFactory, Enhesa.BusinessObjects.Adapter"
    OnPerformSelect="LLBLGenProDataSource21_PerformSelect"
    OnPerformWork="LLBLGenProDataSource21_PerformWork">
</llblgenpro:LLBLGenProDataSource2>

So, where is a good place to add the RegulationBaseEntity so my e.UoW.Commit(adapter) in PerformWork will take account of it?

        private void PerformWork(object sender, PerformWorkEventArgs2 e)
        {
            using (var adapter = new AdapterFactory().New())
            {
                e.Uow.Commit(adapter, true);
            }
        }
neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 11-Jun-2012 16:13:36   

I am using a workaround for this at the moment which seems a bit laborious:

1 In PerformWork handler, set the EntityToBeInserted to a field variable in the code behind if it exists (i.e. there is a new record).

2 Set its RegulationBase entity to a new one.

3 In the grid's RowInserted handler, get the base fields one by one and set the fields in the previously stored entity

4 Call adapter.SaveEntity here rather than in PerformWork.

5 in PerformSelect set the e.ContainedCollection to the stored entity if it exists

It works, which is half the battle, but it all seems a bit hacky. I am sure I am missing something that would get this to be an elegant (i.e. easy to read and maintain) approach.

  public class Presenter
    {
        private readonly IView view;
        private RegulationTextEntity regulationTextEntity;

        public Presenter(IView view)
        {
            this.view = view;
        }

        public void InitView(bool isPostBack, bool isCallback)
        {
            view.PerformSelect += PerformSelect;
            view.PerformWork += PerformWork;
            view.RowInserted += RowInserted;
        }
        /// <summary>
        /// used when creating a new row
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void RowInserted(object sender, ASPxDataInsertedEventArgs e)
        {
            regulationTextEntity.CountryCode = e.NewValues["CountryCode"].ToString();
            regulationTextEntity.RegulationReference = e.NewValues["RegulationReference"].ToString();
            regulationTextEntity.RegulationType = Convert.ToInt32(e.NewValues["RegulationType"]);
            regulationTextEntity.BaseLanguageCode = e.NewValues["BaseLanguageCode"].ToString();
            regulationTextEntity.LanguageCode = regulationTextEntity.BaseLanguageCode;
            using (var adapter = new AdapterFactory().New())
            {
                adapter.SaveEntity(regulationTextEntity, true);
            }
        }
        /// <summary>
        /// simple performwork when editing
        /// but on new record the regulationtexteEntity needs its base entity to be appended
        /// at this point, no base values are available so the are updated in the grid's RowInserted event
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void PerformWork(object sender, PerformWorkEventArgs2 e)
        {
            var elementsToInsert = e.Uow.GetEntityElementsToInsert();
            if (elementsToInsert.Count() == 1)
            {
                regulationTextEntity = (RegulationTextEntity) elementsToInsert[0].Entity;
                regulationTextEntity.Regulation = new RegulationBaseEntity();
                return;
            }
            using (var adapter = new AdapterFactory().New())
            {
                e.Uow.Commit(adapter, true);
            }
        }

        private void PerformSelect(object sender, PerformSelectEventArgs2 e)
        {
            if (regulationTextEntity != null)
            {
                e.ContainedCollection.Add(regulationTextEntity);
                return;
            }
            using (var adapter = new AdapterFactory().New())
            {
                var metaData = new LinqMetaData(adapter);
                var model = new Model(metaData);
                var entities = model.CreateLinq<RegulationBaseEntity>();
                foreach (var entity in entities.Take(1))
                {
                    e.ContainedCollection.Add(entity.RegulationText.First(a=>a.LanguageCode == "en"));
                }
            }
        }
    }
Walaa avatar
Walaa
Support Team
Posts: 14995
Joined: 21-Aug-2005
# Posted on: 11-Jun-2012 20:15:38   

Can you just try to use the PerformWork() to create the RegulationBase entity, fill it by fields from the Grid's selected row, and Add it to the UoW to be committed.

neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 12-Jun-2012 09:13:35   

Walaa wrote:

Can you just try to use the PerformWork() to create the RegulationBase entity, fill it by fields from the Grid's selected row, and Add it to the UoW to be committed.

Will do. I think that will be better than what I am currently doing although I would need to get them the same way and create fields to store them in between the RowInserting and the PerformWork handlers. (PerformWork fires before RowInserted)

However, I was wondering if creating a new EntityFactory or modifying an existing one to return the 2 entities combined would be a good idea? i.e. Something like this added to the USER_CODE_REGION (I'll test it, but it might have an obvious flaw to you):

    /// <summary>Factory to create new, empty RegulationTextEntity objects.</summary>
    [Serializable]
    public partial class RegulationTextEntityFactory : EntityFactoryBase2<RegulationTextEntity> {
        /// <summary>CTor</summary>
        public RegulationTextEntityFactory() : base("RegulationTextEntity", Enhesa.BusinessObjects.Adapter.EntityType.RegulationTextEntity, false) { }
        
        /// <summary>Creates a new RegulationTextEntity instance but uses a special constructor which will set the Fields object of the new IEntity2 instance to the passed in fields object.</summary>
        /// <param name="fields">Populated IEntityFields2 object for the new IEntity2 to create</param>
        /// <returns>Fully created and populated (due to the IEntityFields2 object) IEntity2 object</returns>
        public override IEntity2 Create(IEntityFields2 fields) {
            IEntity2 toReturn = new RegulationTextEntity(fields);
            // __LLBLGENPRO_USER_CODE_REGION_START CreateNewRegulationTextUsingFields
            ((RegulationTextEntity) toReturn).Regulation = new RegulationBaseEntity();
            // __LLBLGENPRO_USER_CODE_REGION_END
            return toReturn;
        }
        #region Included Code

        #endregion
    }

So instead of using the default RegulationTextEntityFactory in the LLB DS I could use the above modified RegulationTextEntityFactory that combines the RegulationBase and -Text entities? That way would mean other applications could benefit.

_(Note we have maybe 8 such Text/Base combinations in the system as these are our translatable services for clients) _

neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 12-Jun-2012 14:46:03   

Tried it with a custom factory. Unfortunately it doesn't work:-)

Walaa avatar
Walaa
Support Team
Posts: 14995
Joined: 21-Aug-2005
# Posted on: 12-Jun-2012 22:40:04   

Try my aproach and tell us if it works with you.

neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 12-Jun-2012 23:22:51   

Hi Walaa - Yes it works and is about the same amount of code as my workaround above, but it is in a better place:-) Thanks very much.

I was hoping to get a solution that didn't require code-behind to actually know what the fields are that get changed. That is the terrific benefit of the LLB DS PerformWork functionality when editing existing entities with prefetch paths - you just do e.UoW.Commit(adapter) and it gets sorted out for you. - Neil

Walaa avatar
Walaa
Support Team
Posts: 14995
Joined: 21-Aug-2005
# Posted on: 12-Jun-2012 23:52:39   

Another thing you could try, is to add a RegulationBaseEntity(), to the newly created RegultaiontextEntity in the PerformSelect event and not in the PerformWork event:

regulationTextEntity.Regulation = new RegulationBaseEntity();

The idea is to have the base entity created before binding to the grid.

And see if it gets saved in PerformWork or not.

neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 13-Jun-2012 00:00:07   

Walaa wrote:

Another thing you could try, is to add a RegulationBaseEntity(), to the newly created RegultaiontextEntity in the PerformSelect event and not in the PerformWork event:

regulationTextEntity.Regulation = new RegulationBaseEntity();

The idea is to have the base entity created before binding to the grid.

And see if it gets saved in PerformWork or not.

The 'New' function doesn't hit the PerformSelect at all so that won't help I'm afraid. It looks like it ignores the LivePersistence="false" and just assumes it is the default 'true'. Is that a bug?

The 'Edit' function does fire the PerformSelect event of course, and as I create the text entity with a base prefetch path for it to add to the e.ContainedCollection, that works perfectly.

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 13-Jun-2012 08:04:23   

neilx wrote:

Walaa wrote:

Another thing you could try, is to add a RegulationBaseEntity(), to the newly created RegultaiontextEntity in the PerformSelect event and not in the PerformWork event:

regulationTextEntity.Regulation = new RegulationBaseEntity();

The idea is to have the base entity created before binding to the grid.

And see if it gets saved in PerformWork or not.

The 'New' function doesn't hit the PerformSelect at all so that won't help I'm afraid. It looks like it ignores the LivePersistence="false" and just assumes it is the default 'true'. Is that a bug?

No, I think that is natural, you are not actually selecting anything, just adding a new entity to the collection.

I think the solution you have right now is acceptable. Another way to do that is write a handler for LLBLGenProDataSource2's OnEntityInserting event:

<llblgenpro:LLBLGenProDataSource2 ID="LLBLGenProDataSource21" runat="server"
    ...
    OnPerformSelect="LLBLGenProDataSource21_PerformSelect"
    OnPerformWork="LLBLGenProDataSource21_PerformWork"
    OnEntityInserting="LLBLGenProDataSource21_EntityInserting">
</llblgenpro:LLBLGenProDataSource2>
protected void LLBLGenProDataSource21_EntityInserting(object sender, CancelableDataSourceActionEventArgs e)
{
    ((RegulationTextEntity) e.InvolvedEntity).Regulation = new RegulationBaseEntity();
}
David Elizondo | LLBLGen Support Team
neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 13-Jun-2012 09:20:57   

That only attempts to insert the text entity without the base entity being inserted first and therefore crashes with:

An exception was caught during the execution of an action query: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_RegulationText_Regulation". The conflict occurred in database "enhesaSQL", table "dbo.Regulation", column 'RegID'.
The statement has been terminated.. Check InnerException, QueryExecuted and Parameters of this exception to examine the cause of this exception.

If I check the e.UoW.EntitiesToSave[0].Entity it has a RegulationTextEntity which doesn't contain any of the data for base entity._ (btw: e.UoW.EntitiesToSave = 1.)_ The RegulationTextEntity.Regulation exists though so it is certainly available for the LLB DS code to use. (If I don't do the EntityInserting handler, then the Regulation entity shows null.)

Something is happening in the LLB DS code that is bypassing the normal functionality of updating related fields on the base entity, but only on Inserts. Edits work fine.

Walaa avatar
Walaa
Support Team
Posts: 14995
Joined: 21-Aug-2005
# Posted on: 13-Jun-2012 18:32:11   

Yes, either the approach I described earlier which can be used if LivePersistence = false, and hence the PerformWork is implemented or the one David suggested for handling the Inserted event, of the DS.

neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 13-Jun-2012 21:26:03   

I am a little confused now. I always use LivePersistence = False and handle both PerformSelect and PerformWork events. PerformSelect doesn't fire when the control being databound is in insert mode.

I understand why this is so, as the ContainedCollection of the LLB DS needs to be empty for an insert. However, the LLB DS code that manages the insert seems to use the entity referenced by the factory and does not know about the prefetch path when added via the EntityInserting handler or included as custom code in the RegulationtextEntityFactory.

If I customize the RegulationTextEntityFactory to add the RegulationBaseEnty, then I would have thought it should work. It certainly works in tests that use the factory directly.

I will look at the source code for the LLB DS to see why it ignores the existence of the PrefetchPath, assuming it is available in the install - like it was in 2.6 - or on your support site.

As you have probably guessed, this is much bigger than one application for us. We have around 8 combinations of entities with Text and Base setups like this, and they form the foundation of the many applications we are writing for both internal use and our clients. This is why I am searching harder than normal for an elegant solution rather than one that needs to know about which fields are in which entity before creating a new Text/Base combo.

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 14-Jun-2012 07:22:41   

neilx wrote:

That only attempts to insert the text entity without the base entity being inserted first and therefore crashes with:

An exception was caught during the execution of an action query: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_RegulationText_Regulation". The conflict occurred in database "enhesaSQL", table "dbo.Regulation", column 'RegID'.
The statement has been terminated.. Check InnerException, QueryExecuted and Parameters of this exception to examine the cause of this exception.

If I check the e.UoW.EntitiesToSave[0].Entity it has a RegulationTextEntity which doesn't contain any of the data for base entity._ (btw: e.UoW.EntitiesToSave = 1.)_ The RegulationTextEntity.Regulation exists though so it is certainly available for the LLB DS code to use. (If I don't do the EntityInserting handler, then the Regulation entity shows null.)

It worked for me. I think that the e.UoW.EntitiesToSave=1 because the persistence queue hasn't built yet at this point. Anyway, if you commit it recursively it should save the related entity if it is a candidate for a save.

David Elizondo | LLBLGen Support Team
neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 14-Jun-2012 08:24:11   

Terrific. I'll try again ASAP. Are you able to let me see your test code?

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 14-Jun-2012 19:56:52   

Well, using Northwind, I have a page with a FormView on it to edit some order. The relevant aspx is:

<llblgenpro:LLBLGenProDataSource2 ID="_orderDS" runat="server" 
    AdapterTypeName="NW.LLBL.MSSQL.Adapter.v31.DatabaseSpecific.DataAccessAdapter, NW.LLBL.MSSQL.Adapter.v31DBSpecific" 
    DataContainerType="EntityCollection" 
    EntityFactoryTypeName="NW.LLBL.MSSQL.Adapter.v31.FactoryClasses.OrderEntityFactory, NW.LLBL.MSSQL.Adapter.v31"
    OnEntityInserting="_orderDS_EntityInserting">
</llblgenpro:LLBLGenProDataSource2>

As you see I'm using LivePersistence=true (default) and just trapping the EntityInserting to add a new customer to that order. This is how:

protected void _orderDS_EntityInserting(object sender, SD.LLBLGen.Pro.ORMSupportClasses.CancelableDataSourceActionEventArgs e)
{
    var cust = new CustomerEntity("MNLP");
    cust.CompanyName = "THis is a new customer";

    ((OrderEntity)e.InvolvedEntity).Customer = cust;
}
David Elizondo | LLBLGen Support Team
neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 14-Jun-2012 20:25:28   

Thanks for that. The difference is that your cust entity has primary key. I'll try that.

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 15-Jun-2012 04:13:09   

neilx wrote:

Thanks for that. The difference is that your cust entity has primary key. I'll try that.

Sure. I will close this for now, please reopen when you have results about that.

David Elizondo | LLBLGen Support Team
neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 15-Jun-2012 10:01:13   

It looks like adding the regulationBaseEntity in the EntityInserting event is too late.

In order to get the entity to be inserted, it needs a primary key. This is an IDENTITY table, so I use 1 and let the database and LLB add the correct one on inserting. This part works as long as I also add any not null columns with no defaults:

        private void EntityInserting(object sender, CancelableDataSourceActionEventArgs e)
        {
            var regulationBaseEntity = new RegulationBaseEntity(1) {RegulationType = 1, CountryCode = "DE"};
            ((RegulationTextEntity)e.InvolvedEntity).Regulation = regulationBaseEntity;
        }

The sql created when I enter RegulationType = 2, CountryCode = "UK" and another optional field RegulationReference = "aaaaa" via the grid's new function is

declare @p3 int
set @p3=49645
exec sp_executesql N'INSERT INTO [dbo].[Regulation] ([RegulationType], [CountryCode]) VALUES (@p2, @p3) ;SELECT @p1=SCOPE_IDENTITY()',N'@p1 int output,@p2 int,@p3 nvarchar(2)',@p1=@p3 output,@p2=1,@p3=N'DE'
select @p3

exec sp_executesql N'INSERT INTO [dbo].[RegulationText] ([RegID], [LanguageCode], [RegTitle]) VALUES (@p1, @p2, @p3)',N'@p1 int,@p2 varchar(6),@p3 nvarchar(1000)',@p1=49645,@p2='en',@p3=N'a fake title'

So, as you see, the default values I used are inserted, not the ones from the new insert. Also, the optional fields are missing.

The regulationTextEntity however has the correct data for itself, but nulls for the related fields. These are set up in the gui correctly as it works fine in all other situations.

I will look for an earlier event to do this so the base entity is added before the datasource updates the contained collection with the inserted data from the grid.

neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 16-Jun-2012 07:49:36   

I have gone back to the idea of a customised factory but added in the new information from this thread that the base entity needs to be seeded with a dummy PK.

The extra lines in my factory is:

// __LLBLGENPRO_USER_CODE_REGION_START CreateNewRegulationTextUsingFields
            if(((RegulationTextEntity)toReturn).RegID == 0)
                ((RegulationTextEntity)toReturn).Regulation = new RegulationBaseEntity { RegID = 1 };
// __LLBLGENPRO_USER_CODE_REGION_END

I need to fully test this to ensure other functionality isn't affected.

neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 16-Jun-2012 18:51:39   

This is pleasing. I can't get it to fail on new and edit with a grid and the Linq in the PerformSelect seems to be unaffected.

This looks like a generic solution to the situation one to many relationship where the one side, to all intents and purposes, is abstract. i.e. all its fields are virtual and accessible via the many entity. This is common for us as we have tables that are translated into 20+ languages, so we have a base table that contains non translatable items and a text table that contains the language specific columns.

Thanks for the suggestions and help getting me through this. It will save our team an awful lot of time. I just counted that we have 43 such entities in our database, not the 8 or so I originally thought.

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 17-Jun-2012 16:39:35   

Good you got it working with that approach now. I didn't understand well why the EntityInserting event didn't work out for you. Anyway good to know you nail it. And thanks for the feedback.

David Elizondo | LLBLGen Support Team
neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 17-Jun-2012 17:31:55   

The EntityInserting happens after the factory has created the entity and after the grid has passed the data to the LLB DS. That means when the datasource sets the fields that are related in the base entity, it doesn't exist yet so neither do the related fields. An exception isn't thrown.

  • grid sends new data to datasource

  • datasource creates entity using factory

  • datasource updates the fields in the entity

  • EntityInserting adds the base entity

  • ...too late

I needed to find a way of adding the base entity to the text entity before the datasource updates the fields in the entity. Customising the factory does that.

It would be good to get this as a designer driven feature. i.e. if the related fields come from an abstract entity, then the factory automatically adds the base entity.

(I looked at the inheritance functionality already in LLB, but that requires the sub and super entities to have the same pk.)

You can visualize this in Northwind if you were to decide that the ProductName could be localized to multiple languages. You would then remove the ProductName from the Products table and add a Text table:

CREATE TABLE [dbo].[Products_Text]
    (
      [ProductID] [int] NOT NULL ,
      [LanguageCode] [varchar](6) NOT NULL ,
      [ProductName] [nvarchar](100) NOT NULL ,
      CONSTRAINT [PK_Products_Text] PRIMARY KEY CLUSTERED
        ( [ProductID] ASC, [LanguageCode] ASC )
    )

Thus the Product entity is actually two tables. We can't map this in an updatable way to LLB unless we add an INSTEAD OF INSERT trigger to a View of the tables and reverse engineer the view to an entity.

This is, I believe, is a common way to design localisation into a database. It is certainly the way we do it to support 20+ languages for around 40+ tables (i.e. over 80 tables are involved).

Walaa avatar
Walaa
Support Team
Posts: 14995
Joined: 21-Aug-2005
# Posted on: 18-Jun-2012 20:24:55   

(I looked at the inheritance functionality already in LLB, but that requires the sub and super entities to have the same pk.)

Wouldn't this fit in your case? In your example, the ProductID field would be a PK/FK in the Products_text table.

neilx
User
Posts: 267
Joined: 02-Nov-2007
# Posted on: 18-Jun-2012 23:43:21   

Not as far as I understand it. The text table has the LanguageCode in it's primary key.LLB's inheritance is strictly is-a and expects the same primary key for both tables, so the entities are 1:1 not 1:m as in localised entities like these.

1  /  2