Thread: [SQLObject] Guidance on ManyToMany
SQLObject is a Python ORM.
Brought to you by:
ianbicking,
phd
From: Tom C. <su...@ic...> - 2009-07-31 08:18:39
|
Hi all, I am trying to get my head around using a ManyToMany relation between tables and hope to get some guidance here since the available docs are a bit sparse on this (no offence - I know how difficult it is to keep docs up to date :-) ) Here is what I have: class Member(SQLObject): name = StringCol(alternateID=True) roles = ManyToMany('Role') class Role(SQLObject): name = StringCol(alternateID=True) members = ManyToMany('Member') I am testing with a SQLite backend for what it's worth. Q1: Is there a way, by adding additional params to the ManyToMany(...) declarations for example, to restrict the same role to be added multiple times to the same member and vice versa? Q2: Is there a way to restrict/cascade deletes of a role or member if that role or member has a link to the other? Q3: When deleting a role or member using the .destroySelf() method, the link record in the intermediate table is not deleted. Is there a standard way to do this? I suppose an answer to Q2 will also address this. I am adding a sample python file, and the output from an iPython session, below to illustrate the questions if that would help. Thanks, Tom ------------- cut : ManyToMany.py ----------------------------------- import os from sqlobject import * myDir = os.path.realpath(os.path.dirname(__file__)) dbURI = 'sqlite://%s/ManyToMany.sqlite' % myDir sqlhub.processConnection = connectionForURI(dbURI) class Member(SQLObject): name = StringCol(alternateID=True) roles = ManyToMany('Role') class Role(SQLObject): name = StringCol(alternateID=True) members = ManyToMany('Member') def initDB(): tables = (Member, Role) for t in tables: t.createTable(ifNotExists=True) def addSamples(): members = ('John', 'Paul', 'George', 'Ringo') roles = ('drums', 'lead', 'rythm', 'bass', 'vocals') for m in members: Member(name=m) for r in roles: Role(name=r) Member.byName('John').roles.add(Role.byName('rythm')) Member.byName('Paul').roles.add(Role.byName('bass')) Member.byName('George').roles.add(Role.byName('lead')) Member.byName('Ringo').roles.add(Role.byName('drums')) ------------- cut : ManyToMany.py ----------------------------------- ------------- cut : iPython session --------------------------------- In [1]: import ManyToMany as mm In [2]: mm.initDB() In [3]: mm.addSamples() In [4]: vocals = mm.Role.byName('vocals') In [5]: john = mm.Member.byName('John') In [6]: john.roles.add(vocals) -----> Q1: Added the 'vocals' role to john, and here it is: In [7]: list(john.roles) Out[7]: [<Role 3 name='rythm'>, <Role 5 name='vocals'>] In [8]: john.roles.add(vocals) -----> Q1: ... but we can add it a second time and now have the same role twice. Some way to specify a unique index on the intermediate table could avoid this and would raise a trapable exception. In [9]: list(john.roles) Out[9]: [<Role 3 name='rythm'>, <Role 5 name='vocals'>, <Role 5 name='vocals'>] ------> Q1: Here is the corresponding rows from the intermediate table for the 'vocals' role In [10]: Q = "SELECT * from member_role where role_id=5" In [11]: mm.Role._connection.queryAll(Q) Out[11]: [(1, 5), (1, 5)] ------> Q3: Destroy the 'vocals' role In [12]: vocals.destroySelf() ------> Q3: It's gone from john's roles In [13]: list(john.roles) Out[13]: [<Role 3 name='rythm'>] ------> Q3: ... but both records are still in the intermediate table In [14]: mm.Role._connection.queryAll(Q) Out[14]: [(1, 5), (1, 5)] ------> Here we add a new role 'foo'. It may just be an SQLite related 'feature', but this new role will get the same id as the just deleted 'vocals' role had. In [15]: foo = mm.Role(name='foo') In [16]: foo Out[16]: <Role 5 name='foo'> ------> ... and due to this, john will automatically get 2 'foo' roles added to his list of roles - not good :-( In [17]: list(john.roles) Out[17]: [<Role 3 name='rythm'>, <Role 5 name='foo'>, <Role 5 name='foo'>] ------------- cut : iPython session --------------------------------- |
From: Oleg B. <ph...@ph...> - 2009-07-31 10:30:11
|
On Fri, Jul 31, 2009 at 09:36:19AM +0200, Tom Coetser wrote: > I am trying to get my head around using a ManyToMany relation between tables > and hope to get some guidance here since the available docs are a bit sparse > on this (no offence - I know how difficult it is to keep docs up to > date :-) ) The worst problem with ManyToMany is not documentation. The code was added by the original author before he left the project, and I don't know if the code by any means complete. I never used it. Consider it an unfinished experimental code. Use RelatedJoin or SQLRelatedJoin instead. > class Member(SQLObject): > name = StringCol(alternateID=True) > roles = ManyToMany('Role') > > class Role(SQLObject): > name = StringCol(alternateID=True) > members = ManyToMany('Member') > > I am testing with a SQLite backend for what it's worth. > > Q1: > Is there a way, by adding additional params to the ManyToMany(...) > declarations for example, to restrict the same role to be added multiple > times to the same member and vice versa? > > Q2: > Is there a way to restrict/cascade deletes of a role or member if that role or > member has a link to the other? > > Q3: > When deleting a role or member using the .destroySelf() method, the link > record in the intermediate table is not deleted. Is there a standard way to > do this? I suppose an answer to Q2 will also address this. What you want could be achieved by a RelatedJoin with an explicit intermediate table: http://sqlobject.org/FAQ.html#how-can-i-define-my-own-intermediate-table-in-my-many-to-many-relationship In the intermediate table you can set cascade, add unique index, and so on. Oleg. -- Oleg Broytmann http://phd.pp.ru/ ph...@ph... Programmers don't die, they just GOSUB without RETURN. |
From: Tom C. <su...@ic...> - 2009-08-11 05:41:06
|
Hi Oleg, I got sidetracked on something else and am only now able to get back to this. Thank you for your reply. On Friday 31 July 2009 12:29:55 Oleg Broytmann wrote: > On Fri, Jul 31, 2009 at 09:36:19AM +0200, Tom Coetser wrote: > > I am trying to get my head around using a ManyToMany relation between [snip] > The worst problem with ManyToMany is not documentation. The code was > added by the original author before he left the project, and I don't know > if the code by any means complete. I never used it. Consider it an > unfinished experimental code. Use RelatedJoin or SQLRelatedJoin instead. Noted, thank you. [snip] I am now using this option but have a question w.r.t. destroySelf(). Given the following: -------- cut --------------------- class Member(SQLObject): name = StringCol(alternateID=True) roles = SQLRelatedJoin('Role', intermediateTable='member_role', createRelatedTable=False) class Role(SQLObject): name = StringCol(alternateID=True) members = SQLRelatedJoin('Member', intermediateTable='member_role', createRelatedTable=False) class MemberRole(SQLObject): member = ForeignKey('Member', notNull=True, cascade=False) role = ForeignKey('Role', notNull=True, cascade=False) -------- cut --------------------- I can create members and roles and add roles to members using the addRole() auto method and vice versa for members to roles. When I call destroySelf() on a member though, I would like to **not** have that member deleted if it still has any roles assigned to it. I tried setting the cascade=False option in the intermediate table, but this does not help because it looks like the base destroySelf() method automatically deletes any RelatedJoins. So the question is: is it possible to manage the intermediate table myself, still use the RelatedJoin functionality to get the benefit of the auto add....() methods, but somehow indicate to destroySelf() whether deletes on records in the intermediate table should be cascaded or not? In other words, can I have my cake AND eat it? :-) Thanks, Tom |
From: Oleg B. <ph...@ph...> - 2009-08-12 08:06:35
|
On Tue, Aug 11, 2009 at 07:40:12AM +0200, Tom Coetser wrote: > class Member(SQLObject): > name = StringCol(alternateID=True) > roles = SQLRelatedJoin('Role', intermediateTable='member_role', > createRelatedTable=False) > > class Role(SQLObject): > name = StringCol(alternateID=True) > members = SQLRelatedJoin('Member', intermediateTable='member_role', > createRelatedTable=False) > > class MemberRole(SQLObject): > member = ForeignKey('Member', notNull=True, cascade=False) > role = ForeignKey('Role', notNull=True, cascade=False) > > When I call destroySelf() on a member though, I would like to **not** have > that member deleted if it still has any roles assigned to it. I tried setting > the cascade=False option in the intermediate table, but this does not help > because it looks like the base destroySelf() method automatically deletes any > RelatedJoins. First thing I can guess your are using a backend that doesn't support CASCADE (SQLite?) With SQLite, you can CREATE a TRIGGER to prevent deletion from the intermediate table, but you have to do it yourself. .destroySelf() actually doesn't rely on the backend support for CASCADE; instead it uses it itself, but not on the RelatedJoin's intermediate table even if the table is declared explicitly. The simplest way I see is to override .destroySelf: class Member(SQLObject): name = StringCol(alternateID=True) roles = SQLRelatedJoin('Role', intermediateTable='member_role', createRelatedTable=False) def destroySelf(self): if list(self.roles): # or self.roles.count() raise RuntimeError('Cannot delete a member (%s) that has roles' % self.id) super(Member, self).destroySelf() class Role(SQLObject): name = StringCol(alternateID=True) members = SQLRelatedJoin('Member', intermediateTable='member_role', createRelatedTable=False) def destroySelf(self): try: self.members[0] except IndexError: super(Role, self).destroySelf() else: raise RuntimeError('Cannot delete a role (%s) that has members' % self.id) I showed three different ways to check for RelatedJoin. This of course only works with .destroySelf - other means will happily delete rows. Oleg. -- Oleg Broytmann http://phd.pp.ru/ ph...@ph... Programmers don't die, they just GOSUB without RETURN. |
From: Tom C. <su...@ic...> - 2009-08-12 11:03:14
|
Hi Oleg, On Wednesday 12 August 2009 10:06:30 Oleg Broytmann wrote: > On Tue, Aug 11, 2009 at 07:40:12AM +0200, Tom Coetser wrote: > > class Member(SQLObject): > > name = StringCol(alternateID=True) > > roles = SQLRelatedJoin('Role', intermediateTable='member_role', > > createRelatedTable=False) > > > > class Role(SQLObject): > > name = StringCol(alternateID=True) > > members = SQLRelatedJoin('Member', intermediateTable='member_role', > > createRelatedTable=False) > > > > class MemberRole(SQLObject): > > member = ForeignKey('Member', notNull=True, cascade=False) > > role = ForeignKey('Role', notNull=True, cascade=False) > > > > When I call destroySelf() on a member though, I would like to **not** > > have that member deleted if it still has any roles assigned to it. I > > tried setting the cascade=False option in the intermediate table, but > > this does not help because it looks like the base destroySelf() method > > automatically deletes any RelatedJoins. > > First thing I can guess your are using a backend that doesn't support > CASCADE (SQLite?) With SQLite, you can CREATE a TRIGGER to prevent deletion No, I am using Postgres after burning my fingers during the testing stages with exactly this using SQLite. > from the intermediate table, but you have to do it yourself. > .destroySelf() actually doesn't rely on the backend support for CASCADE; > instead it uses it itself, but not on the RelatedJoin's intermediate table > even if the table is declared explicitly. The simplest way I see is to > override .destroySelf: > > class Member(SQLObject): > name = StringCol(alternateID=True) > roles = SQLRelatedJoin('Role', intermediateTable='member_role', > createRelatedTable=False) > > def destroySelf(self): > if list(self.roles): # or self.roles.count() > raise RuntimeError('Cannot delete a member (%s) that has roles' > % self.id) > super(Member, self).destroySelf() > > class Role(SQLObject): > name = StringCol(alternateID=True) > members = SQLRelatedJoin('Member', intermediateTable='member_role', > createRelatedTable=False) > > def destroySelf(self): > try: > self.members[0] > except IndexError: > super(Role, self).destroySelf() > else: > raise RuntimeError('Cannot delete a role (%s) that has members' > % self.id) > > I showed three different ways to check for RelatedJoin. This of course > only works with .destroySelf - other means will happily delete rows. Thanks. Yes, I did end up overriding .destroySelf() and that seems to work fine. The other methods of deleting records are handled directly by the backend via the cascade=False on the ForeignKeys on the intermediate table. The only awkward part is that exception handling is a bit weird due to different exceptions being raised for essentially the same reason but using different delete methods. But for now I can live with this :-) Thanks for the help. Cheers, Tom |