views:

157

answers:

2

I'm trying to use association proxy for tags, in a very similar scenario to the example in the docs. Here is a subset of my schema (it's a blog), using declarative:

class Tag(Base):
    __tablename__ = 'tags'
    id            = Column(Integer, primary_key=True)
    tag           = Column(Unicode(255), unique=True, nullable=False)

class EntryTag(Base):
    __tablename__ = 'entrytags'
    entry_id      = Column(Integer, ForeignKey('entries.id'), key='entry', primary_key=True)
    tag_id        = Column(Integer, ForeignKey('tags.id'), key='tag', primary_key=True)

class Entry(Base):
    __tablename__ = 'entries'
    id            = Column(Integer, primary_key=True)
    subject       = Column(Unicode(255), nullable=False)
    # some other fields here
    _tags         = relation('Tag', backref='entries', secondary=EntryTag.__table__)
    tags          = association_proxy('_tags','tag')

Here's how I'm trying to use it:

>>> e = db.query(Entry).first()
>>> e.tags
[u'foo']
>>> e.tags = [u'foo', u'bar']  # really this is from a comma-separated input
db.commit()
Traceback (most recent call last):
[...]
sqlalchemy.exc.IntegrityError: (IntegrityError) duplicate key value violates unique constraint "tags_tag_key"
 'INSERT INTO tags (id, tag) VALUES (%(id)s, %(tag)s)' {'tag': 'bar', 'id': 11L}
>>> map(lambda t:(t.id,t.tag), db.query(Tag).all())
[(1, u'foo'), (2, u'bar'), (3, u'baz')]

The tag u'bar' already existed with id 2; why didn't SQLAlchemy just attach that one instead of trying to create it? Is my schema wrong somehow?

+2  A: 

Disclaimer: it's been ages since I used SQLAlchemy so this is more of a guess than anything.

It looks like you're expecting SQLAlchemy to magically take the string 'bar' and look up the relevant Tag for it when performing the insert on the many-to-many table. I expect this is invalid, because the field in question ('tag') is not a primary key.

Imagine a similar situation where your Tag table is actually Comment, also with an id and a text field. You'd expect to be able to add Comments to an Entry with the same e.comments = ['u'Foo', 'u'Bar'] syntax that you've used above, but you'd want it to just perform INSERTs, not check for existing comments with the same content.

So that is probably what it's doing here, but it hits the uniqueness constraint on your tag name and fails, assuming that you're attempting to do the wrong thing.

How to fix it? Making tags.tag the primary key is arguably the correct thing to do, although I don't know how efficient that is nor how well SQLAlchemy handles it. Failing that, try querying for Tag objects by name before assigning them to the entry. You may have to write a little utility function that takes a unicode string and either returns an existing Tag or creates a new one for you.

Kylotan
Well, the example I linked to seems to imply that it'd work. At least, it seems silly for them to define this nice proxy to present the tags as a string list, only to say that I'm supposed to go and look up a tag id manually before using it.One strange thing about their example is it doesn't actually have the unique constraint on `Tag.tag`. Perhaps their example just ends up with lots of duplicate tags, at which point I may as well ditch the `Tag` object and have the tag in `EntryTag` directly. But then I'd lose the ability to have the nice backref `entries`, wouldn't I?
Chris Boyle
Actually, you're right. They even put it in their FAQ: http://www.sqlalchemy.org/trac/wiki/FAQ#Isthereawaytoautomagicallyhaveonlyuniquekeywordsorotherkindsofobjectswithoutdoingaqueryforthekeywordandgettingareferencetotherowcontainingthatkeyword and their suggested fix: http://www.sqlalchemy.org/trac/wiki/UsageRecipes/UniqueObject
Chris Boyle
Looking at it, I agree that their example is ambiguous, since you might reasonably expect that identical keywords resolve to the same instance. It would be better if they chose a 2nd table with less of a suggestion of uniqueness.
Kylotan
A: 

I've never used SQLAlchemy 0.5 yet (my last app using it was 0.4 based) but I can see one quirk in your code: you should modify the association_proxy object, not reassign it.

Try doing something like:

e.tags.append(u"bar")

Instead of

e.tags = ...

If that doesn't work, try pasting a complete working example for those tables (including the imports, please!) and I'll give you some more advice.

Alan Franzoni
Good thought, but an `append` gives the same result (duplicate key on commit). Will complete the example when I'm not at work (this isn't a work project).
Chris Boyle