In symfony, versioning a model was not terribly difficult. I had my own specialized brute-force way of doing this.
In my experience it’s been a lot easier to write python code than PHP code, so naturally I figured this would be an easy task. It was not. I suspect that it was not easy because I have a naive understanding of Django and my symfony knowledge was fairly well grounded.
I tried looking for a generic implementation of django model versioning, but failed. So I came up with a specific solution.
How I think of versions
For my app I chose to use a sparse versioning system. I’d have one interface for interacting with a model. For example, I have a Restaurant
model and a RestaurantVersion
model.
Restaurant
would directly store immutable elements like name
or rating
or approved
and it would encapsulate versioned elements like description
by proxying to RestaurantVersion
.
This means I only need to interface with one class and let the versioning happen behind the scenes.
The Django implementation of RestaurantVersion
RestaurantVersion
held the data that I thought might suffer from corruption, and therefore require reversion. Therefore there’s nothing surprising in this table:
The only thing of note is that I’m enforcing a 1:M
relation between Restaurant
and RestaurantVersion
.
The Django implementation of Restaurant
Restaurant
is where the magic happens. It needs to do a few things:
- Do what a normal object does.
- Proxy attributes to the corresponding attribute of a
RestaurantVersion
- Generate a new
RestaurantVersion
on demand.
- (optional) Manage which version is “active”.
I say “4” is optional because I don’t do this myself. I built versioning into a few models because it was easier to do upfront than worry about it down the road.
Here’s the code I use:
It’s not rocket science, but it wasn’t necessarily easy either. Let’s look at requirements 2 and 4 in a bit more detail.
Proxy attributes to the corresponding RestaurantVersion
attributes
Proxying attributes is a way of masking the whole versioning infrastructure from the developer. We do this with __getattr__
and __setattr__
methods.
The __getattr__
determins if you are looking for description
or url
attributes of a Restaurant
. If you are then it uses the current RestaurantVersion
’s attribute.
__setattr__
is very similar, except since we’re changing a versionable attribute we’re going to make a request to a method, get_new_version()
, which we will detail later. It does what it says, though, it gets the “new” RestaurantVersion
of the Restaurant
.
Generate a new RestaurantVersion
on demand
As you can see from the above code, I am “auto-versioning”. This is done mostly via get_new_version()
. We also have to do a few tricks to make sure the bidirectional relationship gets maintained on save.
get_new_version()
either returns the current “new version”, a brand new “new version” or a “copy” of the current version.
The “copy” is done simply by setting the id
attribute of the new version to None
. Django takes care of assigning it a new id
on save, thus preserving the old version.
Note that when we create a brand new RestaurantVersion
, we don’t immediately set it’s restaurant
attribute. That’s because we haven’t saved the current restaurant
yet. It’s a “chicken and the egg” problem that gets solved in our save()
method:
We first save the Restaurant
, then we save a new version if there is one. If we save a new version we want to update the Restaurant
a second time. This ensures that there’s a 1:1
relationship between Restaurant
and the current RestaurantVersion
and it also establishes a 1:M
relationship between Restaurant
and all RestaurantVersion
s.
Conclusion and Drawbacks
While, I think that this setup works in my particular situation, I feel like it has some major flaws.
The code is quite messy and very specific to this model. My goal was to take care of this quickly, not necessarily in a reusable manner. I also was not familiar enough with the Django model to create some sort of extension.
This way of versioning does not allow for versioned attributes to be set upon instantiation of the model:
r=Restaurant(description="Great place")
won’t work. You’ll have to do:
r=Restaurant()
r.description="Great place"
I figured this was acceptable.
Lastly I’m not entirely happy with having to explicitly save the Restaurant
model twice, but I think my bidirectional relation requires this.
All in all this works in my particular situation. I’m hoping that this can be simplified. I’m curious to hear about other versioning schemes.