3

I've had a ton of trouble properly implementing many to many relationships in flask-sqlalchemy. It started with duplicate tags and now, many days later is a mapping problem. Fair warning, my code was much prettier before these issues crept up. I also added a addproduct.py file to speed up testing. So here it is.

models.py

1 from app import app, db                                                                                                                                     
2 
3 product_tags = db.Table('association',
4         db.Column('product_id', db.Integer, db.ForeignKey('product.id')),
5         db.Column('tag_name', db.Integer, db.ForeignKey('tag.name'))
6 )
7 
8 class Product(db.Model):                                                                                                                                    
9     id = db.Column(db.Integer, primary_key=True)
10     title = db.Column(db.String(128))                                                                                                                       
11     description = db.Column(db.Text)
12     image = db.Column(db.String(64))
13     link = db.Column(db.String(256))
14     price = db.Column(db.Float())
15     timestamp = db.Column(db.DateTime)                                                                                                                      
16     expiration = db.Column(db.String(6))
17     tags = db.relationship('Tag', secondary=product_tags,                                                                                                   
18             backref=db.backref('product', lazy='dynamic'))
19 
20     def __init__(self, title, description, image, link, price, timestamp, expiration,    tags):                                                                
21         self.title = title                                                                                                                                  
22         self.description = description                                                                                                                      
23         self.image = image                                                                                                                                  
24         self.link = link                                                                                                                                    
25         self.price = price                                                                                                                                  
26         self.timestamp = timestamp
27         self.expiration = expiration                                                                                                                        
28         self.tags = tags                                                                                                                                    
29         print self.title                                                                                                                                    
30 
31     def __repr__(self):                                                                                                                                     
32         return '<Title %r, Description %r, Image %r, Link %r, Price %r, Timestamp %r,    Expires %r, Tags %r>' % (self.title, self.description, self.image, sel
33 
34 class Tag(db.Model):                                                                                                                                        
35     name = db.Column(db.String(32), primary_key=True)
36 
37     def __init__(self, name):
38         self.name = name                                                                                                                                    
39 
40     def __repr__(self):                                                                                                                                     
41         return '<Tag %r>' % self.name

addproduct.py

1 from app import db                                                                                                                                          
2 from app.models import Product, Tag, product_tags
3 from datetime import datetime                                                                                                                               
4 
5 imagefolder = 'static/img/'
6 
7 title = 'product'
8 description = 'description'
9 image = 'image.jpg'
10 link = 'http://link.com'
11 price = 2000.00
12 expiration = ''
13 tags = ['tag1','tag2']                                                                                                                              
14 
15 newtags = []                                                                                                                                                
16 
17 def create_product(title, description, image, link, price, expiration, tags):                                                                               
18     image = imagefolder + image                                                                                                                             
19     tag_assoc = []
20     for tag in tags:                                                                                                                                        
21         tagcheck = Tag.query.filter_by(name=tag).first()                                                                                                    
22         if tagcheck == None:                                                                                                                                
23             tag_assoc.append(Tag(tag))                                                                                                                      
24         else:                                                                                                                                               
25             newtags.append(tag)                                                                                                                             
26 
27     product = Product(title, description, image, link, price, datetime.utcnow(),    expiration, tag_assoc)                                                     
28     create_assoc(newtags)                                                                                                                                   
29     return product                                                                                                                                          
30 
31 def create_assoc(newtags):                                                                                                                                  
32     title_search = Product.query.filter_by(title=title).first()                                                                                             
33     for tag in newtags:
34         assoc = product_tags.insert().values(product_id=title_search.id,    tag_name=tag)
35         db.session.add(assoc)                                                                                                                               
36     db.session.commit()
37 
38 if __name__ == '__main__':
39     product = create_product(title, description, image, link, price, expiration,    tags)
40     db.session.add(product)                                                                                                                                 
41     db.session.commit()                                                                                                                                     
42     create_assoc(newtags) 

The error message I get is:

11:11 ~/shop $ python addproduct.py
product
Traceback (most recent call last):
File "addproduct.py", line 39, in <module>
product = create_product(title, description, image, link, price, expiration, tags)
File "addproduct.py", line 28, in create_product
create_assoc(newtags)
File "addproduct.py", line 35, in create_assoc
db.session.add(assoc)
File "/home/username/.local/lib/python2.7/site-packages/sqlalchemy/orm/scoping.py", line 114, in do
return getattr(self.registry(), name)(*args, **kwargs)
File "/home/username/.local/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 1358, in add
raise exc.UnmappedInstanceError(instance)
sqlalchemy.orm.exc.UnmappedInstanceError: Class 'sqlalchemy.sql.expression.Insert' is not mapped

This is my first webapp not directly from a tutorial, and I am totally lost. Please help!

1 Answer 1

7

The first mistake I can see if in your models-- your association correctly has two foreign keys, but the foreign key for the tag is an Integer but your primary key within your Tag class is a String-- they should match. Apart from that the models look good.

You also should be able to simplify your create_product function:

def create_product(title, description, image, link, price, expiration, tags):
    image = imagefolder + image
    tag_list = []
    for tag in tags:
        tagcheck = Tag.query.filter_by(name=tag).first()
        if tagcheck is None:
            tag_list.append(Tag(tag))
        else:
            tag_list.append(tagcheck)
    product = Product(title, description, image, link, price, datetime.utcnow(), expiration, tag_list)
    return product

This can easily be moved inside your __init___ constructor. In your example you deal directly with the association table, but you don't have to at all-- just trust the ORM to do the correct thing, that's the beauty of SQLAlchemy.

Here's an example of what your product model could look like:

class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(128))
    description = db.Column(db.Text)
    image = db.Column(db.String(64))
    link = db.Column(db.String(256))
    price = db.Column(db.Float())
    timestamp = db.Column(db.DateTime)
    expiration = db.Column(db.String(6))
    tags = db.relationship('Tag', secondary=product_tags,
            backref=db.backref('products', lazy='dynamic'))

    def __init__(self, title, description, image, link, price, timestamp, expiration, tags):
        self.title = title
        self.description = description
        self.image = image
        self.link = link
        self.price = price
        self.timestamp = timestamp
        self.expiration = expiration

        for tag in tags:
            tagcheck = Tag.query.filter_by(name=tag).first()
            if tagcheck is None:
                self.tags.append(Tag(tag))
            else:
                self.tags.append(tagcheck)

    def __repr__(self):
        return '<{}, {}>'.format(self.title, ":".join([x.name for x in self.tags]))

And to test it out, first lets add a couple of tags to the system:

ta = Tag('cat')
tb = Tag('dog')
db.session.add_all([ta, tb])
db.session.commit()

>>> Tag.query.all()
[<Tag u'cat'>, <Tag u'dog'>]

Now lets add a product that uses those tags, plus a new tag.

p = Product(
    'title',
    'description',
    'image',
    'link',
    0.0,
    datetime.now(),
    'expiry',
    ['dog','cat','horse']
)
db.session.add(p)
db.session.commit()

When we create that product, the constructor takes each of those three string tags, and says "Hey, does a tag of this name already exist?" if so, it uses it, and if not, it creates a new tag by that name. SQLAlchemy is clever enough to know to add the new Tag into the session, and to commit it when the product is committed.

>>> Tag.query.all()
[<Tag u'cat'>, <Tag u'dog'>, <Tag u'horse'>]

Now lets find all products with the dog tag (assuming more products have been added).

>>> tag = Tag.query.get('dog')
>>> products = tag.products
>>> [x.title for x in products]
['title','other','examples']

Again, at no point am I touching the association table at all, there's no need. SQLAlchemy is saving us effort.

Sign up to request clarification or add additional context in comments.

6 Comments

If I put that in the Tag init would the tag be created no matter what? If it's a duplicate tag I don't want it created, I just want the association created. Also, thanks for pointing out the integer/string issue.
I tried the changes you suggested, but I got the error AttributeError: 'Table' object has no attribute 'append'. It is in reference to product_tags.append.
I mean add it into the product init, you pass in the tags the product needs as strings, then the product init decides if it needs to create them or if it can reuse existing ones within the database. Also I believe I've fixed the append error, I had a variable conflict in my example.
Ok, I moved the relevant functions to models.py, but I'm still having trouble only creating the relationship if the tags are already present. Here is a link to the new models.py in pastebin. Also when I just appended tagcheck it threw an error. Thanks again!
You're still directly hitting the association table for no reason-- trust the ORM! :) I'll add what I think your product model should look like to the bottom of the answer.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.