数据库列的混合内容(float || unicode)

时间:2011-05-27 15:27:53

标签: python database-design sqlalchemy

为了简单起见,我想说我有一个问题。

每个答案都会得分。

有些问题是定性,因此用户必须在其中一个文字答案中进行选择。

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

  1. cat [1分]
  2. 狗[2分]
  3. 凯门鳄[3分]
  4. 回答我得到2分。

    有些问题是定量,因此用户输入一个数字并通过线性插值得分:

    你一天喝多少啤酒?

    1. 0 [0分]
    2. 1 [1分]
    3. 3 [5分]
    4. 如果我回答 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._value answer.question.quantitativeTrue

      2. 我可以在单独的列中回答文字和数字值(例如valuetext,无论如何我都没有数百万条记录)

      3. 或者...

      4. 什么应该更有效且易于使用?

        请考虑SQLAlchemy魔术来处理很多肮脏的工作,我想保持这么简单。


        修改

        由于啤酒示例可能会产生误导,我会与其他人合并:

        问:你以多少钱捐给慈善机构?

        1. 0 [0分]
        2. 10 [1分]
        3. 100 [2分]
        4. 喜欢宠物&啤酒问题我已将数据库中存储的值"0""10""100"作为answers.value列中的字符串进行回答,以便插值以获得回答{{ 1}}我有时间将50转换为浮动。

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

2 个答案:

答案 0 :(得分:3)

使这种不必要的复杂化的原因是尝试优化定量答案。

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

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

但是出于软件目的,它可以很好地考虑所有答案的定性,并保持任何定量映射完全分离。


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

  

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

正确。

  

插入值以获得答案50的分数我有时间将答案转换为值。浮动值。

不正确的。

以与处理宠物相同的方式查看它们。这是一个简单的连接。像做宠物一样做一切。将所有数据视为“定性”。一个简单的规则;不是两个规则。这是正确的标准解决方案。

答案 1 :(得分:1)

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

我希望此版本的代码能够回答评论中提出的所有问题。