[오늘의 배움] 035 SQLAlchemy, SQLite, Flask

이상민·2021년 1월 22일
0

[오늘의 배움]

목록 보기
38/70
post-thumbnail

1. SQLAlchemy 릴레이션십

1-1. 릴세이션십 양방향으로 설정하기

relationship.backref 또는 relationship.back_populates로 설정할 수 있다

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    addresses = relationship("Address", backref="user")

class Address(Base):
    __tablename__ = 'address'
    id = Column(Integer, primary_key=True)
    email = Column(String)
    user_id = Column(Integer, ForeignKey('user.id'))

모델을 위에 처럼 만들면 backref로 인해 User.address, Address.user로 참조할 수 있게된다. backref와 back_populates는 같은 역할을 하고 위 모델을 back_populates를 사용해 작성하면 아래와 같다.

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    addresses = relationship("Address", back_populates="user")

class Address(Base):
    __tablename__ = 'address'
    id = Column(Integer, primary_key=True)
    email = Column(String)
    user_id = Column(Integer, ForeignKey('user.id'))

    user = relationship("User", back_populates="addresses")

1-2. 1:M

부모를 참조하는 외부키를 자식 테이블에 둔다. 이후 relationship(backref)를 명시한다.

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    children = relationship("Child", backref="parent")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))

공식문서들에는 backref를 부모에 명시하는 걸로 나오지만 자식에 명시해도 상관없는 것 같다.

1-3. 1:1

스칼라 애트리뷰트를 가진 양방향 릴레이션십이다.

스칼라 애트리뷰트로 설정하려면 uselist=False 플래그를 사용해야한다. 아래 모델은 M:1 모델을 1:1로 바꾼 모습이다.

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    child_id = Column(Integer, ForeignKey('child.id'))
    child = relationship("Child", backref=backref("parent", uselist=False))
    
class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)

1-4. N:M

두 테이블 사이에 association 테이블을 추가한다. association 테이블은 양쪽의 외부키를 애트리뷰트로 갖는다.

relationship()에 secondary 플래그로 association 테이블임을 명시한다.

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,
        back_populates="parents")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)
    parents = relationship(
        "Parent",
        secondary=association_table,
        back_populates="children")

backref를 사용하면 아래와 같다

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,
                    backref="parents")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)


2. SQLite ALTER

SQLite는 Alter문을 지원하지 않아, batch 연산을 이용해야한다.

ALTER : 테이블 스키마를 수정할 수 있는 문법. SQLite는 지원하지않아 새로운 테이블을 만들고 기존 데이터를 옮기고 기존 테이블을 지우는 연산을 해야한다. Alembic은 이를 위해 batch 연산을 제공한다.

아래처럼 마이그레트를 동작하게 할 때 render_as_batch=True 플래그를 설정해 마이그레이션시 batch를 사용해 자동생성하도록 할 수 있다.

if app.config['SQLALCHEMY_DATABASE_URI'].startswith("sqlite"):
    migrate.init_app(app, db, render_as_batch=True)

init_app()은 플라스크 표준으로 익스텐션을 실행할 때 사용된다.


3. Flask 파일 업로드

3-1. 업로드 폼 만들기

<form action="{{ url_for('post.upload_post') }}" enctype="multipart/form-data" method="post">
    <input type="file" name="file" multiple>
    <input type="submit" value="업로드">
</form>
  • action : 폼을 전달할 라우트 함수
  • enctype : 데이터 인코딩 방법, multipart/form-data = 문자를 인코딩하지 않음. 파일/이미지 전송 시 사용
  • multiple : 여러 파일을 전송하려면 설정해야하는 플래그

3-2. 업로드 위치와 파일 형식 지정하기

# config.py
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'flatagram/static/img/post/')
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'gif', 'png'}

그리고 파일 이름을 받으면 허용된 파일 형식인지 확인해 True/False로 반환하는 함수를 작성한다

def allowed_file(filename):
    return '.' in filename and \
            filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']

3-3. 업로드 라우트 함수 만들기

@bp.route('/upload/', methods=('GET', 'POST'))
def upload_post():
    if request.method == 'POST':
        # check if post request has file part
        if 'file' not in request.files:
            flash('No file part')
            return redirect(url_for('post.upload_post'))
        file = request.files['file']
        # if user does not select file, browser also
        # submit an empty part without filename
        if file.filename == '':
            flash('No selected file')
            return redirect(url_for('post.upload_post'))
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename))
            return redirect(url_for('main.home'))
    return render_template('/posts/post_upload.html')
  • request.method가 get이면 업로드 템플릿 렌더링
  • request.files : MultiDict 객체
    i) 키은 <input type="file" name="">에서 name으로 정해진다.
    ii) 값은 Werkzeug FileStorage 객체이다.
  • 파일 이름이 없으면(선택하지 않았으면) 오류 반환
  • secure_filename : 유저가 파일 이름으로 서버에 파일을 조작할 수도 있으므로 사용해야한다.

https://flask.palletsprojects.com/en/1.1.x/patterns/fileuploads/

profile
편하게 읽기 좋은 단위의 포스트를 추구하는 개발자입니다

1개의 댓글

comment-user-thumbnail
2021년 12월 1일

감사합니다. 많은 도움이 되었습니다.

답글 달기