Create An Object With A Many-To-Many Relationship - python

How do I initialize a group object with the list of members that are part of the group (and the users have to be aware of the groups that they are a part of)?
Users have many groups. Groups have many users.
I have also tried an add_to_group method in the User class, but that didn't really work out.
This is my first time dealing with a many-to-many relationship, so I haven't figured out how to do it yet, and all SO posts refer to query many-to-many relationships rather than creating objects that use them.
class User(db.Model):
__tablename__ = 'User'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique = False, nullable = False)
username = db.Column(db.String(80), unique = True, nullable = False)
fb_id = db.Column(db.String(80), unique = True, nullable = False)
groups = db.relationship('Groups', secondary=groups, backref=db.backref('User', lazy='dynamic'))
groups = db.Table('groups',
db.Column('group_id', db.Integer, db.ForeignKey('Group.id')),
db.Column('user_id', db.Integer, db.ForeignKey('User.id'))
)
class Group(db.Model):
__tablename__ = 'Group'
id = db.Column(db.Integer, primary_key=True)
last_updated = db.Column(db.DateTime, mutable=True)
def __init__(self):
self.last_updated = datetime.utcnow();
Thanks!

In SQLAlchemy, many-to-many relations are modeled as attributes whose values are lists.
u = User()
g = [Group(), Group(), Group()]
u.groups = g
You can also alter the list in-place:
g1 = Group()
u.groups.append(g)
The other side of the relationship would work in the same way:
g.users.append(User())
When you alter one side of a many-to-many relationship, SQLAlchemy keeps the other side up-to-date—in other words, if you remove a User from a Group's users list, then that Group will no longer appear in that User's groups list.

Related

Ordering relationship by property of child elements

I use flask-sqlalchemy on a Flask project to model my database.
I need to sort the elements of a many-to-many relationship based on properties of different child elements of one side.
I have "Work" (the parent element), "Tag" (the children), "Type" (a one-to-many relationship on Tag) and "Block" (a one-to-many relationship on Type). Tags and Works are joined with a mapping table "work_tag_mapping".
In essence, each tag has exactly one type, each type belongs to exactly one block, and many tags can be added on many works.
I now want the list of tags on a work be sorted by block first and type second (both have a "position" column for that purpose).
Here are my tables (simplified for the sake of the question):
class Work(db.Model):
__tablename__ = 'work'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255, collation='utf8_bin'))
tags = db.relationship('Tag', order_by="Tag.type.block.position, Tag.type.position", secondary=work_tag_mapping)
class Tag(db.Model):
__tablename__ = 'tag'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255, collation='utf8_bin'))
type_id = db.Column(db.Integer, db.ForeignKey('type.id'), nullable=False)
type = db.relationship('Type')
work_tag_mapping = db.Table('work_tag_mapping',
db.Column('id', db.Integer, primary_key=True),
db.Column('work_id', db.Integer, db.ForeignKey('work.id'), nullable=False),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), nullable=False)
)
class Type(db.Model):
__tablename__ = 'type'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255, collation='utf8_bin'))
position = db.Column(db.Integer)
block_id = db.Column(db.Integer, db.ForeignKey('block.id'), nullable=False)
block = db.relationship('Block')
class Block(db.Model):
__tablename__ = 'block'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255, collation='utf8_bin'))
position = db.Column(db.Integer)
Now, it is the "order_by" in the "tags" relationship that doesn't work as I initially hoped.
The error I get is "sqlalchemy.exc.InvalidRequestError: Property 'type' is not an instance of ColumnProperty (i.e. does not correspond directly to a Column)."
I am new to SQLalchemy, Flask and indeed Python, and none of the ressources or questions here mention a case like this.
While this appears not to be possible directly, adding a getter and performing the sorting on retrieval does the trick. Adding lazy='dynamic' ensures the collection behaves as a query, so joins can be performed.
_tags = db.relationship('Tag', lazy='dynamic')
#hybrid_property
def tags(self):
return self._tags.join(Type).join(Block).order_by(Block.position, Type.position)

Item and Category database relationship

So I would like to have users add an item and an arbitrary category. Right now I use if statements to make sure that if the category has been created already, not to add it again. Is there a better way to make use of SQLAlchemy relationships so that I could skip some of the logic I had to write to ensure that the categories are unique?
Here are the model's I used:
class Category(Base):
__tablename__ = 'category'
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
class Item(Base):
__tablename__ = 'item'
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
description = Column(String)
category_id = Column(Integer, ForeignKey('category.id'))
category = relationship(Category)
date_created = Column(DateTime)
date_updated = Column(DateTime)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship(User)
Here is an example of how I would edit an item:
if new_category_name != category.name:
if db_session.query(Category).\
filter_by(name=new_category_name).count() == 0:
new_category = Category(name=new_category_name)
else:
new_category = db_session.query(Category)\
.filter_by(name=new_category_name).one()
is_last_of_category = db_session.query(Item)\
.filter_by(category_id=item.category_id).count() == 1
if is_last_of_category:
db_session.delete(category)
item.category = new_category
db_session.commit()
Any other suggestions you are willing to make I am happy to listen to.
Use the unique constraint,
Quoting from sqlalchemy's docs
unique – When True, indicates that this column contains a unique
constraint, or if index is True as well, indicates that the Index
should be created with the unique flag. To specify multiple columns in
the constraint/index or to specify an explicit name, use the
UniqueConstraint or Index constructs explicitly.
Example from sqlalchemy documentation:
from sqlalchemy import UniqueConstraint
meta = MetaData()
mytable = Table('mytable', meta,
# per-column anonymous unique constraint
Column('col1', Integer, unique=True),
Column('col2', Integer),
Column('col3', Integer),
# explicit/composite unique constraint. 'name' is optional.
UniqueConstraint('col2', 'col3', name='uix_1')
)

SqlAlchemy Table and Query Issues

Still wrapping my head around SqlAlchemy and have run into a few issues. Not sure if it is because I am creating the relationships incorrectly, querying incorrect, or both.
The general idea is...
one-to-many from location to user (a location can have many users but users can only have one location).
many-to-many between group and user (a user can be a member of many groups and a group can have many members).
Same as #2 above for desc and user.
My tables are created as follows:
Base = declarative_base()
class Location(Base):
__tablename__ = 'location'
id = Column(Integer, primary_key=True)
name = Column(String)
group_user_association_table = Table('group_user_association_table', Base.metadata,
Column('group_id', Integer, ForeignKey('group.id')),
Column('user_id', Integer, ForeignKey('user.id')))
class Group(Base):
__tablename__ = 'group'
id = Column(Integer, primary_key=True)
name = Column(String)
users = relationship('User', secondary=group_user_association_table, backref='group')
desc_user_association_table = Table('desc_user_association', Base.metadata,
Column('desc_id', Integer, ForeignKey('desc.id')),
Column('user_id', Integer, ForeignKey('user.id')))
class Desc(Base):
__tablename__ = 'desc'
id = Column(Integer, primary_key=True)
name = Column(String)
users = relationship('User', secondary=desc_user_association_table, backref='desc')
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
user_name = Column(String)
location_id = Column(Integer, ForeignKey('location.id'))
groups = Column(String, ForeignKey('group.id'))
descs = Column(String, ForeignKey('desc.id'))
location = relationship('Location', backref='user')
Here are some examples as to how I am creating the data (all being scraped from the web):
location = Location(id=city[1], name=city[0]) #city = ('name', id)
profile = User()
profile.id = int(str(span2class[0].a['href'][7:]))
profile.user_name = str(span2class[0].a.img['alt'])
profile.location_id = location.id
g = Group(id=gid, name=str(group.contents[0])) # add the group to the Group table
self.db_session.add(g)
# Now add the gid to a list that will be added to the profile that eventually gets added to the user table
profile.groups.append(str(gid)) # stick the gid into the list
profile.groups = ','.join(profile.groups) # convert list to csv string
# Repeat basically same thing above for desc
self.db_session.add(profile)
self.db_session.commit()
As far as queries go, I've got some of the basic ones working such as:
for instance in db_session.query(User).all():
print instance.id, instance.user_name
But when it comes to performing a join to get (for example) group.id and group.name for a specific user.id... nothing I've tried has worked. I am guessing that the form would be something like the following:
db_session.query(User, Group).join('users').filter(User.id==42)
but that didn't work.
Joins works from left to right, so you should join on the relationship from User to Group:
db_session.query(User, Group).join(User.group).filter(User.id == 42)
But this return you a list of tuples (<User>, <Group>), so if the user belongs to 2 or more groups, you will receive 2 or more rows.
If you really want to load both the user and its groups in one (SQL) query, a better way would be to load a user, but configure query to preload groups:
u = (session.query(User)
.options(joinedload(User.group))
.get(42)
)
print("User = {}".format(u))
for g in u.group:
print(" Group = {}".format(g))

Filed in SQLAlchemy of user's list

I have one model that is Users in which there is a field in this model that I would like to store a list of Users. The idea is that you can add frieds and store them somewhere.
class User (db.Model):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(50), unique = True)
email = Column(String(120), unique = True)
password = Column(String(50))
date = Column(DateTime(), default=datetime.now())
friends = "Should be a list of users"
I have thought to have a string with the id of each user but, is there any posibility to do it with a relationship to the same model? like this:
friends = relationship("User")
Thanks a lot!
Proposed solutions based on Adjacency List Relationships would only work in case when someone can be a friend of maximum one person, which I do not believe to be the case in the real world.
A pattern you need to apply in this case is called Self-Referential Many-to-Many Relationship. Please read the sample linked to above. In order to make it work for your model, you would need to create additional table to keep the pairs of friends, and configure the relationship as below:
# object model
t_userfriend = Table("user_friend", Base.metadata,
Column("user_id", Integer, ForeignKey("users.id"), primary_key = True),
Column("friend_id", Integer, ForeignKey("users.id"), primary_key = True),
)
class User (Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String(50), unique = True)
# ...
friends = relationship("User",
secondary = t_userfriend,
primaryjoin = (id == t_userfriend.c.user_id),
secondaryjoin = (id == t_userfriend.c.friend_id),
backref = "friend_of",
)
I guess that the other question you need to ask yourself is whether in your model if A is a friend of B, does this mean that B is a friend of A? In case this is true, you might want/need to:
either store just one side of the relationship, and calculate the other
make sure you always store both sides to the relationship
You can use Adjacency List Relationship and this link have the same issue so you can learn from it.
How to create relationship many to many in SQLAlchemy (python, flask) for model User to itself
Yes you can do it with Adjacency List Relationships.
class User (db.Model):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(50), unique = True)
email = Column(String(120), unique = True)
password = Column(String(50))
date = Column(DateTime(), default=datetime.now())
friends = relationship("User",
backref=backref('parent', remote_side=[id])
)

How to avoid inserting duplicate entries when adding values via a sqlalchemy relationship?

Let's assume we have two tables in a many to many relationship as shown below:
class User(db.Model):
__tablename__ = 'user'
uid = db.Column(db.String(80), primary_key=True)
languages = db.relationship('Language', lazy='dynamic',
secondary='user_language')
class UserLanguage(db.Model):
__tablename__ = 'user_language'
__tableargs__ = (db.UniqueConstraint('uid', 'lid', name='user_language_ff'),)
id = db.Column(db.Integer, primary_key=True)
uid = db.Column(db.String(80), db.ForeignKey('user.uid'))
lid = db.Column(db.String(80), db.ForeignKey('language.lid'))
class Language(db.Model):
lid = db.Column(db.String(80), primary_key=True)
language_name = db.Column(db.String(30))
Now in the python shell:
In [4]: user = User.query.all()[0]
In [11]: user.languages = [Language('1', 'English')]
In [12]: db.session.commit()
In [13]: user2 = User.query.all()[1]
In [14]: user2.languages = [Language('1', 'English')]
In [15]: db.session.commit()
IntegrityError: (IntegrityError) column lid is not unique u'INSERT INTO language (lid, language_name) VALUES (?, ?)' ('1', 'English')
How can I let the relationship know that it should ignore duplicates and not break the unique constraint for the Language table? Of course, I could insert each language separately and check if the entry already exists in the table beforehand, but then much of the benefit offered by sqlalchemy relationships is gone.
The SQLAlchemy wiki has a collection of examples, one of which is how you might check uniqueness of instances.
The examples are a bit convoluted though. Basically, create a classmethod get_unique as an alternate constructor, which will first check a session cache, then try a query for existing instances, then finally create a new instance. Then call Language.get_unique(id, name) instead of Language(id, name).
I've written a more detailed answer in response to OP's bounty on another question.
I would suggest to read Association Proxy: Simplifying Association Objects. In this case your code would translate into something like below:
# NEW: need this function to auto-generate the PK for newly created Language
# here using uuid, but could be any generator
def _newid():
import uuid
return str(uuid.uuid4())
def _language_find_or_create(language_name):
language = Language.query.filter_by(language_name=language_name).first()
return language or Language(language_name=language_name)
class User(Base):
__tablename__ = 'user'
uid = Column(String(80), primary_key=True)
languages = relationship('Language', lazy='dynamic',
secondary='user_language')
# proxy the 'language_name' attribute from the 'languages' relationship
langs = association_proxy('languages', 'language_name',
creator=_language_find_or_create,
)
class UserLanguage(Base):
__tablename__ = 'user_language'
__tableargs__ = (UniqueConstraint('uid', 'lid', name='user_language_ff'),)
id = Column(Integer, primary_key=True)
uid = Column(String(80), ForeignKey('user.uid'))
lid = Column(String(80), ForeignKey('language.lid'))
class Language(Base):
__tablename__ = 'language'
# NEW: added a *default* here; replace with your implementation
lid = Column(String(80), primary_key=True, default=_newid)
language_name = Column(String(30))
# test code
user = User(uid="user-1")
# NEW: add languages using association_proxy property
user.langs.append("English")
user.langs.append("Spanish")
session.add(user)
session.commit()
user2 = User(uid="user-2")
user2.langs.append("English") # this will not create a new Language row...
user2.langs.append("German")
session.add(user2)
session.commit()

Categories