sqlalchemy.InvalidRequestError: object load error

2 분 소요

들어가며

업무중에 server report에 본적 없던 아래와 같은 오류가 발생해 원인을 분석하게 되었다.

sqlalchemy.exc.InvalidRequestError: Row with identity key (<class '__main__.User'>, (1,), None) can't be loaded into an object; the polymorphic discriminator column 'User.user_type' refers to mapped class SubUserA->SubUserA, which is not a sub-mapper of the requested mapped class SubUserB->SubUserB

원인

확인해본 결과 database에 data가 잘못 들어가 있었던 것이 근본적인 원인이 되었다. 간단히 설명하자면 parent class에서 여러 sub class를 구분할 수 있는 column에 ‘a’라는 data가 들어가 있어야 하는데 ‘b’라고 잘못 들어가 있었다.

재현

flask, flask_sqlalchemy library를 이용해 간단히 구현한 테스트용 app이다.

from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_type = db.Column(db.String(20), nullable=False)
    __tablename__ = "User"
    __mapper_args__ = {
        "polymorphic_identity": __tablename__,
        "polymorphic_on": user_type,
    }


class SubUserA(User):
    id = db.Column(db.Integer, db.ForeignKey("User.id"), primary_key=True)
    __tablename__ = "SubUserA"
    __mapper_args__ = {"polymorphic_identity": __tablename__}


class SubUserB(User):
    id = db.Column(db.Integer, db.ForeignKey("User.id"), primary_key=True)
    __tablename__ = "SubUserB"
    __mapper_args__ = {"polymorphic_identity": __tablename__}


app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "mysql+pymysql://test_id:test_pw@localhost:3306/test_database"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True


@app.route("/test")
def test():
    SubUserB.query.all()
    return jsonify({"test": "success"})


if __name__ == "__main__":
    db.init_app(app)
    app.run(port=5000)

User를 상속받은 SubUserA, SubUserB가 있으며 user_type으로 SubUserA인지 SubUserB인지 구분하도록 구현되어있다. Database 또한 아래 SQL과 같이 User, SubUserA, SubUserB, Request 각 table로 이루어져 있으며, User의 user_type은 복수의 값을 가질 수 없으므로 SubUserA나 SubUserB 둘 중 하나만 될 수 있다.

CREATE TABLE `User` (
    `id` INT PRIMARY KEY,
    `user_type` VARCHAR(20) NOT NULL
);

CREATE TABLE `SubUserA` (
    `id` INT NOT NULL,
    FOREIGN KEY (`id`) REFERENCES `user` (`id`)
);

CREATE TABLE `SubUserB` (
    `id` INT NOT NULL,
    FOREIGN KEY (`id`) REFERENCES `user` (`id`)
);

이 상태에서 Database의 User table에 user_type은 ‘SubUserA’로 저장하고 SubUserB에 User table의 id와 같은 id 값을 저장한다.

INSERT INTO `User` VALUES (1, 'SubUserA');
INSERT INTO `SubUserB` VALUES (1);

아래의 명령어를 실행하면 처음에 본 에러가 발생하게 된다.

curl "http://localhost:5000/v2/ping"

분석

SQLAlchemy library의 sqlalchemy/orm/loading.py의 _decorate_polymorphic_switch() 내부 함수인 polymorphic_instance()를 확인해보면 query로 가져온 row들을 object로 변환하는 과정에서 error가 발생했음을 볼 수 있다.

def polymorphic_instance(row):
    discriminator = getter(row)  # row의 column 중 polymorphic_on으로 정의한 값을 가져옴
    if discriminator is not None:
        _instance = polymorphic_instances[discriminator]  # discriminator와 query 생성시 사용한 mapper를 비교하여 동일하거나 부모 클래스인지 확인
        if _instance:
            return _instance(row)
        elif _instance is False:
            identitykey = ensure_no_pk(row)  # primary_key가 null인지 확인, null일 경우 None 반환

            if identitykey:
                raise sa_exc.InvalidRequestError(
                    "Row with identity key %s can't be loaded into an "
                    "object; the polymorphic discriminator column '%s' "
                    "refers to %s, which is not a sub-mapper of "
                    "the requested %s"
                    % (
                        identitykey,
                        polymorphic_on,
                        mapper.polymorphic_map[discriminator],
                        mapper,
                    )
                )
            else:
                return None
        else:
            return instance_fn(row)
    else:
        identitykey = ensure_no_pk(row)

        if identitykey:
            raise sa_exc.InvalidRequestError(
                "Row with identity key %s can't be loaded into an "
                "object; the polymorphic discriminator column '%s' is "
                "NULL" % (identitykey, polymorphic_on)
            )
        else:
            return None

References

댓글남기기