为了简单起见,我想说我有一个问题。
每个答案都会得分。
有些问题是定性,因此用户必须在其中一个文字答案中进行选择。
问:你最喜欢的宠物是什么?
回答狗我得到2分。
有些问题是定量,因此用户输入一个数字并通过线性插值得分:
你一天喝多少啤酒?
如果我回答 2升,我会 3分。
现在我使用sqlalchemy并在每一行都有一个答案表:
questions
id PK
name String
quantitative Bool
answers
id Integer PK
id_question Integer FK
value String
每次我必须将它作为插值数等处理时,并将answers.value
转换为浮动等等。
我可以将列名value
更改为_value
,并为answer.value
生成getter和setter函数,如果问题是数字,则每次都会转换为answer._value
answer.question.quantitative
是True
)
我可以在单独的列中回答文字和数字值(例如value
和text
,无论如何我都没有数百万条记录)
或者...
什么应该更有效且易于使用?
请考虑SQLAlchemy魔术来处理很多肮脏的工作,我想保持这么简单。
修改:
由于啤酒示例可能会产生误导,我会与其他人合并:
问:你以多少钱捐给慈善机构?
喜欢宠物&啤酒问题我已将数据库中存储的值"0"
,"10"
,"100"
作为answers.value
列中的字符串进行回答,以便插值以获得回答{{ 1}}我有时间将50
转换为浮动。
这是我在同一个db列中混合内容类型的地方。
答案 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
我希望此版本的代码能够回答评论中提出的所有问题。