1. 自關聯
class Comment(db.Model): __tablename__ = 'albumy_comment' id = db.Column(db.Integer, primary_key=True) body = db.Column(db.Text) timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True) flag = db.Column(db.Integer, default=0) replied_id = db.Column(db.Integer, db.ForeignKey('albumy_comment.id')) user_id = db.Column(db.Integer, db.ForeignKey('albumy_user.id')) photo_id = db.Column(db.Integer, db.ForeignKey('albumy_photo.id')) photo = db.relationship('Photo', back_populates='comments') user = db.relationship('User', back_populates='comments') replies = db.relationship('Comment', back_populates='replied', cascade='all') # 一 我下面所有給我的評論 replied = db.relationship('Comment', back_populates='replies', remote_side=[id]) # 多 我對哪條評論進行的評論
以評論表為例,評論下又可以有針對該評論的回復,因此在表中增加 replied_id 外鍵字段,指向該表的主鍵id。
在設置關系屬性時,需要再多的一方設置remote_side=[id]。
2. 第三張表中的多個外鍵字段執行同一個表中的同一個字段
class Follow(db.Model): __tablename__ = 'albumy_follow' follower_id = db.Column(db.Integer, db.ForeignKey('albumy_user.id'), primary_key=True) followed_id = db.Column(db.Integer, db.ForeignKey('albumy_user.id'), primary_key=True) timestamp = db.Column(db.DateTime, default=datetime.utcnow) follower = db.relationship('User', foreign_keys=[follower_id], back_populates='following', lazy='joined') followed = db.relationship('User', foreign_keys=[followed_id], back_populates='followers', lazy='joined') class User(UserMixin, db.Model): __tablename__ = 'albumy_user' id = db.Column(db.INTEGER, primary_key=True) # 資料 username = db.Column(db.String(20), unique=True, index=True) email = db.Column(db.String(254), unique=True, index=True) password_hash = db.Column(db.String(128)) name = db.Column(db.String(30)) website = db.Column(db.String(255)) bio = db.Column(db.String(120)) location = db.Column(db.String(50)) member_since = db.Column(db.DateTime, default=datetime.utcnow) avatar_s = db.Column(db.String(64)) avatar_m = db.Column(db.String(64)) avatar_l = db.Column(db.String(64)) avatar_raw = db.Column(db.String(64)) receive_comment_notification = db.Column(db.Boolean, default=True) receive_follow_notification = db.Column(db.Boolean, default=True) receive_collect_notification = db.Column(db.Boolean, default=True) show_collections = db.Column(db.Boolean, default=True) role_id = db.Column(db.Integer, db.ForeignKey('albumy_role.id')) role = db.relationship('Role', back_populates='users') photos = db.relationship('Photo', back_populates='user', cascade='all') collections = db.relationship('Collect', back_populates='collector', cascade='all') # 如:都收藏了那些圖片 comments = db.relationship('Comment', back_populates='user', cascade='all') following = db.relationship('Follow', foreign_keys=[Follow.follower_id], back_populates='follower', lazy='dynamic', cascade='all') # 都關注了哪些用戶 followers = db.relationship('Follow', foreign_keys=[Follow.followed_id], back_populates='followed', lazy='dynamic', cascade='all') # 都被哪些用戶 notifications = db.relationship('Notification', back_populates='receiver', cascade='all') # 用戶狀態 confirmed = db.Column(db.Boolean, default=False) locked = db.Column(db.Boolean, default=False) active = db.Column(db.Boolean, default=True)
以 Follow表(關注表)與user表為例,follow表中記錄着關注者id 與被關注着id,這兩個外鍵字段都指向user表中的id。
因為在Follow模型中,兩個字段定義的外鍵是指向同一個表的同一個字段(user.id)的。而當我們需要在Follow模型上建立反向屬性時,SQLAlchemy沒法知道哪個外鍵對應哪個反向屬性,所以我們需要在關系函數中使用foreign_keys參數來明確對應的字段。
Follow表:
follower = db.relationship('User', foreign_keys=[follower_id], back_populates='following', lazy='joined') followed = db.relationship('User', foreign_keys=[followed_id], back_populates='followers', lazy='joined')
User表:
following = db.relationship('Follow', foreign_keys=[Follow.follower_id], back_populates='follower', lazy='dynamic', cascade='all') # 都關注了哪些用戶 followers = db.relationship('Follow', foreign_keys=[Follow.followed_id], back_populates='followed', lazy='dynamic', cascade='all') # 都被哪些用戶
3. 使用關聯表表示多對多關系
class Role(db.Model): __tablename__ = 'albumy_role' id = db.Column(db.Integer, primary_key=True) code = db.Column(db.String(21), unique=True) name = db.Column(db.String(21), unique=True) desc = db.Column(db.String(64), nullable=True) users = db.relationship('User', back_populates='role') permissions = db.relationship('Permission', secondary='albumy_roles_permissions', back_populates='roles') class Permission(db.Model): __tablename__ = 'albumy_permission' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(21), unique=True) desc = db.Column(db.String(64), nullable=True) roles = db.relationship('Role', secondary='albumy_roles_permissions', back_populates='permissions') roles_permissions = db.Table( 'albumy_roles_permissions', db.Column('id', db.Integer, primary_key=True), db.Column('role_id', db.Integer, db.ForeignKey('albumy_role.id')), db.Column('permission_id', db.Integer, db.ForeignKey('albumy_permission.id')) )
1. 使用關聯表很方便,唯一的缺點是只能用來表示關系,不能用來存儲數據。
2. 當使用關聯表時,SQLAlchemy會幫助我們操作關系,所以對關系某一側調用關系屬性會直接返回關系另一側的對應記錄。但是使用關聯模型時,我們則需要手動操作關系。
4. 使用關聯模型表示多對多關系
class Photo(db.Model): __tablename__ = 'albumy_photo' id = db.Column(db.Integer, primary_key=True) description = db.Column(db.String(500)) filename = db.Column(db.String(64)) filename_s = db.Column(db.String(64)) filename_m = db.Column(db.String(64)) flag = db.Column(db.Integer, default=0) # 舉報次數 can_comment = db.Column(db.Boolean, default=True) timestamp = db.Column(db.DateTime, default=datetime.utcnow) user_id = db.Column(db.Integer, db.ForeignKey('albumy_user.id')) user = db.relationship(User, back_populates='photos') tags = db.relationship('Tag', back_populates='photos', secondary='albumy_photos_tags', cascade='all') collectors = db.relationship('Collect', back_populates='collected', cascade='all') # 如:被收藏的數量 comments = db.relationship('Comment', back_populates='photo', cascade='all')
# 關聯模型 class Collect(db.Model): __tablename__ = 'albumy_collect' # id = db.Column(db.Integer, primary_key=True) collector_id = db.Column(db.Integer, db.ForeignKey('albumy_user.id'), primary_key=True) # 收藏者id collected_id = db.Column(db.Integer, db.ForeignKey('albumy_photo.id'), primary_key=True) # 被收藏圖片id timestamp = db.Column(db.DateTime, default=datetime.utcnow) collector = db.relationship('User', back_populates='collections', lazy='joined') collected = db.relationship('Photo', back_populates='collectors', lazy='joined') # __table_args__ = ( # db.UniqueConstraint('collector_id', 'collected_id', name='u_collector_id_collected_id'), # # db.Index('ix_user_post_user_id_insert_time', 'user_id', 'insert_time'), # )
當使用關聯表時,SQLAlchemy會幫助我們操作關系,所以對關系某一側調用關系屬性會直接返回關系另一側的對應記錄。但是使用關聯模型時,我們則需要手動操作關系。具體的表現是,我們在Photo和User模型中定義的關系屬性返回的不再是關系另一側的記錄,而是存儲對應關系的中間人——Collect記錄。在Collect記錄中添加的標量關系屬性collector和collected,分別表示收藏者和被收藏圖片,指向對應的User和Photo記錄,我們需要進一步調用這兩個關系屬性,才可以獲取關系另一側的記錄。

from flask_login import UserMixin from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from flask_avatars import Identicon from flask import current_app import os from albumy.extensions import db class Follow(db.Model): __tablename__ = 'albumy_follow' follower_id = db.Column(db.Integer, db.ForeignKey('albumy_user.id'), primary_key=True) followed_id = db.Column(db.Integer, db.ForeignKey('albumy_user.id'), primary_key=True) timestamp = db.Column(db.DateTime, default=datetime.utcnow) follower = db.relationship('User', foreign_keys=[follower_id], back_populates='following', lazy='joined') followed = db.relationship('User', foreign_keys=[followed_id], back_populates='followers', lazy='joined') class User(UserMixin, db.Model): __tablename__ = 'albumy_user' id = db.Column(db.INTEGER, primary_key=True) # 資料 username = db.Column(db.String(20), unique=True, index=True) email = db.Column(db.String(254), unique=True, index=True) password_hash = db.Column(db.String(128)) name = db.Column(db.String(30)) website = db.Column(db.String(255)) bio = db.Column(db.String(120)) location = db.Column(db.String(50)) member_since = db.Column(db.DateTime, default=datetime.utcnow) avatar_s = db.Column(db.String(64)) avatar_m = db.Column(db.String(64)) avatar_l = db.Column(db.String(64)) avatar_raw = db.Column(db.String(64)) receive_comment_notification = db.Column(db.Boolean, default=True) receive_follow_notification = db.Column(db.Boolean, default=True) receive_collect_notification = db.Column(db.Boolean, default=True) show_collections = db.Column(db.Boolean, default=True) role_id = db.Column(db.Integer, db.ForeignKey('albumy_role.id')) role = db.relationship('Role', back_populates='users') photos = db.relationship('Photo', back_populates='user', cascade='all') collections = db.relationship('Collect', back_populates='collector', cascade='all') # 如:都收藏了那些圖片 comments = db.relationship('Comment', back_populates='user', cascade='all') following = db.relationship('Follow', foreign_keys=[Follow.follower_id], back_populates='follower', lazy='dynamic', cascade='all') # 都關注了哪些用戶 followers = db.relationship('Follow', foreign_keys=[Follow.followed_id], back_populates='followed', lazy='dynamic', cascade='all') # 都被哪些用戶 notifications = db.relationship('Notification', back_populates='receiver', cascade='all') # 用戶狀態 confirmed = db.Column(db.Boolean, default=False) locked = db.Column(db.Boolean, default=False) active = db.Column(db.Boolean, default=True) def __init__(self, **kwargs): super(User, self).__init__(**kwargs) self.generate_avatar() self.set_role() self.follow(self) def generate_avatar(self): """生成用戶頭像""" avatar = Identicon() filenames = avatar.generate(text=self.username) self.avatar_s = filenames[0] self.avatar_m = filenames[1] self.avatar_l = filenames[2] db.session.commit() def set_role(self): """為用戶設置角色,默認為user""" if self.role is None: role = Role.query.filter_by(code='user').first() self.role = role db.session.commit() def set_password(self, pwd): """設置加密密碼""" self.password_hash = generate_password_hash(pwd) def check_password(self, pwd): """檢驗密碼正確性""" return check_password_hash(self.password_hash, pwd) def is_admin(self): """判斷用戶是否具有管理員的角色""" return self.role.code == 'administrator' def can(self, permission_name): """判斷用戶是否具有某個權限""" permission = Permission.query.filter_by(name=permission_name).first() return permission is not None and self.role is not None and permission in self.role.permissions def collect(self, photo): """ 收藏圖片 :param photo: 圖片對象 :return: """ if not self.is_collecting(photo): collect = Collect(collector=self, collected=photo) db.session.add(collect) db.session.commit() def uncollect(self, photo): """ 取消圖片收藏 :param photo: 圖片對象 :return: """ collect = Collect.query.with_parent(self).filter_by(collected_id=photo.id).first() if collect: db.session.delete(collect) db.session.commit() def is_collecting(self, photo): """ 是否收藏圖片 :return: 圖片對象 """ collect = Collect.query.with_parent(self).filter_by(collected_id=photo.id).first() if collect: return True else: return False def follow(self, user): """ 關注用戶 :param user: user對象 :return: """ if not self.is_following(user): follow = Follow(follower=self, followed=user) db.session.add(follow) db.session.commit() def unfollow(self, user): """ 取消關注 :param user: user對象 :return: """ follow = self.following.filter_by(followed_id=user.id).first() if follow: db.session.delete(follow) db.session.commit() def is_following(self, user): """ 是否關注用戶 :param user: user對象 :return: """ if user.id is None: return False return self.following.filter_by(followed_id=user.id).first() is not None def is_followed_by(self, user): """ 用戶是否被關注 :param user: user對象 :return: """ return self.followers.filter_by(follower_id=user.id).first() is not None def lock(self): self.locked = True self.role = Role.query.filter_by(name='Locked').first() db.session.commit() def unlock(self): self.locked = False self.role = Role.query.filter_by(name='User').first() db.session.commit() @property def is_active(self): return self.active def block(self): self.active = False db.session.commit() def unblock(self): self.active = True db.session.commit() @db.event.listens_for(User, 'after_delete', named=True) def delete_avatars(**kwargs): target = kwargs['target'] for filename in [target.avatar_s, target.avatar_m, target.avatar_l, target.avatar_raw]: if filename is not None: # avatar_raw may be None path = os.path.join(current_app.config['AVATARS_SAVE_PATH'], filename) if os.path.exists(path): # not every filename map a unique file os.remove(path) class Role(db.Model): __tablename__ = 'albumy_role' id = db.Column(db.Integer, primary_key=True) code = db.Column(db.String(21), unique=True) name = db.Column(db.String(21), unique=True) desc = db.Column(db.String(64), nullable=True) users = db.relationship('User', back_populates='role') permissions = db.relationship('Permission', secondary='albumy_roles_permissions', back_populates='roles') class Permission(db.Model): __tablename__ = 'albumy_permission' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(21), unique=True) desc = db.Column(db.String(64), nullable=True) roles = db.relationship('Role', secondary='albumy_roles_permissions', back_populates='permissions') roles_permissions = db.Table( 'albumy_roles_permissions', db.Column('id', db.Integer, primary_key=True), db.Column('role_id', db.Integer, db.ForeignKey('albumy_role.id')), db.Column('permission_id', db.Integer, db.ForeignKey('albumy_permission.id')) ) class Photo(db.Model): __tablename__ = 'albumy_photo' id = db.Column(db.Integer, primary_key=True) description = db.Column(db.String(500)) filename = db.Column(db.String(64)) filename_s = db.Column(db.String(64)) filename_m = db.Column(db.String(64)) flag = db.Column(db.Integer, default=0) # 舉報次數 can_comment = db.Column(db.Boolean, default=True) timestamp = db.Column(db.DateTime, default=datetime.utcnow) user_id = db.Column(db.Integer, db.ForeignKey('albumy_user.id')) user = db.relationship(User, back_populates='photos') tags = db.relationship('Tag', back_populates='photos', secondary='albumy_photos_tags', cascade='all') collectors = db.relationship('Collect', back_populates='collected', cascade='all') # 如:被收藏的數量 comments = db.relationship('Comment', back_populates='photo', cascade='all') # 為Photo創建一個數據庫事件監聽函數 @db.event.listens_for(Photo, 'after_delete', named=True) def delete_photo_file(**kwargs): """刪除photo對象時, 刪除對應的文件""" """ kwargs = {'connection': <sqlalchemy.engine.base.Connection object at 0x0000025B138A7978>, 'mapper': <Mapper at 0x25b134fb2b0; Photo>, 'target': <Photo 8> } 如果不加named=True, 需要傳三個位置參數 """ target = kwargs['target'] # <Photo 8> for filename in [target.filename, target.filename_s, target.filename_m]: if filename is not None: path = os.path.join(current_app.config['ALBUMY_UPLOAD_PATH'], filename) if os.path.exists(path): os.remove(path) class Tag(db.Model): __tablename__ = 'albumy_tag' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(12)) desc = db.Column(db.String(64)) photos = db.relationship(Photo, back_populates='tags', secondary='albumy_photos_tags') photos_tags = db.Table( 'albumy_photos_tags', db.Column('id', db.Integer, primary_key=True), db.Column('photo_id', db.Integer, db.ForeignKey('albumy_photo.id')), db.Column('tag_id', db.Integer, db.ForeignKey('albumy_tag.id')), ) # 關聯模型 class Collect(db.Model): __tablename__ = 'albumy_collect' # id = db.Column(db.Integer, primary_key=True) collector_id = db.Column(db.Integer, db.ForeignKey('albumy_user.id'), primary_key=True) # 收藏者id collected_id = db.Column(db.Integer, db.ForeignKey('albumy_photo.id'), primary_key=True) # 被收藏圖片id timestamp = db.Column(db.DateTime, default=datetime.utcnow) collector = db.relationship('User', back_populates='collections', lazy='joined') collected = db.relationship('Photo', back_populates='collectors', lazy='joined') # __table_args__ = ( # db.UniqueConstraint('collector_id', 'collected_id', name='u_collector_id_collected_id'), # # db.Index('ix_user_post_user_id_insert_time', 'user_id', 'insert_time'), # ) class Comment(db.Model): __tablename__ = 'albumy_comment' id = db.Column(db.Integer, primary_key=True) body = db.Column(db.Text) timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True) flag = db.Column(db.Integer, default=0) replied_id = db.Column(db.Integer, db.ForeignKey('albumy_comment.id')) user_id = db.Column(db.Integer, db.ForeignKey('albumy_user.id')) photo_id = db.Column(db.Integer, db.ForeignKey('albumy_photo.id')) photo = db.relationship('Photo', back_populates='comments') user = db.relationship('User', back_populates='comments') replies = db.relationship('Comment', back_populates='replied', cascade='all') # 一 replied = db.relationship('Comment', back_populates='replies', remote_side=[id]) # 多 class Notification(db.Model): id = db.Column(db.Integer, primary_key=True) message = db.Column(db.Text) is_read = db.Column(db.Boolean, default=False) timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True) receiver_id = db.Column(db.Integer, db.ForeignKey('albumy_user.id')) receiver = db.relationship(User, back_populates='notifications')