One-to-Many, Many-to-Many Relationships

rang-dev·2020년 3월 17일
0

One-to-Many Relationship Setup

이제까지 만들어온 Todo app에 To-Do Lists를 추가하고 To-do model과 새로운 To-Do List model간에 relationship을 설정해본다.

  • To-Do List는 많은 To-Dos를 가질 수 있고, 각 To-Do는 하나의 To-Do List에 속하므로 두 모델 간에는 one to many relationship이 존재한다.
    • TodoList: Parent
    • Todo: Child

Creating the TodoList model and adding the foreign key to child Todo model

class 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)

Create and run a migration to upgrade the schema

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를 하게 되면 새로운 테이블인 todoliststodos에 새로 추가한 foreign key 컬럼을 발견한다.
  • migration file을 확인 한 후 flask db upgrade를 하게되면 NotNullViolation error가 발생한다.
    • 이유는 기존의 data에서는 추가될 새 컬럼(nullable=False)인 list_id의 값이 null이기 때문

NotNullViolation Error 해결방법

  1. 생성된 migration 파일에 가서 list_id의 컬럼 속성을 nullable=False에서 nullable=True로 바꾼다.
  2. app.py에서도 마찬가지로 nullable=True로 변경한다.
  3. flask db upgrade를 해주면 변경된 migration script가 성공적으로 실행된다.
  4. 생성된 todolists 테이블에서 todoslist_id가 참조할 값들을 생성한다.
  • (e.g. INSERT INTO todolists (name) VALUES ('Uncategorized');)
  1. todos 테이블에서 null값이 들어간 list_id를 해당되는 값으로 업데이트 해준다.
  • (e.g. UPDATE todos SET list_id = 1 WHERE list_id IS NULL;)
  1. app.py로 가서 list_idnullable을 다시 False로 설정한다.
  2. flask db migrate를 하면 해당 변화를 발견하고 migration이 생성되고, flask db upgrade를 해준다.

CRUD on a LIST of To-Dos

  • 좌측에 카테고리 리스트, 우측에 To-Dos를 배치하여 원하는 카테고리를 누르면 그 카테고리에 속한 To-Do들이 나타나도록 해야한다.

MVC Flow

  • View: 사용자가 리스트를 클릭할 수 있도록 하고 클릭하면 controller로 클릭한 리스트의 ID를 포함한 정보와 함께 request를 보내야한다.
  • Controller: Model에게 해당 리스트 ID에 속한 to-dos들을 가져오라고 알린다. 모든 item들을 불러오면 View가 업데이트된(선택한 리스트의 id에 속한) to-do item들을 나타내도록 알려준다.

Homepage 수정하기

기존에는 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들을 나타내도록 한다.

리스트에 새 카테고리 추가하기

  • In Terminal
# 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
  • SQlAlchemy는 parent가 add될때 관련된 children이 함께 자동으로 add될 수 있도록 해준다.
  • 또한 테이블 내에서 발생하는 모든 ordering details를 처리해준다.

Update View

좌측에 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()
)
  • 데이터베이스에서 todolists, todos의 데이터를 모두 가져온다.


Many-to-Many Relationships

  • One to many
    • ex) A class has many students
  • One to one
    • ex) A passport belongs to a person
  • Many to many
    • ex) A school teaches many subjects, and a subject is taught in many schools

Foreign Key를 설정할때 one to many와 one to one은 그 방법이 같다. 한가지 다른점은 one to many는 child 테이블에서 parent의 id가 여러번 포함될 수 있지만 one to one은 한번만 나온다는 것이다.

하지만 many to many relationship에서는 두개의 one-to-many관계를 조인할 세번째 테이블이 필요하다.

manytomany

중간에 있는 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들의 정보를 가져오면 된다.


Modeling a many-to-many relatuinship in SQLAlchemy ORM

Setting up the many-to-many relationship

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)

Example

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)
  • SQLAlchemy의 Table을 이용하여 sociation table을 선언할 수 있다.
  • db.relationshipsecondary 옵션을 통해 association table과 parent model을 매핑할 수 있다.
  • 또한 backref를 이용하여 주어진 product의 order를 호출할 수 있고 또한 두 model(oder, proudct)들을 매핑할 수 있도록 해준다. 따라서 product.orders, order.products가 가능하다.

테이블 작성해보기

  • Run Python in terminal
>>>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()
  • 테이블 확인(psql)
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개 행)
profile
지금 있는 곳에서, 내가 가진 것으로, 할 수 있는 일을 하기 🐢

0개의 댓글