Is there a reverse function of .in_ in SQLAlchemy?

随声附和 提交于 2019-12-11 17:05:51

问题


I'm trying to organize my blog posts by "tags", which is just a string of words separated by commas. I want to be able to select a tag which would only show posts that have that tag somewhere in the string of tags.

For example: Post1 - tags: "world politics, technology"

Post2 -tags: "technology"

Selecting "world politics" - I only want Post1. Selecting "technology" I want Post1 and Post2.

I'm trying to use the .in_ filter function, but as of right now it won't select anything

@app.route('/index/<tag>')
def taggedindex(tag):
    posts = Post.query.filter(Post.tags.in_(tag)).all()

If I use

posts = Post.query.filter(tag == tag).all()

It will only select direct matches, obviously.


回答1:


Your current model is not in 1st normal form, since Post.tags is not atomic, or in other words does not contain a single value of its domain. This makes querying against it more challenging. For example the solution offered by @blakebjorn suffers from false positives. Say you have posts 1 and 2 with tags "nice, boat" and "ice, cool", and you're looking for posts with tag "ice". The predicate tags LIKE '%ice%' will match nice as well, and so you get both posts 1 and 2 as a result:

In [4]: session.add_all([Post(tags="nice,boat"), Post(tags="ice,cool")])

In [5]: session.commit()

In [6]: session.query(Post).filter(Post.tags.like("%ice%")).all()
Out[6]: [<__main__.Post at 0x7f83b27e5b70>, <__main__.Post at 0x7f83b27e5be0>]

The proper solution is to split the tags into single tags. In order to avoid repeating the other fields in Post you must then split tags to their own table:

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Unicode, unique=True)

Because a post can have many tags and a tag can be related to many posts, you need an association table to connect the two, also called a "secondary" table in SQLAlchemy:

post_tag = db.Table(
    "post_tag",
    db.Column("post_id", db.ForeignKey("post.id"), primary_key=True),
    db.Column("tag_id", db.ForeignKey("tag.id"), primary_key=True)
)

You would then probably want to map this relationship in your models as a many to many relationship:

class Post(db.Model):
    ...
    tags = db.relationship("Tag", secondary="post_tag")

The relationship can be used to query for posts with certain tag(s):

In [15]: session.query(Post).filter(Post.tags.any(name="ice")).all()
Out[15]: [<__main__.Post at 0x7fb45d48a518>]

In [24]: session.query(Post).filter(Post.tags.any(Tag.name.in_(["boat", "ice"]))).all()
Out[24]: [<__main__.Post at 0x7fb45d3d0470>, <__main__.Post at 0x7fb45d48a518>]

Using an association proxy you can hide the fact that tags are not just strings, but models in their own right:

class Post(db.Model):
    ...
    tag_objects = db.relationship("Tag", secondary="post_tag")
    tags = db.association_proxy("tag_objects", "name",
                                creator=lambda name: Tag(name=name))

Association proxies also support basic queries:

In [22]: session.query(Post).filter(Post.tags.any(Tag.name == "ice")).all()
Out[22]: [<__main__.Post at 0x7fb45d48a518>]

and (a bit non intuitively, since it proxies to a scalar attribute):

In [23]: session.query(Post).filter(Post.tags == "ice").all()
Out[23]: [<__main__.Post at 0x7fb45d48a518>]

Note that we made Tag.name unique, so inserting will fail if using an existing tag name. This can be solved for example using the "unique object" pattern.




回答2:


use the sql like operator with wildcards

@app.route('/index/<tag>')
def taggedindex(tag):
    posts = Post.query.filter(Post.tags.like(f'%{tag}%')).all()
    # case insensitive
    # posts = Post.query.filter(Post.tags.ilike(f'%{tag}%')).all()


来源:https://stackoverflow.com/questions/55380586/is-there-a-reverse-function-of-in-in-sqlalchemy

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!