이제까지 만들어온 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개 행)