이제까지 만들어온 Todo app에 To-Do Lists를 추가하고 To-do model과 새로운 To-Do List model간에 relationship을 설정해본다.
TodoList model and adding the foreign key to child Todo modelclass TodoList(db.Model):
__tablename__='todolists'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(), nullable=False)
todos = db.relationship('Todo', backref='list', lazy=True)
class Todo(db.Model):
__tablename__ = 'todos'
id = db.Column(db.Integer, primary_key=True)
description = db.Column(db.String(), nullable=False)
completed = db.Column(db.Boolean, default=False)
list_id = db.Column(db.Integer, db.ForeignKey('todolists.id'), nullable=False)
C:\Users\gusfk\Desktop\class-demos\todoapp>flask db migrate
C:\ProgramData\Anaconda3\lib\site-packages\flask_sqlalchemy\__init__.py:835: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future. Set it to True or False to suppress this warning.
'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'todolists'
INFO [alembic.ddl.postgresql] Detected sequence named 'todos_id_seq' as owned by integer column 'todos(id)', assuming SERIAL and omitting
INFO [alembic.autogenerate.compare] Detected added column 'todos.list_id'
INFO [alembic.autogenerate.compare] Detected added foreign key (list_id)(id) on table todos
Generating C:\Users\gusfk\Desktop\class-demos\todoapp\migrations\versions\dde729cffcaf_.py ... done
flask db migrage를 하게 되면 새로운 테이블인 todolists와 todos에 새로 추가한 foreign key 컬럼을 발견한다.flask db upgrade를 하게되면 NotNullViolation error가 발생한다.list_id의 값이 null이기 때문list_id의 컬럼 속성을 nullable=False에서 nullable=True로 바꾼다.app.py에서도 마찬가지로 nullable=True로 변경한다.flask db upgrade를 해주면 변경된 migration script가 성공적으로 실행된다.todolists 테이블에서 todos의 list_id가 참조할 값들을 생성한다.INSERT INTO todolists (name) VALUES ('Uncategorized');)todos 테이블에서 null값이 들어간 list_id를 해당되는 값으로 업데이트 해준다.UPDATE todos SET list_id = 1 WHERE list_id IS NULL;)app.py로 가서 list_id의 nullable을 다시 False로 설정한다.flask db migrate를 하면 해당 변화를 발견하고 migration이 생성되고, flask db upgrade를 해준다.기존에는 homepage에서 데이터베이스에 있는 모든 todo items이 나열되도록 했었다. 하지만 이제는 특정 list에 속한 item들만을 나타내고자한다.
@app.route('/lists/<list_id>')
def get_list_todos(list_id):
return render_template('index.html', data = Todo.query.filter_by(list_id=list_id).order_by('id).all())
@app.route('/') #homepage
def index():
return redirect(url_for('get_list_todos', list_id=1))
## 홈페이지에서 get_list_todos route handler에 redirect하여 list_id=1에 속하는 todo item들을 나타내도록 한다.
# move to your project directory and run python
>>> from app import db, TodoList, Todo
>>> list = TodoList(name='Urgent') # 새 카테고리 추가
>>> todo = Todo(description='Urgent todo 1')
>>> todo2 = Todo(description='Urgent todo 2')
>>> todo3 = Todo(description='Urgent todo 3')
>>> todo.list = list # todo와 연관된 parent object를 연결해주어야 하므로 앞에서 선언한 list로 설정한다.
>>> todo2.list = list
>>> todo3.list = list
>>> db.session.add(list) # list만 add하면 관련된 children은 자동으로 추가 된다. cascade option의 기본 설정이기 때문이다.
>>> db.session.commit
좌측에 list, 우측에 todos가 나타나도록 view를 업데이트한다.
# app.py
@app.route('/lists/<list_id>')
def get_list_todos(list_id):
return render_template('index.html',
lists=TodoList.query.all(),
active_list=TodoList.query.get(list_id), # view의 todos 부분에서 현재 어떤 카테고리인지 알 수 있도록 표시하기 위함
todos=Todo.query.filter_by(list_id=list_id).order_by('id').all()
)
Foreign Key를 설정할때 one to many와 one to one은 그 방법이 같다. 한가지 다른점은 one to many는 child 테이블에서 parent의 id가 여러번 포함될 수 있지만 one to one은 한번만 나온다는 것이다.
하지만 many to many relationship에서는 두개의 one-to-many관계를 조인할 세번째 테이블이 필요하다.

중간에 있는 association table은 many to many 관계에 필요한 다른 foreign table들을 연결하는 여러개의 foreign key를 가진다.
예를들어 위와 같이 한 order에서 어떤 products를 주문했는지 확인하려면 order_items에서 order_id를 확인한 후 그에 속하는 product_id를 확인하여 정보를 가져오면 된다. 반대로 한 product를 주문한 모든 orders를 확인하려면 order_items에서 product_id를 확인한 후 그에 맞는 order_id들의 정보를 가져오면 된다.
association_table = Table('association', Base.metadata,
Column('left_id', Integer, ForeignKey('left.id')),
Column('right_id', Integer, ForeignKey('right.id'))
)
class Parent(Base):
__tablename__ = 'left'
id = Column(Integer, primary_key=True)
children = relationship("Child", secondary=association_table)
class Child(Base):
__tablename__ = 'right'
id = Column(Integer, primary_key=True)
order_items = db.Table('order_items', # name of association table
db.Column('order_id', db.Integer, db.ForeignKey('order.id'), primary_key=True),
db.Column('product_id', db.Integer, db.ForeignKey('product.id'), primary_key=True)
)
class Order(db.Model):
id = db.Column(db.Integer, primary_key=True)
status = db.Column(db.String(), nullable=False)
products = db.relationship('Product', secondary=order_items,
backref=db.backref('orders', lazy=True))
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(), nullable=False)
Table을 이용하여 sociation table을 선언할 수 있다.db.relationship의 secondary 옵션을 통해 association table과 parent model을 매핑할 수 있다.backref를 이용하여 주어진 product의 order를 호출할 수 있고 또한 두 model(oder, proudct)들을 매핑할 수 있도록 해준다. 따라서 product.orders, order.products가 가능하다.>>>from app import db, Order, Product
>>>db.create_all()
>>>order = Order(status='ready')
>>>product = Product(name='Great widget')
>>>order.products = [product] # 많은 product가 올 수도 있다.
>>>product.orders = [order] # 많은 order가 올 수도 있다.
>>>db.session.add(order)
>>>db.session.commit()
todoapp=# \dt
릴레이션(relation) 목록
스키마 | 이름 | 종류 | 소유주
--------+-----------------+--------+----------
public | alembic_version | 테이블 | postgres
public | order | 테이블 | postgres
public | order_items | 테이블 | postgres
public | product | 테이블 | postgres
public | todolists | 테이블 | postgres
public | todos | 테이블 | postgres
(6개 행)
todoapp=# select * from "order"; # 그냥 order로 쓰면 order_by로 착각하여 error 발생
id | status
----+--------
1 | ready
(1개 행)
todoapp=# select * from product;
id | name
----+--------------
1 | Great widget
(1개 행)
todoapp=# select * from order_items;
order_id | product_id
----------+------------
1 | 1
(1개 행)