Для быстрого и грязного решения я бы предложил по крайней мере использовать два разных столбца для хранения разных ответов.Вы также можете добавить ограничение 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)
)
Приведенный ниже код представляет собой полный рабочий скрипт, который отображает как объектную модель, ее отображение в базе данных, так и сценарии использования.Поскольку она разработана, модель легко расширяется с помощью других типов вопросов / ответов без какого-либо влияния на существующие классы.В основном вы получаете менее хакерский и более гибкий код просто потому, что у вас есть объектная модель, которая правильно отражает ваш случай.Код ниже:
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
Я надеюсь, что эта версия кода отвечает на все вопросы, заданные в комментариях.