Suppose you have the following definitions :
class Info(Entity):
title = Field(Unicode(64))
texts = OneToMany('Text')
class Text(Entity):
acts_as_versioned()
line = Field(Unicode)
info = ManyToOne('Info')
t = Text()
When you try to access info in an item of t.versions you get
AttributeError: 'TextVersion' object has no attribute 'info'
I had a look to the versioning code and discovered that only the table
columns were duplicated. :'(
Any idea on how so solve that ?
> I had a look to the versioning code and discovered that only the
> table columns were duplicated. :'( Any idea on how so solve that ?
Sorry for the lateness of my response. I have been on vacation, and
am still catching up on all the email that I have missed out on!
Currently, the versioning extensions doesn't handle relations. Its
a problem that we are interested in tackling, and I have heard from
several people who were planning to work on it, but haven't seen any
code just yet. If you'd like to take a crack at it, we'd certainly
be open to any patches!
Best of luck -
--
Jonathan LaCour
http://cleverdevil.org
Just to say I am one of 'those people' who was working on this and am
still doing so (though much delayed by other things). The last major
discussion of this was:
<http://groups.google.com/group/sqlelixir/browse_thread/thread/50aee902ce3555fb/e4dad2b3dd27f314>
I've been playing around with the patch that Gaeten provided at the end
of that thread and experimenting generally but have not yet got
something final. My most recent thoughts on the issues are below.
As should be clear getting proper domain model traversal and versioned
m2m is nontrivial (and significantly harder than simple object
versioning which is is fairly straightforward -- its just copy on
write!). Finally just in case there are wrapping issues i note the
original text can be found in the README at (along with additional code):
<http://knowledgeforge.net/ckan/svn/vdm/trunk/vdm/elixir/>
~rufus
## Issues -- 2007-12-23
(in rough order of priority)
### 1. Support for versioned many to many
### 2. Model traversal (for 'old' objects) works correctly
* this is very closely related to versioned many to many
* minor subissue: ensure one gets continuity object not a 'version'
when you do changes
* in terms of use case below what happens if we do:
some_other_object.movie = m1r1
* not a problem if you can only do changes using HEAD
### Support delete and undeleting (State)
Current versioning model despite being versioned does not allow deleting
and undeleting of objects.
### 4. Conflicts, merging and locking
Once we have versioning we have conflicts, merging and locking ...
### 5. Revisions should be 'hidden' -- i.e. created automatically
Decision: wontfix
Why:
* problem with this is where do you set up the revision itself (e.g.
log_message, author)
* thus think it is inevitable that you create some kind of Revision
object, however no need to have to call commit on revision (just use
session.flush stuff)
* simplest approach is simply to require the user to create a revision
and attach it to session before calling flush (if absolutely require
could provide a backup where this revision is auto created if
nonexistent)
* alternative is to override flush on extension to take an argument
(or have a SessionExtension and override before_flush in some way)
-- but don't like this much
* TODO: without explicit call to commit how do we set timestamp on
revision -- could just let it be as it was when revision was
created or just override before_update on Revision
## Analysis and Use Cases
Here's the basis use case setup we will refer to below:
class Movie(Entity):
id = Field(Integer, primary_key=True)
title = Field(String(60), primary_key=True)
description = Field(String(512))
ignoreme = Field(Integer, default=0)
director = ManyToOne('Director', inverse='movies')
actors = ManyToMany('Genre', inverse='movies',
tablename='movie_2_genre')
using_options(tablename='movies')
acts_as_versioned(ignore=['ignoreme'])
class Director(Entity):
name = Field(String(60))
movies = OneToMany('Movie', inverse='director')
using_options(tablename='directors')
class Genre(Entity):
name = Field(String(60))
movies = ManyToMany('Movie', inverse='genres',
tablename='movie_2_genre')
using_options(tablename='actors')
### Versioned Many to Many
This is fairly trivial conceptually: just turn the intermediate object
in a many to many into a versioned object in its own right but also
adding a state attribute.
I.e. we implement ManyToMany explicitly using:
class Movie2Genre
movie = ManyToOne('Movie')
actor = ManyToOne('Actor')
state = Field(Integer)
acts_as_versioned()
and the association_proxy provided by sqlalchemy.
However to get this to work 'nicely' with elixir involves:
* setting up a new m2m property which is aware of state
* proper model traversal (o/w we still have the problem that we only
ever get the HEAD value for movie.genres even when using an old
version of movie).
### Model Traversal
# revision 1
m1 = Movie(id=1)
d1 = Director(id=1, name='Blogs')
m1.director = d1
flush()
ts1 = datetime.now()
# revision 2
m1 = Movie.get(1)
d1 = Director.get(2)
d1.name = 'Jones'
g1 = Genre(...)
d2 = Director(...)
m1.genres.append(g1)
m1.director = d2
flush()
ts2 = datetime.now()
# basic domain model traversal (requires remembering
revision/timestamp)
# in elixir
# assert m1r1.director.name == 'Jones'
assert m1r1.director.name == 'Blogs'
m1 = Movie.get(1)
m1r1 = m1.get_as_of(ts1)
assert len(m1r1.genres) == 0
# in elixir this results in an error as m1r1 does not have
attribute genres
# (or any m2m versioning support)
# with broken traversal but m2m attribute we get
# assert len(m1r1.genres) == 1
# note that traversal would include proper m2m versioning
#### The Issue
How do we pass around information about what the current reference
timestamp/revision this is because doing many to many involves
*traversing* the domain model i.e. we have moved from the Movie object
to the implicit Movie2Genre object and then on to the Genre object.
The key question in resolving this is deciding what we get back when we
do:
m1r1 = m1.get_as_of(ts1)
Solution 1
At present the elixir approach is that this returns a MovieVersion
object appropriate at ts1. The problem with this is that:
1. This does not behave like the continuity object (i.e. Movie) in
important respects most notably in terms of 'special' properties such as
m2m lists (and any other properties you've specially added).
2. Even if it did it would be unclear what the m2m links would work with
(i.e. point to)? Should they point to GenreVersion or to Genre (more
explicitly should we have MovieVersion2GenreVersion objects or
MovieVersion2Genre objects or ...)
To put this formally what happens when one does:
# does this return d1r1 or just d1 in elixir this returns d1
thedirector = m1r1.director
# again do we get g1 or g1r1 and what list do we get (as it was at
# r1 or now)
thegenres = m1r1.genres
# and this really gets difficult if genres were to have some foreign
# key or even another m2m
mylist = m1r1.genres.some_other_m2m
Solution 2
m1r1 is m1 (i.e. the continuity object) but with some information
telling it to return information on attribute calls as if it was at ts1.
[This was approach taken with the sqlobject code -- see ../sqlobject/
and ../../README.txt].
However this is problematic because it means overriding all property
read accesses to ensure (if necessary) the call is passed down to the
relevant history object.
To be continued ...