ormar

repository·master·Indexed 23 days ago

https://github.com/ormar-orm/ormar

An asynchronous mini ORM for Python designed for the FastAPI ecosystem. It integrates Pydantic for data validation and SQLAlchemy Core for query building, supporting Postgres, MySQL, and SQLite. Key features include single model maintenance (eliminating separate Pydantic and ORM models), async-first design, and support for complex queries, aggregations, and model signals. Version 0.26.0.

Tokens
50.2K
Snippets
110
Records
251
Agent score
77%

What's inside ormar

  1. Overview of ormar

    master

    ormar is an asynchronous mini ORM for Python that supports Postgres, MySQL, and SQLite. It is designed to be used directly as request and response models with async frameworks like fastapi and starlette.

    Key features:

    • Async-first: Built for asynchronous frameworks.
    • Single Model Maintenance: Uses Pydantic for data validation, meaning you don't need to maintain separate Pydantic models and ORM models (like SQLAlchemy or Peewee).
    • Ecosystem Support: Supported by libraries such as fastapi-crudrouter and fastapi-pagination.
  2. Filter and sort data in Ormar

    master

    Ormar provides several methods to filter (SQL WHERE clause) and sort (SQL ORDER BY clause) data using a QuerySet or QuerysetProxy.

    Filtering Methods

    • filter(*args, **kwargs) -> QuerySet: Returns a QuerySet containing objects that match the criteria.
    • exclude(*args, **kwargs) -> QuerySet: Returns a QuerySet containing objects that do not match the criteria. Multiple arguments are joined as a union of conditions (e.g., exclude(name='John', age__gte=35) becomes WHERE NOT (name='John' AND age >= 35)).
    • get(*args, **kwargs) -> Model: Returns a single model instance matching the criteria.
    • get_or_none(*args, **kwargs) -> Optional[Model]: Returns a single model instance or None if no match is found.
    • get_or_create(_defaults: Optional[dict[str, Any]] = None, *args, **kwargs) -> tuple[Model, bool]: Returns a tuple of (instance, created_boolean).
    • all(*args, **kwargs) -> list[Optional[Model]]: Returns a list of all matching instances.

    Sorting Methods

    • order_by(columns: Union[list, str, OrderAction]) -> QuerySet: Sorts the results by the specified columns.
  3. What is QuerySetProxy and how does it work?

    master

    When you access a ManyToMany field or a ReverseForeignKey directly on a model instance, Ormar returns a QuerySetProxy.

    This proxy provides a subset of the standard QuerySet API, allowing you to filter, create, or select related models directly from the parent model without needing to use the .objects attribute. For example, instead of post.categories.objects.all(), you use post.categories.all().

    Important Behavior: In-place Modification Querying related models via the proxy cleans the list of related models already loaded on the parent model. If you call a method that returns data (like get(), all(), first(), or get_or_create()), the parent model's internal list of related models is replaced by the result of that query.

    Example: If post.categories contains 4 items and you call post.categories.limit(2).all(), the post.categories list will now only contain 2 items.

  4. Access Pydantic model attributes from an Ormar Model

    master

    Since all Model classes inherit from pydantic.BaseModel, you can access all standard Pydantic attributes and methods. This allows you to inspect the schema, such as listing the model's fields.

    Note that for primary key fields like id (which defaults to autoincrement=True), the field is often treated as optional in the Pydantic schema.

  5. Detect column type changes in Alembic migrations

    master

    By default, Alembic's --autogenerate does not detect changes to column types (e.g., changing ormar.String(max_length=20) to ormar.String(max_length=50) or modifying timezone settings).

    To enable detection of these changes, pass compare_type=True to the context.configure() method in both run_migrations_offline() and run_migrations_online() within your env.py file.

    context.configure(
        connection=connection,
        target_metadata=target_metadata,
        user_module_prefix='sa.',
        compare_type=True,
    )
  6. Use different syntaxes for fields() and exclude_fields()

    master

    The fields() and exclude_fields() methods accept several input formats to define column selection:

    1. String/List/Set: Simple paths like 'id' or ['id', 'name'].
    2. Dictionary with Ellipsis (...):
      • Use a key with ... to include a whole nested model: {'manufacturer': ...}.
      • Use nested dictionaries for specific sub-fields: {'manufacturer': {'name': ...}}.
    3. Dictionary with Sets: To specify fields at the last nesting level, use a set: {'manufacturer': {'name', 'founded'}}.
    4. Chaining: Calling fields() or exclude_fields() multiple times accumulates the selection.
  7. How nested transactions work with savepoints

    master

    Ormar supports nested transactions using SQLAlchemy savepoints. Transactions are managed as task-local state using context variables.

    When you nest a database.transaction() block inside another, the inner block creates a savepoint. If the inner block fails (e.g., an exception is raised), only the operations within that inner block are rolled back to the savepoint, allowing the outer transaction to continue and potentially commit its own changes.

    async def create_multiple_authors_with_books():
        async with database:
            # Outer transaction
            async with database.transaction():
                author1 = await Author.objects.create(name="Stephen King")
    
                # Nested transaction (uses savepoint)
                try:
                    async with database.transaction():
                        book1 = await Book.objects.create(
                            title="The Shining",
                            author=author1
                        )
                        # Simulate an error
                        raise ValueError("Something went wrong!")
                except ValueError:
                    # Inner transaction is rolled back to savepoint
                    # author1 is still in the outer transaction
                    pass
    
                # Continue with outer transaction
                author2 = await Author.objects.create(name="J.K. Rowling")
                book2 = await Book.objects.create(
                    title="Harry Potter",
                    author=author2
                )
                # author1, author2, and book2 are committed
                # book1 was rolled back
  8. Configure reverse relations in ManyToMany

    master

    By default, ormar automatically registers a reverse relation on the target model. The default name is the lowercase version of the source model name followed by 's' (e.g., Post.categories creates Category.posts).

    Customizing the reverse name

    You can override this using the related_name parameter:

    categories: Optional[Union[Category, list[Category]]] = ormar.ManyToMany(
        Category, through=PostCategory, related_name="new_categories"
    )

    Warning: If you define multiple ManyToMany relations to the same target model, you must provide a related_name for all but one (or all) of them to avoid naming collisions.

  9. Perform create, get_or_create, and update_or_create on related models via QuerysetProxy

    master

    When accessing a ManyToMany field or a ReverseForeignKey, Ormar returns a QuerysetProxy. This proxy allows you to perform creation and update operations directly from the perspective of the relation.

    • QuerysetProxy.create(**kwargs): Creates a new related object from the other side of the relation.
    • QuerysetProxy.get_or_create(_defaults=None, **kwargs): Queries or creates a related object.
    • QuerysetProxy.update_or_create(**kwargs): Updates or creates a related object.

    This allows you to filter and manage related models directly through the parent model's attribute.

  10. Handle ForeignKey relations in model inheritance

    master

    When using inheritance with ForeignKey relations, you must ensure that the related_name is unique across all models that reference the parent.

    If you do not provide a related_name, ormar automatically calculates one using the child model's name. However, if you provide a manual related_name in a parent class, it will be overwritten on the related model side by every child class unless you handle it.

    To avoid collisions, you have two options:

    1. Redefine the field in child models: Manually provide a unique related_name in each subclass.
    2. Use ormar's auto-adjustment: Ormar automatically adjusts the related_name for child models by appending the child model's table name to the original related_name (e.g., original_name_childtablename).
    # parent model - needs to be abstract
    class Car(ormar.Model):
        ormar_config = base_ormar_config.copy(abstract=True)
    
        id: int = ormar.Integer(primary_key=True)
        name: str = ormar.String(max_length=50)
        owner: Person = ormar.ForeignKey(Person)
        # manual related_name required if multiple relations to same model exist
        co_owner: Person = ormar.ForeignKey(Person, related_name="coowned")
    
    class Bus(Car):
        ormar_config = base_ormar_config.copy(tablename="buses")
        # Redefining the field to provide a cleaner related_name
        owner: Person = ormar.ForeignKey(Person, related_name="buses")
        max_persons: int = ormar.Integer()
  11. Use Non-Database Pydantic Fields

    master

    You can define standard Pydantic fields (using pydantic.Field) within an ormar Model. These fields are part of the Pydantic model (subject to validation, appearing in model_dump()), but they are not saved to the database and are ignored by migrations and database operations.

    Important Constraints:

    • Non-database fields must always be Optional to avoid database load validation failures.
    • They are useful for passing extra data through FastAPI requests/responses or for calculated values.
  12. Handle ManyToMany relations in model inheritance

    master

    Inheriting ManyToMany relations requires special handling because each child model needs its own unique Through model to link the tables.

    Key Behaviors:

    • Automatic Through Model Cloning: Ormar automatically clones the Through model for each child class, appending the child class name to the new model name and using the child's table name for the new through table.
    • Database Impact: Each subclass of a model with a ManyToMany relation generates a new database table. You must ensure these new through tables are created via migrations (e.g., Alembic) or manual SQL.
    • Naming: The related_name is automatically adjusted using the pattern: original_related_name + _ + child_table_name.

    Warning: If you convert an existing model into an abstract parent model, you may lose access to the original through table data if you do not migrate carefully, as ormar will switch to using the new cloned through tables.

    # through model
    class PersonsCar(ormar.Model):
        ormar_config = base_ormar_config.copy(tablename="cars_x_persons")
    
    # parent model
    class Car2(ormar.Model):
        ormar_config = base_ormar_config.copy(abstract=True)
    
        id: int = ormar.Integer(primary_key=True)
        owner: Person = ormar.ForeignKey(Person, related_name="owned")
        co_owners: list[Person] = ormar.ManyToMany(
            Person, through=PersonsCar, related_name="coowned"
        )
    
    # child model
    class Bus2(Car2):
        ormar_config = base_ormar_config.copy(tablename="buses2")
        max_persons: int = ormar.Integer()
    
    # Resulting through table for Bus2 will be 'cars_x_persons_buses2'