TIL- Flask, flask-migrate, sqlalchemy, alembic ORM, sqlite3

kyoungyeon·2024년 9월 23일
0

TIL

목록 보기
118/122

Status

  • 행복합니다
  • 내부에서만 돌던 err-log가 밖으로 풀렸거든
    • 아 완전 속시원하다는 느낌
  • 지난 한달간 속으로 앓던 분노의 기록을 영향력 있는 외부인들에게 정량적 평가 및 증거를 가지고 release 하고 update 시켰다. 하하

    leaking이란건 정말 아름다운 거군요..

  • 단 한 번의 칼춤으로 팀장의 action 이 단번에 바뀜 ^^
    • 역시 너는 존나게 계산 빠른 친구구나..

쓰기 싫은 수 였는데 결국 이럴줄 알았지.


솔직히 섭섭합니다

지금까지 제가 한 말을 무시했다는게 맞다는게 증명된 거니까요.

  • 제가 성별이라 프로젝트 생산성에 논의하는 대화의 값어치가 없던가요?
  • 멘토님은 잘 보여야 해서 들을만한 가치가 있고?
  • 그 와중에 협회 매니저 말은 또 무섭냐?

팀장님.. 너 생각하는게 거기서 거기던데 제발 사회물 먹은 짬바 좀 작작 무시해...

Action

기록을 위한 기록 Error Handling 하며 알게된 flask 지식 update

DB

  • sqlite3
    • 파일처럼 db 만 있으면 된다 (mysql 처럼 server, client 둘 다 필요 하지 않음)
    • 명확한 설계 원칙을 따른다
      • refining your filtering logic to use a single, unique attribute
      • complex data types such as lists and dictionaries goes beyond
      • requiring special handling techni ques like serialization, decomposition into multiple tables
      • building robust and maintainable applications
    • JSON serialization/deserialization 위 두문제 때문에 JSON CRUD에서 애를 먹음
      • preload ( crud시) , post dump (db에 commit 될때) 부분을 schema에서 처리도 해줘야 했음
      • python module 이 돌때 모든 module 과 해당 파일을 다 import 하기 때문에 생기는 에러라고 한다.
      • validates_schema( validation error -
    • 위 특징떄문에 complex data를 처리하지 못하는 단점이 되기도 한다.

Factory Pattern

  • 이번에 쓴게 이건줄 몰랐다.
  • 단순히 한 페이지가 완성이 되면... 다른 페이지가 고장나고
  • 다른페이지가 고장나니 원래 잘 돌아가던 페이지도 고치라고 특급 사수 CLAUDE 님이 그러셨는데 진짜 미쳐 돌아가는줄 알았음
  • 때문에 서로 component 화 해서 최대한 영향이 덜가는 쪽으로 생각하게 되었다,.
  • validation, circular Error을 어떻게든 줄이고, app 이 실행되는 위치 정리가 필요했음
  • 매번 필요할때마다 선언하다보니 코드가 스파게티가 되서.. 결국 삭제하고 또 삭제하다보니
  • 부모 class(init)에서 인터페이스처럼 받아 쓰려고 노력을 했음
  • 특히 flask 를 run 하면서 main 함수는 패턴을 최대한 고정한 상태로 최대한 덜 건들려고 가져다 쓰는 형식을 별도로 관리하고자 했음
  • 그러다 보니 패턴이 만들어졌다.
 project/
│
├── utils/
│   ├── __init__.py
│   ├── api_error_handlers.py
│   ├── constants.py
│   ├── email_utils.py
│   └── logger.py
│   └── string_utils.py
│   └── db_error_handlers.py
├── migrations/
├── models/
├── api/
├── static/
├── script/
├── db/
├── api/
│   ├──services/
│   ├──routers/
├── utils/
├── extensions.py
├── init.py
├── app.py
└── config.py
  • Benefits of Application Factory Pattern:

    • Avoids Circular Imports: By initializing extensions within the factory and importing modules inside functions or routes, you prevent modules from importing each other at the top level.

    • Flexibility: Allows creating multiple instances of the app with different configurations, useful for testing and different environments.

    • Better Organization: Encourages modular code organization, making it easier to manage large projects.

Flask-migrate

  • 특이점: alembic 버전관리를 통해 migration 도 처리하고 schema도 처리해줌
  • 코드가 간결해지고, db migrate 할때 매우 쉬움 (db migrate, upgrade)
    • 단 data를 이젠 수동으로 못넣고 모듈화 시켜서 넣어야함 ㅜㅠ
    • 즉 초기 dummy data 세팅이 정말 지루하고 밤샘작업을 해야했음
    • 그리고 좀만 data 형식이나 db와 schema가 맞지 않으면... db를 날리는게 그냥 답 이였음
      - 물론 alembic 버전관리 파일이 pattern 화 되어있어서 몇번 보면 눈에 익지만..
      • 예 : fk_db_db키 , constrain key 가 NULL 인 경우 에러가 잦았음
      • 사유 : marshmallow가 읽지를 못함/ sqlalchemy가 인식을못함/ sqlite3에 이미 alembic_version db가 있음/ 임시로 만든 db가 있으니 삭제해야함 등등등
      - 어떤 메소드는 인식하고/ 어떤 메소드는 인식못하고 이걸 분간하는게 쉽지 않음.
       - sql문 안쓰고 싶어서 쓴 건데 migrate 도중 실패시 결국 sql문  써야한다는 모순이 있음
       - alembic 문법 쓰느니 sqlite3에서 drop 쓰는게 훨씬 빠르던데요?
       - 데이터 양에 비해 갈수록 너무 과한 설정이라는  생각이.. 
  • 규모 큰 프로젝트에 data가 많아질수록 관리에 용이하다고 하지만 글쎄?
    • 매번 schema에 맞는 data를 load하기 위해 코드 리팩토링하는 공수...
      • 잘 작동하게 만들어도, 결국은 기존의 데이터의 전체/ 일부 가 날아가는 리스크가 너무 컸음.
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db = SQLAlchemy(app)
migrate = Migrate(app, db)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))

# 마이그레이션 초기화 (처음 한 번만)
$ flask db init

# 마이그레이션 스크립트 생성
$ flask db migrate -m "Create users table"

# 마이그레이션 적용
$ flask db upgrade

SQLALchmey

  • 장점 : sql 문을 쓰지 않고 python 을이용해 객체지향적으로 개발가능
# before
app.route('/update_user', methods=['POST'])
def update_user():
    data = request.json
    user_id = data['id']
    favorite_numbers = json.dumps(data['favorite_numbers'])  # 리스트를 문자열로 변환
    conn = sqlite3.connect('database.db')
    cursor = conn.cursor()
    cursor.execute("UPDATE users SET favorite_numbers=? WHERE id=?", (favorite_numbers, user_id))
    conn.commit()
    conn.close()
    return jsonify({"message": "User updated successfully"})
  • sqlalchemy만 사용
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))
    favorite_numbers = db.Column(MutableList.as_mutable(JSON))
    
# 데이터베이스 초기화 (애플리케이션 시작 시 한 번만 실행)
db.create_all()
@app.route('/user/<int:user_id>', methods=['GET'])
def get_user_with_list(user_id):
    user = User.query.get(user_id)
    if user:
        user_dict = {
            "id": user.id,
            "name": user.name,
            "favorite_numbers": user.favorite_numbers  # SQLAlchemy가 자동으로 JSON을 리스트로 변환
        }
        return jsonify(user_dict)
    else:
        return jsonify({"error": "User not found"}), 404
        
@app.route('/user/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    user = User.query.get(user_id)
    if not user:
        return jsonify({"error": "User not found"}), 404

    data = request.json
    user.name = data.get('name', user.name)
    user.favorite_numbers = data.get('favorite_numbers', user.favorite_numbers)
    
    db.session.commit()
    return jsonify({"message": "User updated successfully", "user": {
        "id": user.id,
        "name": user.name,
        "favorite_numbers": user.favorite_numbers
    }})
@app.route('/user', methods=['POST'])
def create_user():
    data = request.json
    new_user = User(name=data['name'], favorite_numbers=data['favorite_numbers'])
    db.session.add(new_user)
    db.session.commit()
    return jsonify({
        "message": "User created successfully",
        "user": {
            "id": new_user.id,
            "name": new_user.name,
            "favorite_numbers": new_user.favorite_numbers
        }
    }), 201

if __name__ == '__main__':
    app.run(debug=True)
  • flask_migrate 과 sqlalchemy 동시사용
# after
@app.route('/user', methods=['POST'])
def create_user():
    data = request.json
    new_user = User(name=data['name'], favorite_numbers=data['favorite_numbers'])
    db.session.add(new_user)
    db.session.commit()
    return jsonify({
        "id": new_user.id,
        "name": new_user.name,
        "favorite_numbers": new_user.favorite_numbers
    }), 201
  • 세팅이 복잡해지면 코드가 간결화되어지는 장점은 있었음

    • 세팅이 겹겹이 되다 보니 도대체 어디서? 뭐 때문에? 에러가 나는지 찾기가 어렵다는 단점도 있었음
      • 내가 잘 못찾은 탓이지 머..

MarshMallow

  • 클라스와 객체를 별도로 구분해서 사용함
  • 이유는 처음엔 멋모르고 사용했는데 (영원한 사수 claude AI 님께서 사용하라 하심 )
  • 나중에 api 호출이나 service 메소드 만들때 편하긴 했음
# 모델 클래스 정의
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

    def __init__(self, name, email):
        self.name = name
        self.email = email

# Marshmallow 스키마 정의
class UserSchema(Schema):
    id = fields.Int(dump_only=True)
    name = fields.Str(required=True)
    email = fields.Email(required=True)

    @post_load
    def make_user(self, data, **kwargs):
        return User(**data)

# 스키마 인스턴스 생성
user_schema = UserSchema()
users_schema = UserSchema(many=True)
# RESTful API 엔드포인트
@app.route('/users', methods=['POST'])
def create_user():
    json_data = request.get_json()
    if not json_data:
        return jsonify({"message": "No input data provided"}), 400
    
    # 데이터 검증 및 역직렬화
    try:
        user = user_schema.load(json_data)
    except ValidationError as err:
        return jsonify(err.messages), 422
    
    # 데이터베이스에 저장
    db.session.add(user)
    db.session.commit()
    
    # 생성된 사용자 정보 반환
    result = user_schema.dump(user)
    return jsonify({"message": "User created successfully", "user": result}), 201

@app.route('/users', methods=['GET'])
def get_users():
    users = User.query.all()
    result = users_schema.dump(users)
    return jsonify(result), 200
  • 장점 : 코드의 간결화
  • 단점 :여전히 복잡한 세팅으로 인해 api 서비스의 문제인지, json 역직렬화의 문제인지, schema내의 데이터 형식문제인지, 그냥 flask-migrate이나 다른 세팅의 문제인지 분간이 너무 힘들었음

스키마

니들은 중첩 스키마 동적 데이터 이딴거 쓰지마라... 쓰지말라면 쓰지마...

중첩스키마

class AddressSchema(Schema):
    street = fields.Str()
    city = fields.Str()

class UserSchema(Schema):
    name = fields.Str()
    address = fields.Nested(AddressSchema)

동적 데이터 구조

  • 스키마가 동적이거나/ 필드 타입이 미정일때
  • nosql 데이터 베이스 등을 다룰때 유용하다고 함..
  • 나의 경우 : API가 다양한 형태의 데이터를 반환해야 할 때 사용
  • 나의 경우 2 : 특정 필드의 데이터를 검증이나 변환 없이 그대로 통과시키고 싶을 때
    • 제발.. list를 string / Integer 든 제발 json 화 해서 보내줘..
from marshmallow import Schema, fields

class DynamicSchema(Schema):
    id = fields.Int()
    data = fields.Raw()
    
class NestedDataSchema(Schema):
    id = fields.Int()
    name = fields.Str()
    complex_data = fields.Raw()  # 복잡하고 가변적인 중첩 데이터
  • 내부에서만 도는 스키마임.
    • 즉 client에서 api로 통신하는게 아님
    • DB서버 내에서 restapi를 활용하여/ eventHanlder처럼/ 작동하는 table임
      내가 생각한 로직이지만 정말 심하게 오버한 고오급 로오직이였음
  • .. 진짜 개발 때려칠 뻔 함.

JSON 역직렬화 /직렬화

  • 이 부분은 워딩이 자주 헷갈림
  • 그리고 DB와 marshmallow Schema와 함께 나를 수면장애에 이끌었던 문제이기도 함 ( Valdiation Error 도 겸사로)
    • 역직렬화 : REST API를 받는다 (BE 입장) - Deserializable
    • 직렬화 : REST API 준다 ( BE 입장) dump로 JSON / jsonify 해서 준다 - Serializable
      • 여기가 해결이 안되서 왜 안되는지 / 자꾸 워딩도 헷갈리고/ 데코레이션도 헷갈리고
  • 특히 marshmallow schema 내 decoration 생성시 주의사항
    • marshmalloow 자체에도 데이터 유효성 검사 및 변환 기능 있음
  from marshmallow import Schema, fields

class UserSchema(Schema):
    name = fields.Str(required=True)
    age = fields.Int(strict=True, validate=lambda n: 18 <= n <= 100)
    email = fields.Email()

자주 쓴 데코레이터

  • 자체적으로 커스텀 메소드도 사용가능
  class UserSchema(Schema):
    name = fields.Str(required=True)
    password = fields.Str(required=True)

    @validates('password')
    def validate_password(self, value):
        if len(value) < 6:
            raise ValidationError("Password must be at least 6 characters long.")

pre_load :

- 데이터가 스키마의 필드로 로드되기 전에 실행됩니다.
- marshmallow 필드 변환전 데이터정제/ 데이터 형식 변환(~key)/Valdiation check 
- 주로 입력 데이터를 정제하거나 형식을 변경하는 데 사용됩니다.

post_load :

- 데이터가 스키마의 필드로 로드된 후에 실행됩니다.
- 주로 최종 객체를 생성하거나 추가적인 비즈니스 로직을 적용하는 데 사용됩니다. 
- python obejct 생성/ 비즈니스 로직 추가 / 관련 object처리

validates :

  • 유효성 검사
  • db 모델에서 할 수도 있고 marshmallow에서도 할 수도 있음
 @validates('age')
def validate_age(self, value):
    if value < 0:
        raise ValidationError("Age must be positive.")

그 외

  • 나는 db model 에서 주로 to_dict을 사용해서 이미 db에서 dictionary 화 했음
  • 오히려 이 데코레이션 때문에 중복(?) 오류가 났었음

pre_dump :

  • 객체 직렬화 전 실행
  • 출력 데이터 전처리
    @pre_dump
    def remove_sensitive_data(self, data, **kwargs):
       data.pop('password', None)
       return data

post_dump :

  • 객체 직렬화 후 실행
    • 최종 출력 수정
@post_dump
def add_envelope(self, data, **kwargs):
    return {'user': data}

주의

  • DB 스키마는 Sqlite3 필드와 무관함
  • FLOW
    Client req > pre_load > marshmallow field > post_load> python dict/object > sqlite3 save
profile
🏠TECH & GOSSIP

0개의 댓글