【问题标题】:Mixed content (float || unicode) for database column数据库列的混合内容(浮点数 || unicode)
【发布时间】:2011-05-27 15:27:52
【问题描述】:

为了简单起见,假设我有一个问题。

每个答案都会获得一个分数。

有些问题是定性的,因此用户必须在文本答案之一之间进行选择。

问:你最喜欢的宠物是什么?

  1. 猫 [1 分]
  2. 狗 [2 分]
  3. 凯门鳄[3分]

回答我得2分。

有些问题是定量的,所以用户输入一个数字并通过线性插值获得评分:

你一天喝多少升啤酒?

  1. 0 [0 分]
  2. 1 [1 分]
  3. 3 [5 分]

如果我回答 2 升,我会得到 3 分

现在我使用 sqlalchemy 并有一个表格,每行都有一个答案:

questions
    id PK
    name String
    quantitative Bool

answers
    id Integer PK
    id_question Integer FK
    value String

每次我必须将answers.value 转换为浮点数以进行插值等处理。

  1. 我可以将列名 value 更改为 _value 并为 answer.value 创建 getter 和 setter 函数,如果问题是数字,则每次转换为浮动 answer._valueanswer.question.quantitativeTrue

  2. 我可以有单独的列来回答文本和数值(例如 valuetext,反正我不会有数百万条记录)

  3. 或者...

什么应该更高效和易于使用?

请考虑一下 SQLAlchemy 魔法,它可以处理很多脏活,我想保持简单。


编辑

由于 啤酒示例 可能会产生误导,因此我将其与另一个示例集成:

问:你捐了多少美元慈善捐款?

  1. 0 [0 点]
  2. 10 [1 分]
  3. 100 [2 分]

喜欢 宠物和啤酒问题 我有答案值 "0""10""100" 作为字符串存储在数据库中 answers.value 列中,以便插入值以获得答案得分50 我一直有时间将answers.value 转换为浮动。

这是我在同一 db 列中混合内容类型的地方。

【问题讨论】:

    标签: python database-design sqlalchemy


    【解决方案1】:

    使这变得不必要的复杂的原因是试图优化量化答案。

    这是多项选择。将定量的答案视为定性的。携带“点”作为每个答案的单独属性。

    是的,数据库中会有 ("3 liters", 3)。是的,对于有思想的人来说,这似乎是多余的。

    但出于软件目的,将所有答案定性考虑并保持任何定量映射完全分开会很有效。


    编辑。不要将答案存储为数字。这完全是错误的。

    对于宠物和啤酒问题,我将答案值“0”、“10”、“100”作为字符串存储在 answers.value 列中的数据库中。

    正确。

    插入值以获得答案 50 的分数我一直将 answers.value 转换为浮点数。

    不正确。

    按照处理宠物的方式查找它们。这是一个简单的连接。像对待宠物一样做每一件事。将所有数据视为“定性的”。一个简单的规则;不是两条规则。这是正确和标准的解决方案。

    【讨论】:

    • 我明白你的意思,但我的主要问题是将数据部分插入“3 升”作为字符串,我将用一个更好的例子来更新我的问题。谢谢
    • @S.Lott:很抱歉,但我无法理解当你说我进行插值以获得中间值时不正确:我确实需要为了对定量进行插值,系统以这种方式工作,所以我根本不能像定性(宠物)一样行事。我最后有 2 列,每列回答 float 类型的 valuestring 类型的 description,并根据 question.quantitative 使用其中一个或另一个。这是多余的,我在问这是一种好方法还是有替代方法。感谢您的宝贵时间
    • @neurino:“这是多余的”。不,您在一张表中有两种不同的数据;你需要两列。 “我在问这是一种好方法还是有其他选择。”我给你另一种选择。不要在一张表中保存两种数据。将所有数据视为定性数据并查找所有定量分值。一种数据比两种数据简单。
    • @S.Lott:“查找所有定量分值”是什么意思?我希望我能接受你的回答,但不知道它是如何工作的。您能否为我的 charity 问题发布一个数据库答案条目示例,以及为answered_value == 50 传递什么给interpolate(answered_value) -> score 函数?
    • @neurino:“查找所有定量得分值”意味着在定性代码和定量值之间进行 JOIN。如果您想坚持某些定性值是“定量的”,请在答案中包含“定量”标志;在这种情况下,代码和值也可能相同。 interpolate(answered_value) -> score 是与答案表的 JOIN,用于将答案值转换为分数。定性和定量总是相同的。想法是这样的:没有“定量”数据。这都是定性的。生活更简单。
    【解决方案2】:

    对于一个快速而肮脏的解决方案,我建议至少使用两个不同的列来存储不同的答案。您还可以向数据库添加 CHECK 约束,以确保其中一个用于任何行,另一个为 NULL。比执行 quick-n-dirty 代码来计算总 Test 分数。

    另一种选择

    这个想法是建立正确的对象模型,将其映射到 RDMBS,这个问题不需要问。我还希望在使用Single Table Inheritance 时,生成的数据库架构几乎与当前实现相同(当您使用选项echo=True 运行脚本时,您可以看到模型):

    CREATE TABLE questions (
        id INTEGER NOT NULL, 
        text VARCHAR NOT NULL, 
        type VARCHAR(10) NOT NULL, 
        PRIMARY KEY (id)
    )
    
    CREATE TABLE answer_options (
        id INTEGER NOT NULL, 
        question_id INTEGER NOT NULL, 
        value INTEGER NOT NULL, 
        type VARCHAR(10) NOT NULL, 
        text VARCHAR, 
        input INTEGER, 
        PRIMARY KEY (id), 
        FOREIGN KEY(question_id) REFERENCES questions (id)
    )
    
    CREATE TABLE answers (
        id INTEGER NOT NULL, 
        type VARCHAR(10) NOT NULL, 
        question_id INTEGER, 
        test_id INTEGER, 
        answer_option_id INTEGER, 
        answer_input INTEGER, 
        PRIMARY KEY (id), 
        FOREIGN KEY(question_id) REFERENCES questions (id), 
        FOREIGN KEY(answer_option_id) REFERENCES answer_options (id), 
        --FOREIGN KEY(test_id) REFERENCES tests (id)
    )
    

    下面的代码是一个完整的工作脚本,它显示了对象模型、它到数据库的映射以及使用场景。按照设计,该模型可以轻松扩展为其他类型的问题/答案,而不会对现有类产生任何影响。基本上你得到的代码更少hacky和更灵活,因为你有一个正确反映你的情况的对象模型。代码如下:

    from sqlalchemy import create_engine, Column, Integer, SmallInteger, String, ForeignKey, Table, Index
    from sqlalchemy.orm import relationship, scoped_session, sessionmaker
    from sqlalchemy.ext.declarative import declarative_base
    
    # Configure test data SA
    engine = create_engine('sqlite:///:memory:', echo=True)
    session = scoped_session(sessionmaker(bind=engine))
    Base = declarative_base()
    Base.query = session.query_property()
    
    class _BaseMixin(object):
        """ Just a helper mixin class to set properties on object creation.  
        Also provides a convenient default __repr__() function, but be aware that 
        also relationships are printed, which might result in loading relations.
        """
        def __init__(self, **kwargs):
            for k,v in kwargs.items():
                setattr(self, k, v)
    
        def __repr__(self):
            return "<%s(%s)>" % (self.__class__.__name__, 
                ', '.join('%s=%r' % (k, self.__dict__[k]) 
                    for k in sorted(self.__dict__) if '_sa_' != k[:4] and '_backref_' != k[:9])
                )
    
    ### AnswerOption hierarchy
    class AnswerOption(Base, _BaseMixin):
        """ Possible answer options (choice or any other configuration).  """
        __tablename__ = u'answer_options'
        id = Column(Integer, primary_key=True)
        question_id = Column(Integer, ForeignKey('questions.id'), nullable=False)
        value = Column(Integer, nullable=False)
        type = Column(String(10), nullable=False)
        __mapper_args__ = {'polymorphic_on': type}
    
    class AnswerOptionChoice(AnswerOption):
        """ A possible answer choice for the question.  """
        text = Column(String, nullable=True) # when mapped to single-table, must be NULL in the DB
        __mapper_args__ = {'polymorphic_identity': 'choice'}
    
    class AnswerOptionInput(AnswerOption):
        """ A configuration entry for the input-type of questions.  """
        input = Column(Integer, nullable=True) # when mapped to single-table, must be NULL in the DB
        __mapper_args__ = {'polymorphic_identity': 'input'}
    
    ### Question hierarchy
    class Question(Base, _BaseMixin):
        """ Base class for all types of questions.  """
        __tablename__ = u'questions'
        id = Column(Integer, primary_key=True)
        text = Column(String, nullable=False)
        type = Column(String(10), nullable=False)
        answer_options = relationship(AnswerOption, backref='question')
        __mapper_args__ = {'polymorphic_on': type}
    
        def get_answer_value(self, answer):
            """ function to get a value of the answer to the question.  """
            raise Exception('must be implemented in a subclass')
    
    class QuestionChoice(Question):
        """ Single-choice question.  """
        __mapper_args__ = {'polymorphic_identity': 'choice'}
    
        def get_answer_value(self, answer):
            assert isinstance(answer, AnswerChoice)
            assert answer.answer_option in self.answer_options, "Incorrect choice"
            return answer.answer_option.value
    
    class QuestionInput(Question):
        """ Input type question.  """
        __mapper_args__ = {'polymorphic_identity': 'input'}
    
        def get_answer_value(self, answer):
            assert isinstance(answer, AnswerInput)
            value_list = sorted([(_i.input, _i.value) for _i in self.answer_options])
            if not value_list:
                raise Exception("no input is specified for the question {0}".format(self))
            if answer.answer_input <= value_list[0][0]:
                return value_list[0][1]
            elif answer.answer_input >= value_list[-1][0]:
                return value_list[-1][1]
            else: # interpolate in the range:
                for _pos in range(len(value_list)-1):
                    if answer.answer_input == value_list[_pos+1][0]:
                        return value_list[_pos+1][1]
                    elif answer.answer_input < value_list[_pos+1][0]:
                        # interpolate between (_pos, _pos+1)
                        assert (value_list[_pos][0] != value_list[_pos+1][0])
                        return value_list[_pos][1] + (value_list[_pos+1][1] - value_list[_pos][1]) * (answer.answer_input - value_list[_pos][0]) / (value_list[_pos+1][0] - value_list[_pos][0])
            assert False, "should never reach here"
    
    ### Answer hierarchy
    class Answer(Base, _BaseMixin):
        """ Represents an answer to the question.  """
        __tablename__ = u'answers'
        id = Column(Integer, primary_key=True)
        type = Column(String(10), nullable=False)
        question_id = Column(Integer, ForeignKey('questions.id'), nullable=True) # when mapped to single-table, must be NULL in the DB
        question = relationship(Question)
        test_id = Column(Integer, ForeignKey('tests.id'), nullable=True) # @todo: decide if allow answers without a Test
        __mapper_args__ = {'polymorphic_on': type}
    
        def get_value(self):
            return self.question.get_answer_value(self)
    
    class AnswerChoice(Answer):
        """ Represents an answer to the *Choice* question.  """
        __mapper_args__ = {'polymorphic_identity': 'choice'}
        answer_option_id = Column(Integer, ForeignKey('answer_options.id'), nullable=True) 
        answer_option = relationship(AnswerOption, single_parent=True)
    
    class AnswerInput(Answer):
        """ Represents an answer to the *Choice* question.  """
        __mapper_args__ = {'polymorphic_identity': 'input'}
        answer_input = Column(Integer, nullable=True) # when mapped to single-table, must be NULL in the DB
    
    ### other classes (Questionnaire, Test) and helper tables
    association_table = Table('questionnaire_question', Base.metadata,
        Column('id', Integer, primary_key=True),
        Column('questionnaire_id', Integer, ForeignKey('questions.id')),
        Column('question_id', Integer, ForeignKey('questionnaires.id'))
    )
    _idx = Index('questionnaire_question_u_nci', 
                association_table.c.questionnaire_id, 
                association_table.c.question_id, 
                unique=True)
    
    class Questionnaire(Base, _BaseMixin):
        """ Questionnaire is a compilation of questions.  """
        __tablename__ = u'questionnaires'
        id = Column(Integer, primary_key=True)
        name = Column(String, nullable=False)
        # @note: could use relationship with order or even add question number
        questions = relationship(Question, secondary=association_table)
    
    class Test(Base, _BaseMixin):
        """ Test is a 'test' - set of answers for a given questionnaire. """
        __tablename__ = u'tests'
        id = Column(Integer, primary_key=True)
        # @todo: add user name or reference
        questionnaire_id = Column(Integer, ForeignKey('questionnaires.id'), nullable=False)
        questionnaire = relationship(Questionnaire, single_parent=True)
        answers = relationship(Answer, backref='test')
        def total_points(self):
            return sum(ans.get_value() for ans in self.answers)
    
    # -- end of model definition --
    
    Base.metadata.create_all(engine)
    
    # -- insert test data --
    print '-' * 20 + ' Insert TEST DATA ...'
    q1 =  QuestionChoice(text="What is your fav pet?")
    q1c1 = AnswerOptionChoice(text="cat", value=1, question=q1)
    q1c2 = AnswerOptionChoice(text="dog", value=2, question=q1)
    q1c3 = AnswerOptionChoice(text="caiman", value=3)
    q1.answer_options.append(q1c3)
    a1 = AnswerChoice(question=q1, answer_option=q1c2)
    assert a1.get_value() == 2
    session.add(a1)
    session.flush()
    
    q2 =  QuestionInput(text="How many liters of beer do you drink a day?")
    q2i1 = AnswerOptionInput(input=0, value=0, question=q2)
    q2i2 = AnswerOptionInput(input=1, value=1, question=q2)
    q2i3 = AnswerOptionInput(input=3, value=5)
    q2.answer_options.append(q2i3)
    
    # test interpolation routine
    _test_ip = ((-100, 0),
                (0, 0),
                (0.5, 0.5),
                (1, 1),
                (2, 3),
                (3, 5),
                (100, 5)
    )
    a2 = AnswerInput(question=q2, answer_input=None)
    for _inp, _exp in _test_ip:
        a2.answer_input = _inp
        _res = a2.get_value()
        assert _res == _exp, "{0}: {1} != {2}".format(_inp, _res, _exp)
    a2.answer_input = 2
    session.add(a2)
    session.flush()
    
    # create a Questionnaire and a Test
    qn = Questionnaire(name='test questionnaire')
    qn.questions.append(q1)
    qn.questions.append(q2)
    session.add(qn)
    te = Test(questionnaire=qn)
    te.answers.append(a1)
    te.answers.append(a2)
    assert te.total_points() == 5
    session.add(te)
    session.flush()
    
    # -- other tests --
    print '-' * 20 + ' TEST QUERIES ...'
    session.expunge_all() # clear the session cache
    a1 = session.query(Answer).get(1)
    assert a1.get_value() == 2 # @note: will load all dependant objects (question and answer_options) automatically to compute the value
    a2 = session.query(Answer).get(2)
    assert a2.get_value() == 3 # @note: will load all dependant objects (question and answer_options) automatically to compute the value
    te = session.query(Test).get(1)
    assert te.total_points() == 5
    

    我希望这个版本的代码能够回答 cmets 中提出的所有问题。

    【讨论】:

    • 第三次阅读您的答案时,我开始弄清楚它是如何工作的,但仍然不清楚我最终会使用哪些表格。在您建议的 quick-n-dirty 解决方案(我已经实施)中,我有 3 个表:Question(带有定量/定性 bool 列),Answers(带有 @987654326 @ 列用于定量,description 用于定性)和分数value 列包含score 用于定性问题或input 用于定量,根据`Question.quantitative)。它有效,但我承认它具有误导性。无论如何都要 +1
    • @neurino:见 Edit-1。如果您以前没有使用过像 SA 这样的 ORM 工具的继承,可能会有一些学习曲线,但 IMO 非常值得。如果我有更多时间,我会将代码作为一个工作示例。
    • 谢谢,我是 ORM 继承的新手,但我学习很好
    • 好的,请参阅完整的工作示例。希望它与您的模型相匹配,并为您提供启动...
    • 非常感谢您的代码,现在我对表格和polymorphic_on 的东西很清楚,您的代码运行顺利。我希望你能得到越来越多的支持
    猜你喜欢
    • 1970-01-01
    • 2016-07-20
    • 1970-01-01
    • 2020-09-20
    • 1970-01-01
    • 2021-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多