graphene-sqlalchemy icon indicating copy to clipboard operation
graphene-sqlalchemy copied to clipboard

Many to Many relationships

Open maquino1985 opened this issue 5 years ago • 6 comments

There's no documentation on how to configure m2m relationships. DJango uses a special ManyToManyField type. Can anyone provide an example with Graphene-SQLAlchemy?

maquino1985 avatar May 22 '19 14:05 maquino1985

Can you elaborate on what you mean by "configure many-to-many relationships"? They should be generated out-of-the box.

Does your referenced the type implement the Node interface?

jnak avatar May 28 '19 19:05 jnak

They do not work out of the box unfortunately.

Take for example the tables user and keyword that are connected by a many-to-many relation in user_keyword:

class CommonMixin(object):
    @declared_attr
    def __tablename__(cls):
        name = cls.__name__
        return name.lower()

    pid = Column(Integer, primary_key=True)

class UserKeyword(Base):
    __tablename__ = 'user_keyword'
    user_id = Column(Integer, ForeignKey('user.pid'), primary_key=True)
    keyword_id = Column(Integer, ForeignKey('keyword.pid'), primary_key=True)


class User(Base, CommonMixin):
    name = Column(String)
    keywords = relationship(
        "Keyword",
        secondary="user_keyword",
        back_populates="users"
    )


class Keyword(Base, CommonMixin):
    name = Column(String)
    users = relationship(
        "User",
        secondary="user_keyword",
        back_populates="keywords"
    )

Then add the schema for User

class User(SQLAlchemyObjectType):
    class Meta:
        model = models.User
        interfaces = (relay.Node,)

and the query

    all_users = SQLAlchemyConnectionField(User)

Then only these fields are available, i.e., keywords is missing:

{
  allUsers {
    edges {
      node {
        id
        pid
        name      
      }
    }    
  }
}

baderj avatar Mar 13 '20 12:03 baderj

You need to manually set the keywords field:

class Keyword(SQLAlchemyObjectType):
    class Meta:
        model = models.Keyword
        interfaces = (relay.Node, )


class User(SQLAlchemyObjectType):
    class Meta:
        model = models.User
        interfaces = (relay.Node,)
    
    keywords = graphene.List(Keyword)

Then this will work:

{
  allUsers {
    edges {
      node {
        id
        pid
        name      
        keywords {
          name
        }        
      }
    }    
  }
}

However, that won't work if you also want to add users to Keyword

baderj avatar Mar 13 '20 12:03 baderj

I'm trying to solve a similar problem, but (to extend the example) also need to go from Keyword to User. Unfortunately, I end up with a circular reference problem.

class Keyword(SQLAlchemyObjectType):
    class Meta:
        model = models.Keyword
        interfaces = (relay.Node, )

    users = graphene.List(User)     #       <--- error because User is defined later

class User(SQLAlchemyObjectType):
    ...

I'm trying to find out how to do the equivalent of a late-bind.

ianepperson avatar May 15 '20 17:05 ianepperson

To answer my own question, there's two ways to do what I need in Graphene - neither are in the docs, but they're in the code. The first is to use a lambda function, the second is to use a string. For my trivial example above, simply define users like so:

    users = graphene.List(lambda: User)

You can also use a string with a dotted reference:

    users = graphene.List('mymodule.myfile.User')

Since I prefer to just use the sqlalchemy object reference, I wrote myself a little helper:

from graphene_sqlalchemy.registry import get_global_registry


def get_model_type(model):
    registry = get_global_registry()
    return lambda: registry.get_type_for_model(model)


class Keyword(SQLAlchemyObjectType):
    ...
    users = graphene.List(get_model_type(models.User))

ianepperson avatar May 29 '20 22:05 ianepperson

You need to manually set the keywords field:

class Keyword(SQLAlchemyObjectType):
    class Meta:
        model = models.Keyword
        interfaces = (relay.Node, )


class User(SQLAlchemyObjectType):
    class Meta:
        model = models.User
        interfaces = (relay.Node,)
    
    keywords = graphene.List(Keyword)

Then this will work:

{
  allUsers {
    edges {
      node {
        id
        pid
        name      
        keywords {
          name
        }        
      }
    }    
  }
}

However, that won't work if you also want to add users to Keyword

This answer was pretty useful for me. Thanks a lot!

matheushent avatar Mar 23 '21 19:03 matheushent