PetaPoco - Partial Record Updates

Friday, 17 June 2011

PetaPoco now supports partial record updates where you can specify a subset of columns to update. What's more, the T4 template generated classes can now track and update just the columns that have been modified.

This post is about PetaPoco - "a tiny ORMish thing for your POCOs". Read more on the PetaPoco Project Page.

By default, when PetaPoco updates a record it updates all columns that have been mapped to properties in the associated POCO object. Depending on the use case, this is often fine but there are times where it can inadvertently overwrite fields that have been updated by other recent transactions.

Take this case for example:

var u = user.SingleOrDefault("WHERE name=@0", username);
u.last_login = DateTime.UtcNow;
u.Update();

The problem with this update is that all fields - the user's name, email address, password, everything are re-written to the database. What would be preferable is if just the last_login field was updated. We'll now we can do this like this:

u.Update(new string[] { "last_login" });

or similarly:

db.Update<user>(u, new string[] { "last_login" });

All of the old Update methods now have new overloads that accept this new parameter, which is defined as IEnumerable<string> and should specify the names of the columns (not the properties) that should be updated.

So that's useful except it's a pain to keep track of which columns need to be updated. Well the POCO classes generated by the T4 templates can now track modified properties automatically. To enable this, there's a new option that can be set in the Database.tt file.

TrackModifiedColumns = true;

When false, POCO properties are implemented the old way:

[Column] string title { get; set; }

When true, it generates accessor methods that track the modified columns:

[Column] 
public string title 
{ 
    get
    {
        return _title;
    }
    set
    {
        _title = value;
        MarkColumnModified("title");
    }
}
string _title;

And the base Record class gets a few new methods:

private Dictionary<string,bool> ModifiedColumns;
private void OnLoaded()
{
    ModifiedColumns = new Dictionary<string,bool>();
}
protected void MarkColumnModified(string column_name)
{
    if (ModifiedColumns!=null)
        ModifiedColumns[column_name]=true;
}
public int Update() 
{ 
    if (ModifiedColumns==null)
        return repo.Update(this); 

    int retv = repo.Update(this, ModifiedColumns.Keys);
    ModifiedColumns.Clear();
    return retv;
}
public void Save() 
{ 
    if (repo.IsNew(this))
        repo.Insert(this);
    else
        Update();
}

To explain this:

  • OnLoaded - is a new method that if any POCO implements will be called by PetaPoco immediately after populating it from the DB. We use this as an indicator to start tracking modified columns.
  • MarkColumnsModified - simply records the names of columns whose value has been changed since OnLoaded has been called.
  • Update and Save have been updated to pass through the list of modified columns to PetaPoco when doing an update.

One thing to note about the set accessors is that they mark the column as modified even if the value didn't actually change. This is intentional for two reasons:

  1. It makes sure that value actually gets sent to the database no matter what, helping to ensure data consistency.
  2. It means the set of queries the database sees isn't dependent on the data entered by users. eg: if two users use the same form to change their profile and one changes their email address and one changes their display name, both will result in the same update query at the database - and presumably the DB can optimize it just once.

These changes are available now in the github master branch now, NuGet package coming soon.

« PetaPoco - Mapping One-to-Many and Many-to-One Relationships Introducing PetaTest - A Tiny Unit Testing Framework »

9 Comments

Why did you choose to use IEnumerable<string> as opposed to params string[]? You're creating a string array anyway, so it would be easier to specify columns just as an arbitrary set of strings than creating an array.

17 June 2011 12:44 PM

You can mark column as modified, only whe it is really modified if(_title!=value) _title = value; MarkColumnModified("title");

You could even store the original value and test the changes against him, or allow some kind of undo.

Keep the good job!!!

20 June 2011 08:16 AM

@Robert: good point, though I suspected there would be cases where client code might want to build up a list of strings and I figured they could pass that directly instead of having to copy to an array. I guess I could add an overload for params array.

@Alberto: See the last few paragraphs in the original post as to why I don't check for changed values. If you really want this it's easy to tweak the T4 template.

20 June 2011 11:49 PM

Why not use INotifyPropertyChanged instead of making a new, but similar system?

1 July 2011 05:49 PM
Ronnie Overby

Wow, I love this thing. Great work!

8 July 2011 02:32 AM

@Jaben Cargman fuck INotifyPropertyChanged, that's basically the antithesis of micro.

@Brad Robinson on your blog post you have

i.last_login = DateTime.UtcNow;

Didn't you mean

u.last_login = DateTime.UtcNow;

20 July 2011 08:59 PM

@Chris Marisic hmm. Because it looks the almost the same from standpoint without the ability to hook an event handler.

26 July 2011 03:05 AM

@Chris: You seem to not understand INotifyPropertyChanged at all. There is only a single event on the interface that should be fired when properties are changed. I'm not sure how that could conflict with the term "micro."

That said, I don't really think it helps here, because you still have to implement it in basically the same way (on the record base class would make the most sense). It could, however, make for an interesting use case where you could listen to the event on your POCO models and trigger some other functionality.

4 August 2011 08:10 PM
radioman

How to decorate the class if the table has 2 columns as primary key (no autoincrement)?

19 December 2011 07:29 AM

Leave a comment

Name (required)
Email (required, not shown, for gravatar)
Website (optional)
Your Message
Leave these blank: