Skip to main content

GINO Is Not ORM - a Python ORM on asyncpg and SQLAlchemy core.

Project description

GINO

https://img.shields.io/pypi/v/gino.svg https://img.shields.io/travis/fantix/gino.svg Documentation Status Updates

GINO - GINO Is Not ORM - is an extremely simple Python ORM, using SQLAlchemy core to define table models, and asyncpg to interact with database.

There’s been a lot of words about ORM a.k.a. Object-relational mapping - good or bad - as well as a lot of ORM libraries in Python. It is crucial to pick a most suitable one for your project, and for your team. GINO tries to stay in the middle between ORM and non-ORM, offering an extremely simple option.

GINO operates database rows with “plain old Python objects” - they are just normal Python objects, a rollback doesn’t magically change their values. Any database operations are explicit - it is crystal clear what is done underneath each GINO API. There are no dirty models, no sessions, no magic. You have concrete control to the database, through a convenient object interface. That’s it.

GINO depends on asyncpg, which means it works only for PostgreSQL and asyncio, which means Python 3 is required - actually 3.6 required for now. Based on SQLAlchemy, gate to its ecosystem is open - feel free to use e.g. Alembic to manage your schema changes. And we specially designed a few candies for the Sanic server.

Example

A piece of code is worth a thousand words:

import asyncio
from gino import Gino, enable_task_local
from sqlalchemy import Column, Integer, Unicode, cast

db = Gino()


class User(db.Model):
    __tablename__ = 'users'

    id = Column(Integer(), primary_key=True)
    nickname = Column(Unicode(), default='noname')


async def main():
    await db.create_pool('postgresql://localhost/gino')

    # Create object, `id` is assigned by database
    u1 = await User.create(nickname='fantix')
    print(u1.id, u1.nickname)  # 1 fantix

    # Retrieve the same row, as a different object
    u2 = await User.get(u1.id)
    print(u2.nickname)  # fantix

    # Update affects only database row and the operating object
    await u2.update(nickname='daisy')
    print(u2.nickname)  # daisy
    print(u1.nickname)  # fantix

    # Returns all user objects with "d" in their nicknames
    users = await User.query.where(User.nickname.contains('d')).gino.all()

    # Find one user object, None if not found
    user = await User.query.where(User.nickname == 'daisy').gino.first()

    # Execute complex statement and return command status
    status = await User.update.values(
        nickname='No.' + cast(User.id, Unicode),
    ).where(
        User.id > 10,
    ).gino.status()

    # Iterate over the results of a large query in a transaction as required
    async with db.transaction():
        async for u in User.query.order_by(User.id).gino.iterate():
            print(u.id, u.nickname)


loop = asyncio.get_event_loop()
enable_task_local(loop)
loop.run_until_complete(main())

The code explains a lot, but not everything. Let’s go through again briefly.

Declare Models

Each model maps to a database table. To define a model, you’ll need a Gino object first, usually as a global variable named db. It is actually an extended instance of sqlalchemy.MetaData, which can be used in Alembic for example. By inheriting from db.Model, you can define database tables in a declarative way as shown above:

db = Gino()

class User(db.Model):
    __tablename__ = 'users'

    id = Column(Integer(), primary_key=True)
    nickname = Column(Unicode(), default='noname')

Note that __tablename__ is required, GINO suggests singular for model names, and plural for table names. After declaration, access to SQLAlchemy columns is available on class level, allowing vanilla SQLAlchemy programming like this:

import sqlalchemy as sa

sa.select([User.nickname]).where(User.id > 10)

But on object level, model objects are just normal objects in memory. The only connection to database happens when you explicitly calls a GINO API, user.update for example. Otherwise, any changes made to the object stay in memory only. That said, different objects are isolated from each other, even if they all map to the same database row - modifying one doesn’t affect another.

Speaking of mapping, GINO automatically detects the primary keys and uses them to identify the correct row in database. This is no magic, it is only a WHERE clause automatically added to the UPDATE statement when calling the user.update method, or during User.get retrieval.

u = await User.get(1)              # SELECT * FROM users WHERE id = 1
await u.update(nickname='fantix')  # UPDATE users SET ... WHERE id = 1
u.id = 2                           # No SQL here!!
await u.update(nickname='fantix')  # UPDATE users SET ... WHERE id = 2

Under the hood, model values are stored in a dict named __values__. And the columns you defined are wrapped with special attribute objects, which deliver the __values__ to you on object level, or as column objects on class level.

Bind Database

Though optional, GINO can bind to an asyncpg database connection or pool to make life easier. The most obvious way is to create a database pool with GINO.

pool = await db.create_pool('postgresql://localhost/gino')

Once created, the pool is automatically bound to the db object, therefore to all the models too. To unplug the database, just close the pool. This API is identical to the one from asyncpg, so can it be used as a context manager too:

async with db.create_pool('postgresql://localhost/gino') as pool:
    # play with pool

Otherwise, you will need to manually do the binding:

import asyncpg

pool = await asyncpg.create_pool('postgresql://localhost/gino')
db = Gino(pool)

# or
db = Gino()
db.bind = pool

It is theoretically possible to bind to a connection object, but this scenario is not normally well tested. And as stated in the beginning, it is possible to use GINO without binding to a database. In such case, you should manually pass asyncpg pool or connection object to GINO APIs as the bind keyword argument:

import asyncpg
conn = await asyncpg.connect('postgresql://localhost/gino')
user = await User.get(3, bind=conn)

At last, GINO can be used to only define models and translate SQLAlchemy queries into SQL with its builtin asyncpg dialect:

query, params = db.compile(User.query.where(User.id == 3))
row = await conn.fetchval(query, *params)

Execute Queries

There are several levels of API available for use in GINO. On model objects:

await user.update(nickname='fantix')
await user.delete()

On model class level, to operate objects:

user = await User.create(nickname='fantix')
user = await User.get(9)

On model class level, to generate queries:

query = User.query.where(User.id > 10)
query = User.select('id', 'nickname')
query = User.update.values(nickname='fantix').where(User.id = 6)
query = User.delete.where(User.id = 7)

On query level, GINO adds an extension gino to run query in place:

users = await query.gino.all()
user = await query.gino.first()
user_id = await query.gino.scalar()

These query APIs are simply delegates to the concrete ones on the Gino object:

users = await gino.all(query)
user = await gino.first(query)
user_id = await gino.scalar(query)

If the database pool is created by db.create_pool, then such APIs are also available on the pool object and connection objects:

async with db.create_pool('...') as pool:
    users = await pool.all(query)
    user = await pool.first(query)
    user_id = await pool.scalar(query)

    async with pool.acquire() as conn:
        users = await conn.all(query)
        user = await conn.first(query)
        user_id = await conn.scalar(query)

Transaction and Context

In normal cases when db is bound to a pool, you can start a transaction through db directly:

async with db.transaction() as (conn, tx):
    # play within a transaction

As you can see from the unpacked arguments, db.transaction() acquired a connection and started a transaction in one go. It is identical to do it separately:

async with db.acquire() as conn:
    async with conn.transaction() as tx:
        # play within a transaction

Please note, there is no db.release to return the connection to the pool, thus you cannot do conn = await db.acquire(). Using async with is the only way, and the reason is about context.

Because GINO offers query APIs on not only connections but also model classes and objects and even query objects, it would be too much trouble passing connection object around when dealing with transactions. Therefore GINO offers an optional feature to automatically manage connection objects, by enabling a builtin task local hack before any tasks are created:

from gino import enable_task_local
enable_task_local()

This switch creates a local storage for each coroutine, where db.acquire() shall store the connection object. Hence executions within the acquire context will be able to make use of the same connection right in the local storage. Furthermore, nested db.acquire() will simply return the same connection. This allows db.transaction() to be nested in the same way that asyncpg conn.transaction() does it - to use database save points.

async with db.transaction() as (conn1, tx1):      # BEGIN
    async with db.transaction() as (conn2, tx2):  # SAVEPOINT ...
        assert conn1 == conn2

If nested transactions or reused connections are not expected, you can explicitly use db.acquire(reuse=False) or db.transaction(reuse=False) to borrow new connections from the pool. Non-reused connections are stacked, they will be returned to the pool in the reversed order as they were borrowed. Local storage covers between different tasks that are awaited in a chain, it is theoretically safe in most cases. However it is still some sort of a hack, but it would be like this before Python officially supports task local storage one day.

Contribute

There are a few tasks in GitHub issues marked as help wanted. Please feel free to take any of them and pull requests are greatly welcome.

To run tests:

python setup.py test

Credits

Credit goes to all contributors listed in the AUTHORS file. This project is inspired by asyncpgsa, peewee-async and asyncorm. asyncpg and SQLAlchemy as the dependencies did most of the heavy lifting. This package was created with Cookiecutter and the audreyr/cookiecutter-pypackage project template.

History

0.3.0 (2017-08-07)

  • Supported __table_args__ (#12)

  • Introduced task local to manage connection in context (#19)

  • Added query.gino extension for in-place execution

  • Refreshed README (#3)

  • Adopted PEP 487 (Contributed by Tony Wang in #17 #27)

  • Used weakref on __model__ of table and query (Contributed by Tony Wang)

  • Delegated asyncpg timeout parameter (Contributed by Neal Wang in #16 #22)

0.2.3 (2017-08-04)

  • Supported any primary key (Contributed by Tony Wang in #11)

0.2.2 (2017-08-02)

  • Supported SQLAlchemy result processor

  • Added rich support on JSON/JSONB

  • Bug fixes

0.2.1 (2017-07-28)

  • Added update and delete API

0.2.0 (2017-07-28)

  • Changed API, no longer reuses asyncpg API

0.1.1 (2017-07-25)

  • Added db.bind

  • API changed: parameter conn renamed to optional bind

  • Delegated asyncpg Pool with db.create_pool

  • Internal enhancement and bug fixes

0.1.0 (2017-07-21)

  • First release on PyPI.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gino-0.3.0.tar.gz (28.0 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page