Ошибка
Can't determine relationship direction for relationship 'UserDataType.response_option_names' - foreign key columns are present in neither the parent nor the child's mapped tables
говорит о том, что отношение внешнего ключа от ResponseOption
до UserDataType
ожидалось и не было найдено.
Этот внешний ключ ожидается, так как вы определили столбец response_option_names = db.relationship("ResponseOption", ...)
в таблице UserDataType
. В этом сценарии «многие ко многим» правильно не иметь такого внешнего ключа, который вы будете связывать UserDataType
и ResponseOption
, используя таблицу ассоциации UserDataTypeResponseOption
.
Следуя документации здесь , вы можете исправить это, изменив
response_option_names = db.relationship("ResponseOption", primaryjoin="UserDataType.id==UserDataTypeResponseOption.user_data_type_id", viewonly=True)
на
associations = db.relationship("UserDataTypeResponseOption", primaryjoin="UserDataType.id==UserDataTypeResponseOption.user_data_type_id", viewonly=True)
Вам также необходимо добавить новое отношение с UserDataTypeResponseOption
на ResponseOption
, например так:
response_option = db.relationship("ResponseOption")
Теперь ваш запрос
user_data_type_details = UserDataType.query.filter(UserDataType.id.in_(user_data_types)).\
filter_by(language_id=language_pref).\
join(UserDataTypeResponseOption, UserDataType.id == UserDataTypeResponseOption.user_data_type_id).\
join(ResponseOption, UserDataTypeResponseOption.response_option_id == ResponseOption.id).\
filter_by(language_id=language_pref)\
будет работать нормально, но обратите внимание, что у вас нет прямого доступа к ResponseOption
с каждого элемента UserDataType
. Вам нужно будет с go по UserDataTypeResponseOption
:
for udt in user_data_type_details:
print('user data type: ', udt)
for assoc in udt.associations:
print('response option: ', assoc.response_option.response_option_name)
ВЫХОД:
user data type: <UserDataType 1, 1>
response option: No e-mails please
response option: Only fun e-mails
response option: All e-mails please
Но мы еще не закончили. На этом этапе, если вы посмотрите на структуру базы данных, вы увидите, что она на самом деле непригодна для использования более чем на одном языке, поскольку таблица ассоциации ничего не знает о языках и вы можете связать UserDataType
с ResponseOption
только соответствующими им идентификаторы и эти идентификаторы не обязательно должны быть уникальными для этих таблиц.
Одним из решений будет изменение структуры ваших таблиц, чтобы таблица ассоциаций также знала идентификаторы языка как для UserDataType
, так и ResponseOption
. Для этого вам нужно добавить два ForeignKeyConstraint
ограничения к UserDataTypeResponseOption
, так как это способ создания внешних ключей с несколькими столбцами в SQLAlchemy
. Я также удалил столбец id
из таблицы сопоставлений и отметил остальные столбцы как первичные ключи:
class UserDataType(db.Model):
__tablename__ = 'user_data_type'
id = db.Column(db.Integer, primary_key=True, nullable=False)
language_id = db.Column(db.Integer, primary_key=True, nullable=False)
user_data_type_name = db.Column(db.Text)
associations = db.relationship("UserDataTypeResponseOption", viewonly=True)
class ResponseOption(db.Model):
__tablename__ = 'response_option'
id = db.Column(db.Integer, primary_key=True, nullable=False)
language_id = db.Column(db.Integer, primary_key=True, nullable=False)
response_option_name = db.Column(db.Text)
class UserDataTypeResponseOption(db.Model):
__tablename__ = 'user_data_type_response_option'
user_data_type_id = db.Column(db.Integer, primary_key=True)
user_data_type_language_id = db.Column(db.Integer, primary_key=True)
response_option_id = db.Column(db.Integer, primary_key=True)
response_option_language_id = db.Column(db.Integer, primary_key=True)
response_option = db.relationship("ResponseOption")
__table_args__ = (ForeignKeyConstraint([user_data_type_id, user_data_type_language_id],
[UserDataType.id, UserDataType.language_id]),
ForeignKeyConstraint([response_option_id, response_option_language_id],
[ResponseOption.id, ResponseOption.language_id]),
{})
И теперь вы можете использовать несколько языков.
Прямой доступ к ResponseOption
s из UserDataType
объектов.
SQLAlchemy
предоставляет способ прямого доступа к ResponseOption
s с помощью расширения прокси-сервера ассоциации (см. здесь ):
Это расширение позволяет настраивать атрибуты, которые будут иметь доступ к двум «прыжкам» с одним доступом, одному «прыжку» к связанному объекту и второму к целевому атрибуту.
Но это сопровождается предупреждением:
Предупреждение Шаблон объекта ассоциации не координирует изменения с отдельным отношением, которое отображает таблицу ассоциации как «вторичную».
Поскольку вы не изменяете никакие сущности, вы можете безопасно использовать расширение следующим образом:
class UserDataType(db.Model):
__tablename__ = 'user_data_type'
id = db.Column(db.Integer, primary_key=True, nullable=False)
language_id = db.Column(db.Integer, primary_key=True, nullable=False)
user_data_type_name = db.Column(db.Text)
response_options = db.relationship("ResponseOption", secondary="user_data_type_response_option", viewonly=True) # added secondary
class ResponseOption(db.Model):
__tablename__ = 'response_option'
id = db.Column(db.Integer, primary_key=True, nullable=False)
language_id = db.Column(db.Integer, primary_key=True, nullable=False)
response_option_name = db.Column(db.Text)
class UserDataTypeResponseOption(db.Model):
__tablename__ = 'user_data_type_response_option'
user_data_type_id = db.Column(db.Integer, primary_key=True)
user_data_type_language_id = db.Column(db.Integer, primary_key=True)
response_option_id = db.Column(db.Integer, primary_key=True)
response_option_language_id = db.Column(db.Integer, primary_key=True)
response_option = db.relationship("ResponseOption", backref="parent_associations") # added backref
__table_args__ = (ForeignKeyConstraint([user_data_type_id, user_data_type_language_id],
[UserDataType.id, UserDataType.language_id]),
ForeignKeyConstraint([response_option_id, response_option_language_id],
[ResponseOption.id, ResponseOption.language_id]),
{})
for udt in user_data_type_details:
print('user data type: ', udt)
for ro in udt.response_options:
print('response option: ', ro.response_option_name)
ВЫХОД:
user data type: <UserDataType 1, 1>
response option: No e-mails please
response option: Only fun e-mails
response option: All e-mails please
Полный код
app.py
* 1 091 *
pref_edit.py
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>pref_edit</title>
</head>
<body>
{% for user_data_type_detail in user_data_type_details %}
<div>
<label for="{{ user_data_type_detail.user_data_type_name }}">{{ user_data_type_detail.user_data_type_name }}</label>
<select name="{{ user_data_type_detail.user_data_type_name }}">
{% for ro in user_data_type_detail.response_options %}
<option value="{{ ro.response_option_name }}">{{ ro.response_option_name }}</option>
{% endfor %}
</select>
</div>
{% endfor %}
</body>
</html>
Другие возможные решения
Существуют и другие решения, если вы хотите изменить дизайн базы данных, чтобы переместить language_id
из UserDataType
и ResponseOption
таблиц в новые таблицы. Проверьте эту интересную статью для разных подходов.