Как настроить зависимые фабрики с помощью Factory Boy и Flask-SQLAlchemy?

Как правильно использовать factory boy с ограничениями Flask-SQLAlchemy и External Key?

Рассмотрим следующую настройку модели Flask SQLAlchemy:

# coding=utf-8
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
db = SQLAlchemy(app)

# ------------------------------
#   SQLAlchemy Table Models
# ------------------------------
class User(db.Model):
    """ A SQLAlchemy simple model class who represents a user with a ForeignKey Constraint"""
    __tablename__ = 'UserTable'

    user_pk = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.Unicode(20))
    group_fk = db.Column(db.ForeignKey("GroupTable.group_pk"), nullable=False)


class Group(db.Model):
    """ A SQLAlchemy simple model class who represents a user """
    __tablename__ = 'GroupTable'

    group_pk = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(), nullable=False)


# -------------------------
#   Create the SQL tables
# -------------------------
db.create_all()

Схема пользователя требует внешнего ключа группы при создании нового пользователя. Поскольку первичный ключ группы назначается из базы данных, фабрике потребуется зафиксировать запись группы и получить первичный ключ записи, чтобы может предоставить его новому Пользователю.

Как мне создать группу, сохранить ее в БД и предоставить ее ключ фабрике пользователей?

В Factory Boy есть примеры работы с внешними ключами , но они похоже, не применяются к SQLAlchemy. Вот заводы и точка отказа:

# ----------------------------------------
#   Factory-Boy User and Group Factories
# ----------------------------------------
from factory import alchemy, Sequence, RelatedFactory


class GroupFactory(alchemy.SQLAlchemyModelFactory):
    class Meta(object):
        model = Group
        sqlalchemy_session = db.session  # the SQLAlchemy session object

    name = Sequence(lambda n: "Group {}".format(n))
    # group_pk = Sequence(lambda n: n)


class UserFactory(alchemy.SQLAlchemyModelFactory):
    class Meta(object):
        model = User
        sqlalchemy_session = db.session  # the SQLAlchemy session object

    user_pk = Sequence(lambda n: n)
    name = Sequence(lambda n: u'User %d' % n)  # coding=utf-8
    group_fk = RelatedFactory(GroupFactory)


# ----------------------
#   Factory tests
# ----------------------
# Create a new Group from our factory
group_from_factory = GroupFactory(name='a new group name')
assert group_from_factory.group_pk is None
# Save it to our DB
db.session.add(group_from_factory)
db.session.commit()

# Verify that Group Saved correctly to DB
group_from_db = db.session.query(Group).filter(Group.group_pk == group_from_factory.group_pk).first()
assert group_from_db.group_pk is not None
assert group_from_db.name == 'a new group name'
assert group_from_db.group_pk == group_from_factory.group_pk

# Create a new User from our factory
user_from_factory = UserFactory()
db.session.add(user_from_factory)
# ----------------------------------------------
#   FAILS AT COMMIT() - NOT NULL constraint failed (group_fk is null)
# ----------------------------------------------
db.session.commit()

assert user_from_factory.user_pk is not None
assert user_from_factory.name is not None
assert user_from_factory.group_fk is not None

person etiology    schedule 15.07.2015    source источник
comment
Подфабрика не работает. Смотрите комментарии к этому. Согласно API Factory Boy, это должно быть, но это не так, поэтому я опубликовал это.   -  person etiology    schedule 16.07.2015


Ответы (3)


Проблема возникает из-за использования RelatedFactory: они предназначены для обратных отношений ForeignKey (например, если вы хотите создать объект Group, который уже содержит User).

Для прямого ForeignKey - как отношение от User до Group, используйте SubFactory:

class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = User
        sqlalchemy_session = db.session

    # No need to force the user_pk, it is built automatically from the database
    # user_pk = Sequence(lambda n: n)
    name = Sequence(lambda n: u'User %d' % n)  # coding=utf-8
    group_fk = factory.SubFactory(GroupFactory)

Я не очень хорошо знаком с Flask-SQLAlchemy, но я только что добавил небольшой пример в репозиторий (по адресу https://github.com/rbarrois/factory_boy/tree/master/examples/flask_alchemy), который работает, но очень похож на вашу ситуацию.

person Xelnor    schedule 15.07.2015
comment
Спасибо за предложение, но SubFactory не решает эту проблему. SQL выдает ошибку «неподдерживаемый тип», поскольку SubFactory возвращает экземпляр Group вместо Integer. Итак, что мне нужно, так это передать значение Group.group_pk Integer атрибуту User.group_fk, чтобы SQL не сходил с ума при выполнении оператора INSERT. Также объект Group, возвращенный из SubFactory, еще не сохранен в базе данных - person etiology; 16.07.2015
comment
sqlalchemy.exc.InterfaceError: (sqlite3.InterfaceError) Ошибка привязки параметра 1 — вероятно, неподдерживаемый тип. [SQL: u'INSERT INTO UserTable (name, group_fk) VALUES (?, ?)'] [параметры: (u'User 0', ‹__main__.Group object at 0x101c20650›)] - person etiology; 16.07.2015
comment
Код github, который вы написали, великолепен. Похоже, это может быть способ сделать это. Это требует изменения модели таблицы SQLAlchemy, поэтому я не рассматривал SubFactory как решение. Это решение, если модель таблицы построена определенным образом, что является лучшей конструкцией. Мне нужно больше времени, чтобы просмотреть его, прежде чем я отмечу ответ, но это хорошо. - person etiology; 16.07.2015

Ссылка Xelnor на git пока показывает лучший ответ, но для этого требуются изменения в модели sqlalchemy. Вот готовая рабочая копия моего поста с использованием решения Xelnor:

# coding=utf-8
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
db = SQLAlchemy(app)

Табличные модели SQLAlchemy

class User(db.Model):
    """ A SQLAlchemy simple model class who represents a user with a ForeignKey Constraint"""
    __tablename__ = 'user'

    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.Unicode(20))

    group_id = db.Column(db.Integer, db.ForeignKey('group.id'), nullable=False)

'group' db.relationship - это то, что заставляет работать вызов SubFactory. UserFactory передает группу модели User, которая настроена с помощью этого определения отношения().

    group = db.relationship('Group', backref=db.backref('groups', lazy='dynamic'))

    def __init__(self, name, group):
        self.name = name
        self.group = group

    def __repr__(self):
        return '<Group for %r: %s>' % (self.group, self.name)


class Group(db.Model):
    """ A SQLAlchemy simple model class who represents a user """
    __tablename__ = 'group'

    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(), nullable=False)

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return '<Group %r>' % self.name

Создайте таблицы SQL

db.create_all()

Factory-Boy Пользовательские и групповые фабрики

from factory import alchemy, Sequence, SubFactory, fuzzy


class BaseFactory(alchemy.SQLAlchemyModelFactory):
    class Meta(object):
        abstract = True
        sqlalchemy_session = db.session


class GroupFactory(BaseFactory):
    class Meta(object):
        model = Group
        sqlalchemy_session = db.session  # the SQLAlchemy session object

    name = fuzzy.FuzzyText()


class UserFactory(BaseFactory):
    class Meta:
        model = User
        sqlalchemy_session = db.session

    name = fuzzy.FuzzyText()
    group = SubFactory(GroupFactory)

Заводские испытания

# Create a new Group from our factory
group_from_factory = GroupFactory(name='a new group name')
assert group_from_factory.id is None
# Save it to our DB
db.session.add(group_from_factory)
db.session.commit()

# Verify that Group Saved correctly to DB
group_from_db = db.session.query(Group).filter(Group.id == group_from_factory.id).first()
assert group_from_db.id is not None
assert group_from_db.name == 'a new group name'
assert group_from_db.id == group_from_factory.id

# Create a new User from our factory
user1_from_factory = UserFactory(name=u'first')
user2_from_factory = UserFactory(name=u'second')
db.session.add(user1_from_factory)
db.session.add(user2_from_factory)
db.session.commit()

assert user1_from_factory.id is not None
assert user1_from_factory.name is not None
assert user1_from_factory.id is not None

assert user2_from_factory.id is not None
assert user2_from_factory.name is not None
assert user2_from_factory.id is not None
person etiology    schedule 16.07.2015

Вы можете использовать LazyAttribute и лямбду для создания новой группы, а затем вытащить ее 'group_pk'.

Рабочая версия вашего кода ниже:

# coding=utf-8
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
db = SQLAlchemy(app)

# ------------------------------
#   SQLAlchemy Table Models
# ------------------------------
class User(db.Model):
    """ A SQLAlchemy simple model class who represents a user with a ForeignKey Constraint"""
    __tablename__ = 'UserTable'

    user_pk = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.Unicode(20))
    group_fk = db.Column(db.ForeignKey("GroupTable.group_pk"), nullable=False)


class Group(db.Model):
    """ A SQLAlchemy simple model class who represents a user """
    __tablename__ = 'GroupTable'

    group_pk = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(), nullable=False)


# -------------------------
#   Create the SQL tables
# -------------------------
db.drop_all()
db.create_all()
# ----------------------------------------
#   Factory-Boy User and Group Factories
# ----------------------------------------
from factory import alchemy, Sequence, LazyAttribute


class GroupFactory(alchemy.SQLAlchemyModelFactory):
    class Meta(object):
        model = Group
        sqlalchemy_session = db.session  # the SQLAlchemy session object

    name = Sequence(lambda n: "Group {}".format(n))
    group_pk = Sequence(lambda n: n)


class UserFactory(alchemy.SQLAlchemyModelFactory):
    class Meta(object):
        model = User
        sqlalchemy_session = db.session  # the SQLAlchemy session object

    user_pk = Sequence(lambda n: n)
    name = Sequence(lambda n: u'User %d' % n)  # coding=utf-8
    group_fk = LazyAttribute(lambda a: GroupFactory().group_pk)


# ----------------------
#   Factory tests
# ----------------------
# Create a new Group from our factory
group_from_factory = GroupFactory(name='a new group name')
# Save it to our DB
db.session.add(group_from_factory)
db.session.commit()

# Verify that Group Saved correctly to DB
group_from_db = db.session.query(Group).filter(Group.group_pk == group_from_factory.group_pk).first()
assert group_from_db.group_pk is not None
assert group_from_db.name == 'a new group name'
assert group_from_db.group_pk == group_from_factory.group_pk

# Create a new User from our factory
user_from_factory = UserFactory()
db.session.add(user_from_factory)
# ----------------------------------------------
#   FAILS AT COMMIT() - NOT NULL constraint failed (group_fk is null)
# ----------------------------------------------
db.session.commit()

assert user_from_factory.user_pk is not None
assert user_from_factory.name is not None
assert user_from_factory.group_fk is not None
person Technoloft    schedule 16.07.2015