Столкновение ID с классом интерфейса Graphene-SQLAlchemy плюс интерфейс узла - PullRequest
1 голос
/ 04 июня 2019

Я написал графеновые модели для полиморфных объектов, представленных в моей базе данных с SQLalchemy.

Проблема проста:

Я хочу создать интерфейс, который отражает мои модели SQLAlchemy для Graphene, но также либо a) реализует Node, либо b) не конфликтует с Node и позволяет мне получать идентификатор модели без необходимости добавлять ... на Node {id } к строке запроса.

необходимо исключить поле ID из моего интерфейса на основе ORM или конфликты полей с интерфейсом Node, делая это для получения идентификатора, тогда вам нужно добавить ... на Node {id}, что ужасно.

Я создал объект SQLAlchemyInterface, который расширяет graphene.Interface. Многие (но не все) мои модели использовали это, а также Node в качестве интерфейсов. Первая проблема заключалась в том, что оно содержит поле идентификатора и конфликтует с интерфейсом узла.

Я исключил поле id, чтобы он не мешал Node, но потом обнаружил, что больше не могу напрямую запрашивать ID на своих моделях, и мне пришлось добавить ... на Node {id} в строку запроса.

Затем я решил использовать этот узел расширения SQLAlchemyInterface. Мне не нравится этот подход, потому что мне нужно использовать другой (именованный) интерфейс Node для всех моих моделей, которые не обязательно должны реализовывать SQLAlchemyInterface

class SQLAlchemyInterface(Node):
    @classmethod
    def __init_subclass_with_meta__(
            cls,
            model=None,
            registry=None,
            only_fields=(),
            exclude_fields=(),
            connection_field_factory=default_connection_field_factory,
            _meta=None,
            **options
    ):
        _meta = SQLAlchemyInterfaceOptions(cls)
        _meta.name = f'{cls.__name__}Node'

        autoexclude_columns = exclude_autogenerated_sqla_columns(model=model)
        exclude_fields += autoexclude_columns

        assert is_mapped_class(model), (
            "You need to pass a valid SQLAlchemy Model in " '{}.Meta, received "{}".'
        ).format(cls.__name__, model)

        if not registry:
            registry = get_global_registry()

        assert isinstance(registry, Registry), (
            "The attribute registry in {} needs to be an instance of "
            'Registry, received "{}".'
        ).format(cls.__name__, registry)

        sqla_fields = yank_fields_from_attrs(
            construct_fields(
                model=model,
                registry=registry,
                only_fields=only_fields,
                exclude_fields=exclude_fields,
                connection_field_factory=connection_field_factory
            ),
            _as=Field
        )
        if not _meta:
            _meta = SQLAlchemyInterfaceOptions(cls)
        _meta.model = model
        _meta.registry = registry
        connection = Connection.create_type(
            "{}Connection".format(cls.__name__), node=cls)
        assert issubclass(connection, Connection), (
            "The connection must be a Connection. Received {}"
        ).format(connection.__name__)
        _meta.connection = connection
        if _meta.fields:
            _meta.fields.update(sqla_fields)
        else:
            _meta.fields = sqla_fields
        super(SQLAlchemyInterface, cls).__init_subclass_with_meta__(_meta=_meta, **options)

    @classmethod
    def Field(cls, *args, **kwargs):  # noqa: N802
        return NodeField(cls, *args, **kwargs)

    @classmethod
    def node_resolver(cls, only_type, root, info, id):
        return cls.get_node_from_global_id(info, id, only_type=only_type)

    @classmethod
    def get_node_from_global_id(cls, info, global_id, only_type=None):
        try:
            node: DeclarativeMeta = one_or_none(session=info.context.get('session'), model=cls._meta.model, id=global_id)
            return node
        except Exception:
            return None

    @staticmethod
    def from_global_id(global_id):
        return global_id

    @staticmethod
    def to_global_id(type, id):
        return id

Примеры интерфейсов, примеры моделей + запросов:

class CustomNode(Node):
    class Meta:
        name = 'UuidNode'

    @staticmethod
    def to_global_id(type, id):
        return '{}:{}'.format(type, id)

    @staticmethod
    def get_node_from_global_id(info, global_id, only_type=None):
        type, id = global_id.split(':')
        if only_type:
            # We assure that the node type that we want to retrieve
            # is the same that was indicated in the field type
            assert type == only_type._meta.name, 'Received not compatible node.'

        if type == 'User':
            return one_or_none(session=info.context.get('session'), model=User, id=global_id)
        elif type == 'Well':
            return one_or_none(session=info.context.get('session'), model=Well, id=global_id)


class ControlledVocabulary(SQLAlchemyInterface):
    class Meta:
        name = 'ControlledVocabularyNode'
        model = BaseControlledVocabulary


class TrackedEntity(SQLAlchemyInterface):
    class Meta:
        name = 'TrackedEntityNode'
        model = TrackedEntityModel

class Request(SQLAlchemyObjectType):
    """Request node."""

    class Meta:
        model = RequestModel
        interfaces = (TrackedEntity,)

class User(SQLAlchemyObjectType):
    """User Node"""

    class Meta:
        model = UserModel
        interfaces = (CustomNode,)

class CvFormFieldValueType(SQLAlchemyObjectType):
    class Meta:
        model = CvFormFieldValueTypeModel
        interfaces = (ControlledVocabulary,)


common_field_kwargs = {'id': graphene.UUID(required=False), 'label': graphene.String(required=False)}

class Query(graphene.ObjectType):
    """Query objects for GraphQL API."""

    node = CustomNode.Field()
    te_node = TrackedEntity.Field()
    cv_node = ControlledVocabulary.Field()

    # Non-Tracked Entities:
    users: List[User] = SQLAlchemyConnectionField(User)
    # Generic Query for any Tracked Entity:
    tracked_entities: List[TrackedEntity] = FilteredConnectionField(TrackedEntity, sort=None, filter=graphene.Argument(TrackedEntityInput))
    # Generic Query for any Controlled Vocabulary:
    cv: ControlledVocabulary = graphene.Field(ControlledVocabulary, controlled_vocabulary_type_id=graphene.UUID(required=False),
                                              base_entry_key=graphene.String(required=False),
                                              **common_field_kwargs)
    cvs: List[ControlledVocabulary] = FilteredConnectionField(ControlledVocabulary, sort=None, filter=graphene.Argument(CvInput))

    @staticmethod
    def resolve_with_filters(info: ResolveInfo, model: Type[SQLAlchemyObjectType], **kwargs):
        query = model.get_query(info)
        log.debug(kwargs)
        for filter_name, filter_value in kwargs.items():
            model_filter_column = getattr(model._meta.model, filter_name, None)
            log.debug(type(filter_value))
            if not model_filter_column:
                continue
            if isinstance(filter_value, SQLAlchemyInputObjectType):
                log.debug(True)
                filter_model = filter_value.sqla_model
                q = FilteredConnectionField.get_query(filter_model, info, sort=None, **kwargs)
                # noinspection PyArgumentList
                query = query.filter(model_filter_column == q.filter_by(**filter_value))
                log.info(query)
            else:
                query = query.filter(model_filter_column == filter_value)
        return query

    def resolve_tracked_entity(self, info: ResolveInfo, **kwargs):
        entity: TrackedEntity = Query.resolve_with_filters(info=info, model=BaseTrackedEntity, **kwargs).one()
        return entity

    def resolve_tracked_entities(self, info, **kwargs):
        query = Query.resolve_with_filters(info=info, model=BaseTrackedEntity, **kwargs)
        tes: List[BaseTrackedEntity] = query.all()
        return tes

    def resolve_cv(self, info, **kwargs):
        cv: List[BaseControlledVocabulary] = Query.resolve_with_filters(info=info, model=BaseControlledVocabulary, **kwargs).one()
        log.info(cv)
        return cv

    def resolve_cvs(self, info, **kwargs):
        cv: List[BaseControlledVocabulary] = Query.resolve_with_filters(info=info, model=BaseControlledVocabulary, **kwargs).all()
        return cv

схема:

schema = Schema(query=Query, types=[*tracked_members, *cv_members])

Я хотел бы иметь возможность не расширять Node с помощью SQLAlchemyInterface, а скорее добавить Node обратно в список интерфейсов для TrackedEntity и ControlledVocabulary, но иметь возможность выполнять такой запрос:

query queryTracked {
      trackedEntities{
            id
            (other fields)
            ... on Request {
                    (request specific fields)
             }
       }
...