Implementing a generic 'SaveOrInsert' style function

Posts   
 
    
JSobell
User
Posts: 145
Joined: 07-Jan-2006
# Posted on: 16-Feb-2022 08:41:29   

I always derive a custom 'Database' class around LinqMetaData to make things simpler, but in the I usually say something like

var existing = _db.Customer.FirstOrDefault(c => c.id = localCustomer.id) ?? new CustomerEntity();
existing.PopulateFromCustomer(localCustomer);
_db.AdapterToUse.Save(existing);

(typed from memory, so please excuse errors)

To simplify the saving I add a bool Save(IEntity2 entity) { _db.AdapterToUse.Save(entity); }

What I'd like to do is enclose the first code into a single SaveOrInsert(IEntity2 entity) that checks for the existence of the entity by its PK, then either does a Save overwriting the existing row or an insert if it's not present. What would be the best approach? Would I use GetQueryableForEntity in some way ? Is there a generic extension to fetch based on an IEntity2 ? Obviously my fallback is to do a fetch and save if not present, but this is an ETL process where it's either a replacement record or a new one, so IsNew=false would be fine.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 16-Feb-2022 09:56:48   

There are two approaches for fetching the entity in a generic manner I think. I think this only works if you know the PK values. I think the easiest would be this:

Using the IEntity2 object you can obtain the PK fields, using IEntity2.PrimaryKeyFields. Using these, you can obtain the PK values. First you need to create a new entity instance, to fetch an existing one. To do that you do:

var factory = entity.GetEntityFactory();
var newEntity = factory.Create();

Using the passed in entity instance, you can set this newEntity's pk fields. This looks something like this in this small test:

[Test]
public void GenericallyObtainEntityUsingExistingEntity()
{
    IEntity2 c = new CustomerEntity("ALFKI");
    using(var adapter = new DataAccessAdapter())
    {
        Assert.IsTrue(adapter.FetchEntity(c));

        var factory = c.GetEntityFactory();
        var newEntity = factory.Create();
        Assert.IsTrue(newEntity.IsNew);
        foreach(var pkField in c.PrimaryKeyFields)
        {
            newEntity.SetNewFieldValue(pkField.FieldIndex, pkField.CurrentValue);
        }
        
        Assert.IsTrue(adapter.FetchEntity(newEntity));
        Assert.IsFalse(newEntity.IsNew);
    }
}

If the entity doesn't exist in the database yet, newEntity.IsNew is true after the FetchEntity call. After this it doesn't matter, you can simply set the other fields from the entity instance passed in and if the newEntity was already there, different values will change fields and they'll be updated, and if newEntity wasn't there, the entity will be 'new' and it'll be inserted.

After that you simply call adapter.SaveEntity(newEntity) and it'll either update or insert based on the IsNew flag in newEntity.

The other approach is building the pk predicates manually, but I wouldn't do that, as in the end it's redoing what FetchEntity already does for you.

Keep in mind that this mechanism, which is called 'Upsert' in DB terms, is something we didn't implement on purpose as it's not reliable so you have to make sure that in the case of 'the entity isn't there yet', it can still fail with a PK violation because in the period between checking and saving the entity another thread/process might have inserted the same entity. This might not occur in your system/situation, but just in case it's something that might occur, it might be helpful to have some sort of retry mechanism controlling this simple_smile

Frans Bouma | Lead developer LLBLGen Pro