strawberry icon indicating copy to clipboard operation
strawberry copied to clipboard

Full support for nested pydantic, sqlmodel and ormar models

Open gazorby opened this issue 2 years ago • 11 comments

This PR add the following features:

  • Full support for deriving nested pydantic models (including when using List, Optional, Union and ForwardRef)
  • Deriving ormar models with relationships (ForeignKey, ManyToMany, and reverse relations)
  • Deriving SQLModel models with Relationship fields
  • Strawberry types declarations don't have to follow model declaration order (eg: children can be defined before parents)
  • Add a new exclude param to the strawberry.experimental.pydantic.type decorator, allowing to include all fields while excluding some

Description

Pydantic

Nested pydantic models should works in most situations, as long as it's GraphQL typed.

Example :

class User(pydantic.BaseModel):
    name: str
    hobby: Optional[List["Hobby"]]

class Hobby(pydantic.BaseModel):
    name: str

@strawberry.experimental.pydantic.type(User, all_fields=True)
class UserType:
    pass

@strawberry.experimental.pydantic.type(Hobby, all_fields=True)
class HobbyType:
    pass

The order in which strawberry types are defined doesn't matter (doesn't have to follow pydantic models declaration order):

class Hobby(pydantic.BaseModel):
    name: str

class User(pydantic.BaseModel):
    name: str
    hobby: Hobby

@strawberry.experimental.pydantic.type(User, all_fields=True)
class UserType:
    pass

@strawberry.experimental.pydantic.type(Hobby, all_fields=True)
class HobbyType:
    pass

Ormar

Omar is an orm that uses pydantic as its base: all ormar models are pydantic models.

ForeignKey, ManyToMany and reverse relations (related fields in Django) are supported.

When using the pydantic decorator to generate a strawberry type, ormar fields will be mapped as you would expect:

class Hobby(ormar.Model):
    name: str    

class User(ormar.Model):
    name: str = ormar.String(max_length=255)
    hobby: Hobby = ormar.ForeignKey(Hobby, nullable=False)

@strawberry.experimental.pydantic.type(Hobby, all_fields=True)
class HobbyType:
    pass

@strawberry.experimental.pydantic.type(User, all_fields=True)
class UserType:
    pass

Will gives the following GraphQL schema:

type HobbyType {
  name: String!
}

type UserType {
  name: String!
  hobby: HobbyType!
}

When using all_fields=True it also includes the reverse relation users that ormar automatically created in the Hobby model:

@strawberry.experimental.pydantic.type(Hobby, all_fields=True)
class HobbyType:
    pass
type HobbyType {
  name: String!
  users: [UserType]
}

type UserType {
  name: String!
  hobby: HobbyType!
}

SQLModel

SQLModel is another pydantic-based orm, that uses SQLAlchemy to define models. All relations are defined using the Relationship field:

class Hobby(SQLModel):
    name: str
    users: List["User"] = Relationship(back_populates="hobby")

class User(SQLModel):
    name: str = Field()
    hobby: Hobby = Relationship(back_populates="users")

@strawberry.experimental.pydantic.type(Hobby, all_fields=True)
class HobbyType:
    pass

@strawberry.experimental.pydantic.type(User, all_fields=True)
class UserType:
    pass

Will translate in the following schema:

type HobbyType {
  name: String!
  users: [UserType!]!
}

type UserType {
  name: String!
  hobby: HobbyType!
}

Types of Changes

  • [ ] Core
  • [ ] Bugfix
  • [ ] New feature
  • [x] Enhancement/optimization
  • [ ] Documentation

Issues Fixed or Closed by This PR

  • #1183

Checklist

  • [x] My code follows the code style of this project.
  • [x] My change requires a change to the documentation.
  • [ ] I have updated the documentation accordingly.
  • [x] I have read the CONTRIBUTING document.
  • [x] I have added tests to cover my changes.
  • [x] I have tested the changes and verified that they work and don't break anything (as well as I can manage).

gazorby avatar Feb 10 '22 20:02 gazorby

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


  • Add Full support for deriving nested pydantic models (including when using List, Optional, Union and ForwardRef)
  • Support for deriving ormar models with relationships (ForeignKey, ManyToMany, and reverse relations)
  • Support for deriving SQLModel models with Relationship fields
  • Strawberry type declarations don't have to follow model declarations order (eg: childs can be defined before parents)
  • Add a new exclude param to the strawberry.experimental.pydantic.type decorator, allowing to include all fields while excluding some

Pydantic

GraphQL container types (List, Optional and Union) and ForwardRef are supported:

class User(pydantic.BaseModel):
    name: str
    hobby: Optional[List["Hobby"]]

class Hobby(pydantic.BaseModel):
    name: str

@strawberry.experimental.pydantic.type(User, all_fields=True)
class UserType:
    pass

@strawberry.experimental.pydantic.type(Hobby, all_fields=True)
class HobbyType:
    pass

Ormar

ForeignKey, ManyToMany and reverse relations are supported:

class Hobby(ormar.Model):
    name: str

class User(ormar.Model):
    name: str = ormar.String(max_length=255)
    hobby: Hobby = ormar.ForeignKey(Hobby, nullable=False)

@strawberry.experimental.pydantic.type(Hobby, all_fields=True)
class HobbyType:
    pass

@strawberry.experimental.pydantic.type(User, all_fields=True)
class UserType:
    pass
type HobbyType {
  name: String!
  users: [UserType]
}

type UserType {
  name: String!
  hobby: HobbyType!
}

SLQModel

SQLModel is another pydantic-based orm, that uses SQLAlchemy to define models. All relations are defined using the Relationship field:

class Hobby(SQLModel, table=True):
    name: str
    users: List["User"] = Relationship(back_populates="hobby")

class User(SQLModel, table=True):
    name: str = Field()
    hobby: Hobby = Relationship(back_populates="users")

@strawberry.experimental.pydantic.type(Hobby, all_fields=True)
class HobbyType:
    pass

@strawberry.experimental.pydantic.type(User, all_fields=True)
class UserType:
    pass
type HobbyType {
  name: String!
  users: [UserType!]!
}

type UserType {
  name: String!
  hobby: HobbyType!
}

Here's the preview release card for twitter:

Here's the tweet text:

🆕 Release (next) is out! Thanks to @gazorby for this great new feature!
Strawberry types can now be generated from SQLModel / ormar models!

Get it here 👉 https://github.com/strawberry-graphql/strawberry/releases/tag/(next)

botberry avatar Feb 10 '22 20:02 botberry

Hi @gazorby! Thanks for this PR, I'll try to take a look in the weekend <3

patrick91 avatar Feb 10 '22 21:02 patrick91

Codecov Report

Merging #1637 (6ac1045) into main (f2cc503) will decrease coverage by 0.02%. The diff coverage is 99.26%.

:exclamation: Current head 6ac1045 differs from pull request most recent head 65da6f2. Consider uploading reports for the commit 65da6f2 to get more accurate results

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1637      +/-   ##
==========================================
- Coverage   98.14%   98.12%   -0.02%     
==========================================
  Files         129      131       +2     
  Lines        4528     4649     +121     
  Branches      779      799      +20     
==========================================
+ Hits         4444     4562     +118     
- Misses         43       45       +2     
- Partials       41       42       +1     

codecov[bot] avatar Feb 10 '22 21:02 codecov[bot]

Hi @patrick91! Will update the doc accordingly once API changes have been finalized

gazorby avatar Feb 12 '22 13:02 gazorby

@gazorby thank you so much for doing this! is there any chance we can split this PR into multiple ones? it would make it easier to get things merged 😊

patrick91 avatar Mar 05 '22 05:03 patrick91

@gazorby @patrick91 I wonder how we could move this forward / split the PR if required. I've used @gazorby 's fork in a POC for a while (the Pydantic features, not ormar or SQLModel) and I feel features like the nested models delivered here are essential for most non-trivial Strawberry implementations using Pydantic.

With the (thankfully!) frequent releases of Strawberry though this is not sustainable for long as we want to uptake the latest features and releases, and some pydantic related code has also been moving again. Would be fantastic to be able to merge the functionality soon.

alexhafner avatar May 07 '22 15:05 alexhafner

Hi @alexhafner, thanks for the message, that's great to know! I might need to do some pydantic+strawberry stuff for work so I should be able to take a look at this PR :)

Are you on discord by the way? I might want to ask you some questions regarding this PR 😊

patrick91 avatar May 07 '22 15:05 patrick91

@patrick91 sure thing, I'll message you there

alexhafner avatar May 07 '22 15:05 alexhafner

Hi @alexhafner @patrick91

Sorry for being silent here, was busy on other projects. I think nested model generation is important for any advanced use-case making use of pydantic or any other model-like abstraction. I recently came through the great expedock/strawberry-sqlalchemy-mapper library that help generating strawberry types from sqlalchemy models, and forked it to add input types generation and full relay support (including pagination) to their already exisiting relay implementation.

I realized that doing the same in strawberry if this PR was merged would be quite cumbersome as it lacks some way of customizing what's happening during nested generation (eg: define a resolver that would accept a page input and return a connection). Of course we don't want to bloat more this PR by adding relay support, but maybe some bindings (decorator param?) to let strawberry users customize how nested types would be generated.

gazorby avatar May 07 '22 21:05 gazorby

Hi @alexhafner @patrick91

Sorry for being silent here, was busy on other projects.

no worries at all!

I think nested model generation is important for any advanced use-case making use of pydantic or any other model-like abstraction.

Yup, I agree 😌

I recently came throught the great expedock/strawberry-sqlalchemy-mapper library that help generating strawberry types from sqlalchemy models, and forked it to add input types generation and full relay support (including pagination) to their already exisiting relay implementation.

Did you make a PR? would be interesting to see how you implemented it

I realized that doing the same in strawbrry if this PR was merged would be quite cumbersome as it lacks some way of customizing what's happening during nested generation (eg: define a resolver that would accept a page input and return a connection). Of course we don't to bloat more this PR by adding relay support, but maybe some bindings (decorator param?) to let strawberry users customize how nested types would be generated.

Would you be able to write a summary for this?

Also what do you think of splitting this PR in multiple pieces?

patrick91 avatar May 07 '22 22:05 patrick91

Hi! Is this feature development still in active development?

ElisaKonga avatar Jul 26 '22 10:07 ElisaKonga

@gazorby a lot of great work here. Worth breaking down what can be merged straight away into a separate pr?

khairm avatar Dec 11 '22 00:12 khairm

@gazorby These are fantastic changes! Any chance we can get them merged soon?

pranav-kunapuli avatar Jan 19 '23 16:01 pranav-kunapuli

hi, I need exclude pls this PR looks great

cat-turner avatar Jan 24 '23 09:01 cat-turner

Unfortunately I have no more interest in deriving strawberry types from sqlmodel/ormar models and pydantic first class support is tracked in #2181. For anyone willing to iterate from there, initial work is at https://github.com/gazorby/strawberry/tree/ref/simpler-orm-implementation

gazorby avatar Jan 30 '23 14:01 gazorby