• 用 Flask 来写个轻博客 (6) — (M)VC_models 的关系(one to many)


    Blog 项目源码:https://github.com/JmilkFan/JmilkFan-s-Blog

    目录

    前文列表

    用 Flask 来写个轻博客 (1) — 创建项目
    用 Flask 来写个轻博客 (2) — Hello World!
    用 Flask 来写个轻博客 (3) — (M)VC_连接 MySQL 和 SQLAlchemy
    用 Flask 来写个轻博客 (4) — (M)VC_创建数据模型和表
    用 Flask 来写个轻博客 (5) — (M)VC_SQLAlchemy 的 CRUD 详解

    扩展阅读

    SQLAlchemy_定义(一对一/一对多/多对多)关系

    前言

    models 中的关系能够映射成为关系型数据库表中的关系,models 中可以相互建立引用,使得相关联的数据能够很容易的一次性的从数据库中取出。

    一对多

    • 首先继续在 models.py 中建立一个 Post models 来表示 Blog 中的文章。而且一个用户 User 可以拥有多篇文章 Post,他们之间的关系是一对多。

    表示一对多的关系时,在子表类 Post 中需要通过 foreign key (外键)引用父表类 User。

    class Post(db.Model):
        """Represents Proected posts."""
    
        __tablename__ = 'posts'
        id = db.Column(db.String(45), primary_key=True)
        title = db.Column(db.String(255))
        text = db.Column(db.Text())
        publish_date = db.Column(db.DateTime)
        # Set the foreign key for Post
        user_id = db.Column(db.String(45), db.ForeignKey('users.id'))
    
        def __init__(self, title):
            self.title = title
    
        def __repr__(self):
            return "<Model Post `{}`>".format(self.title)

    其中 user_id 字段是 posts 表的外键,代表了数据库中的一种约束规则 —— 外键约束。这种规则强制规定了字段 user_id 的值必须同时存在于 User.id 列中。用来保证每一篇 post 都能对应找到一个 user,而且一个 user 能够对应多篇 posts。

    NOTE: 如果你没有在父表类指定 __tablename__ 属性,那么这一条语句我们应该这么写:

    user_id = db.Column(db.String(45), db.ForeignKey('User.id'))

    但是一般不建议写成这样,因为在 SQLAlchemy 初始化期间, User 对象可能还没有被创建出来,所以同时也建议在定义 models class 的时候应该指定 __tablename__ 属性。

    • 然后我们还需要在父表类 User 中定义出这种 one to many 的关系:
    class User(db.Model):
        """Represents Proected users."""
    
        # Set the name for table
        __tablename__ = 'users'
        id = db.Column(db.String(45), primary_key=True)
        username = db.Column(db.String(255))
        password = db.Column(db.String(255))
        # Establish contact with Post's ForeignKey: user_id
        posts = db.relationship(
            'Post',
            backref='users',
            lazy='dynamic')
    
        def __init__(self, id, username, password):
            self.id = id
            self.username = username
            self.password = password
    
        def __repr__(self):
            """Define the string format for instance of User."""
            return "<Model User `{}`>".format(self.username)
    • db.relationsformat(self.username)hip: 会在 SQLAlchemy 中创建一个虚拟的列,该列会与 Post.user_id (db.ForeignKey) 建立联系。这一切都交由 SQLAlchemy 自身管理。

    • backref:用于指定表之间的双向关系,如果在一对多的关系中建立双向的关系,这样的话在对方看来这就是一个多对一的关系。

    • lazy:指定 SQLAlchemy 加载关联对象的方式。

      • lazy=subquery: 会在加载 Post 对象后,将与 Post 相关联的对象全部加载,这样就可以减少 Query 的动作,也就是减少了对 DB 的 I/O 操作。但可能会返回大量不被使用的数据,会影响效率。
      • lazy=dynamic: 只有被使用时,对象才会被加载,并且返回式会进行过滤,如果现在或将来需要返回的数据量很大,建议使用这种方式。Post 就属于这种对象。

    再一次 sync db

    每一次新增了 models class,都需要导入到 manage.py 中,在通过 manager shell 来同步数据库。

    # import Flask Script object
    from flask.ext.script import Manager, Server
    import main
    import models
    
    # Init manager object via app object
    manager = Manager(main.app)
    
    # Create some new commands
    manager.add_command("server", Server())
    
    @manager.shell
    def make_shell_context():
        """Create a python CLI.
    
        return: Default import object
        type: `Dict`
        """
        return dict(app=main.app,
                    db=models.db,
                    User=models.User,
                    Post=models.Post)
    
    if __name__ == '__main__':
        manager.run()

    NOTE: 因为前面我们对原有的 users 表结构做了修改,所以我们需要将 users 表删除,再重新同步数据库。但需要注意,这是一种非常不推荐的方法,以后我们会介绍一种更加科学的增量同步数据库的方法。

    mysql> DROP TABLE users;
    • 重新同步数据库
    (env) [root@flask-dev JmilkFan-s-Blog]# python manage.py shell
    >>> db.create_all()
    >>> Post
    <class 'models.Post'>
    mysql> show tables;
    +------------------+
    | Tables_in_myblog |
    +------------------+
    | posts            |
    | users            |
    +------------------+
    2 rows in set (0.00 sec)
    
    mysql> desc posts
    ;
    +--------------+--------------+------+-----+---------+-------+
    | Field        | Type         | Null | Key | Default | Extra |
    +--------------+--------------+------+-----+---------+-------+
    | id           | varchar(45)  | NO   | PRI | NULL    |       |
    | title        | varchar(255) | YES  |     | NULL    |       |
    | text         | text         | YES  |     | NULL    |       |
    | publish_date | datetime     | YES  |     | NULL    |       |
    | user_id      | varchar(45)  | YES  | MUL | NULL    |       |
    +--------------+--------------+------+-----+---------+-------+
    5 rows in set (0.00 sec)

    How to use

    >>> from uuid import uuid4
    # 实例化一个 User 的对象
    >>> user = User(id=str(uuid4()), username='jmilkfan', password='fanguiju')
    # 写入一条 users 记录     
    >>> db.session.add(user)
    >>> db.session.commit()
    
    >>> user.posts
    <sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x22bc410>

    NOTE: 因为 user 的关联对象的加载方式为动态方式,所以 user.posts 会返回一个 Query 对象,需要调用 filter()/all()/first() 来获取实际需要被使用到的对象。
    反之,如果是子查询方式的话,就是直接将关联对象全部返回

    # 现在因为还没有添加 posts 的记录所以为空
    >>> user.posts.all()
    []
    
    # 实例化一个 Post 的对象
    >>> post_one = Post('First Post')
    # 主键值是非空的,必须指定一个,否则会报错
    >>> post_one.id = str(uuid4())
    # 指定该 post 是属于哪一个 user 的
    >>> post_one.user_id = user.id
    >>> db.session.add(post_one)
    >>> db.session.commit()
    
    >>> user.posts.all()
    [<Model Post `First Post`>]

    NOTE: 必须在 commit 了 post_one 对象之后,user 才能够通过关系来获取关联对象 posts。

    上面一个例子是为 user 添加一个 post,那么反过来能不能为一个 post 指定一个 user 呢?
    如果我们有使用到 backref 参数的话,那答案就是肯定的,这也是该参数所谓 双向 的含义。

    # 获取一个已经存在数据库中的记录 user 
    >>> user = db.session.query(User).first()
    >>> user.id
    u'ad7fd192-89d8-4b53-af96-fceb1f91070f'
    
    # 实例化一个 Post 的对象 post_second
    >>> post_second = Post('Second Post')
    # 必须为其设置主键值
    >>> post_second.id = str(uuid4())
    # 现在该 post_second 对象是没有关联到任何 user 的
    >>> post_second.users
    # 为 post_second 指定一个 user 对象
    >>> post_second.users = user

    NOTE:# 另一种建立联系的写法为 user.posts.append(post_second)
    因为 user.posts 本质上是一个列表,只要该列表中存在 post 那么 post 就是属于这个 user 的

    # 将 post_second 写入数据库
    >>> db.session.add(post_second)
    >>> db.session.commit()
    # 写入完成之后,user 才能够通过关系来访问到属于其下的 posts
    >>> user.posts.all()
    [<Model Post `Second Post`>, <Model Post `First Post`>]

    ERROR LOG

    InvalidRequestError: This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). Original exception was: (pymysql.err.InternalError) (1364, u"Field 'id' doesn't have a default value") [SQL: u'INSERT INTO posts (title, text, publish_date, user_id) VALUES (%(title)s, %(text)s, %(publish_date)s, %(user_id)s)'] [parameters: {'text': None, 'title': 'First Post', 'publish_date': None, 'user_id': '9ecae9b3-f4d2-4c8e-b033-616bb1642842'}]

    TS: 因为 commit 一个指定了 user_id 的 port 对象时,实际上数据库中还不存在被关联的 user 对象,导致报错。

  • 相关阅读:
    有效的括号
    数组
    复杂度分析
    技术派-epoll和IOCP之比较
    2020 University Rankings US News(美国)
    2020 University Rankings US News(亚洲)
    2020 University Rankings US News(中国)
    技术派-如果编译提示winnt.h(222):error C2146错误
    技术派-github常见的一些用法和缩写
    技术派-9个常用的代码托管平台
  • 原文地址:https://www.cnblogs.com/jmilkfan-fanguiju/p/10589873.html
Copyright © 2020-2023  润新知