Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mysql support #685

Merged
merged 29 commits into from
Sep 17, 2020
Merged

mysql support #685

merged 29 commits into from
Sep 17, 2020

Conversation

wwwjfy
Copy link
Member

@wwwjfy wwwjfy commented May 26, 2020

A workable version based on master before baked queries.

It's a "painful" journey patching the existing system.

The main pain points:

  • MySQL doesn't support RETURNING
  • aiomysql doesn't support PREPARE
  • Syntax differences
  • Hard to tweak current test cases to satisfy both MySQL and PostgreSQL. I ended up having a new mysql_tests suite after spending too much time to make it compatible and failed.

The code is structured catering for PostgreSQL. It'll be easier if we have a generic implementation in 2.0 integrating with other databases like SQLite

Refs #381

@wwwjfy wwwjfy requested a review from fantix May 26, 2020 19:01
@fantix
Copy link
Member

fantix commented May 26, 2020

wow!

@wwwjfy
Copy link
Member Author

wwwjfy commented May 27, 2020

"fixed" codacy by excluding mysql_tests/**

@wwwjfy
Copy link
Member Author

wwwjfy commented May 27, 2020

The warning seems related to pylint-dev/pylint#2315
It's defined in its ancestor class, and line 461 is a comment 🤷🏻‍♀️

@xnuinside
Copy link
Contributor

xnuinside commented May 27, 2020

wow, I just come with questions about plans to support different databases and see this :) very cool

@woostundy
Copy link

Cool !
Any fresh developments ?

@wwwjfy
Copy link
Member Author

wwwjfy commented Jun 21, 2020

Sync'ed with latest master and added bakery.

Note: aiomysql doesn't support PREPARE at the moment.

@wwwjfy wwwjfy changed the title [WIP] mysql support mysql support Jun 21, 2020
Copy link
Member

@fantix fantix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job! I think we could merge this to master first, release 1.1b2 and address the remaining issues in separate PRs.

# noinspection PyArgumentList
query = query.returning(*cols)

async def _execute_and_fetch(conn, query):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, MySQL is really uneasy. The only concern for this method is, we're issuing 2 queries but leaving the transaction handling to the user. I'm not 100% sure but we may need to either wrap this in a (sub-)transaction or use CTEs - I think this is also tied to the "auto-commit" issue, please see above/below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about it, and I can't find a strong reason to do subtransaction.

BTW, How SQLAlchemy handles this is to mark the object as dirty and run select next it's accessed. This is not I would like to use.


# noinspection PyUnreachableCode
async def test_acquire_timeout():
e = await create_engine(MYSQL_URL, minsize=1, maxsize=1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we unify the parameter naming to min_size/max_size by adding an adapter in the dialect/pool? In GINO2 I think we'll give up using the native asyncpg/aiomysql pools and switch to the native SQLAlchemy pool, which has only pool_size and max_overflow. For transition, I think we could support all 4 parameters in GINO 1.4 (version number to match SQLAlchemy 1.4/2.0, as well as leaving some space for GINO 1.2/1.3 to add features on top of SQLAlchemy 1.3 in the future if any) and it might be easier if min_size/max_size is consistent between pg and mysql.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decision to make here is how to manage users' expectation. The parameters will be passed to the used engine. Shall we use the same or we want to customize some common ones.
I don't have a preferred answer now.

import gino

db1 = gino.Gino()
await db1.set_bind(MYSQL_URL, autocommit=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For autocommit: looks like this is a difference between asyncpg and DB-API. asyncpg has no concept of autocommit - any queries are executed in its own transaction and committed if there is no explicit transaction like a leading BEGIN; (similar to autocommit=True but SQLAlchemy is removing autocommit in 2.0). This is actually the nature of the PostgreSQL binary protocol. As GINO < 1.1 is very asyncpg-specific, all APIs are assuming "auto-commit" - statements are simply "committed" when executed, unless they are wrapped in an explicit transaction.

I'm wondering if the MySQL protocol has similar behaviors or not - even though both psycopg2 and pymysql provided the (simulated) "non-auto-commit" feature. Either way, in the current GINO2 WIP, I'm using the "driver-level auto-commit" to keep the GINO API be consistent with "auto-commit", so maybe we should do the same for MySQL in GINO 1.1, so that users don't have to specify autocommit=True for GINO to behave consistently.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't taken a closer look, but per MySQL doc, it's enabled by default. I'm not sure why it's not the case here. It can be caused by aiomysql.

I set it here just for easier testing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's set to False by aiomysql. I think we can set it to None which means to use server default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix! I'm drafting a doc about autocommit and SQLAlchemy 2.0, hopefully that could provide more ideas.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oleeks
Copy link

oleeks commented Sep 11, 2020

Error again after adding two fields;
After looking at the test cases and other questions, I found that it supports datetime.now, but I made a mistake.

from fastapi import APIRouter

import shortuuid
from sqlalchemy import Column, String

router = APIRouter()


class Posts(db.Model):
    __tablename__ = "posts"
    id = Column(String(100), primary_key=True, default=shortuuid.uuid)
    username = Column(String(50))
    created = Column(DATETIME, default=datetime.now)
    updated =Column(DATETIME, default=datetime.now)

@router.post('/add')
async def add_posts():
    p = await Posts.create(username='fantix')
2020-09-11 16:10:05,537 INFO gino.engine._SAEngine INSERT INTO posts (id, username, created, updated) VALUES (%s, %s, %s, %s)
2020-09-11 16:10:05,537 INFO gino.engine._SAEngine ('Bjq5NMnSaaJXLrd9s7pPB5', 'fantix', datetime.datetime(2020, 9, 11, 16, 10, 5, 537142), datetime.datetime(2020, 9, 11, 16, 10, 5, 537142))
2020-09-11 16:10:05,537 INFO gino.engine._SAEngine COMMIT
2020-09-11 16:10:05,540 INFO gino.engine._SAEngine SELECT posts.id, posts.username, posts.created, posts.updated 
FROM posts 
WHERE posts.id = %s AND posts.username = %s AND posts.created = %s AND posts.updated = %s
2020-09-11 16:10:05,540 INFO gino.engine._SAEngine ('Bjq5NMnSaaJXLrd9s7pPB5', 'fantix', datetime.datetime(2020, 9, 11, 16, 10, 5, 537142), datetime.datetime(2020, 9, 11, 16, 10, 5, 537142))
INFO:     127.0.0.1:30712 - "POST /add HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 388, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\fastapi\applications.py", line 179, in __call__
    await super().__call__(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\applications.py", line 111, in __call__
    await self.middleware_stack(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\middleware\errors.py", line 181, in __call__
    raise exc from None
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\middleware\errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\gino_starlette.py", line 86, in __call__
    await self.app(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\exceptions.py", line 82, in __call__
    raise exc from None
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\exceptions.py", line 71, in __call__
    await self.app(scope, receive, sender)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\routing.py", line 566, in __call__
    await route.handle(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\routing.py", line 227, in handle
    await self.app(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\routing.py", line 41, in app
    response = await func(request)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\fastapi\routing.py", line 183, in app
    dependant=dependant, values=values, is_coroutine=is_coroutine
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\fastapi\routing.py", line 133, in run_endpoint_function
    return await dependant.call(**values)
  File "C:/Users/Admin/PycharmProjects/untitled2/post.py", line 24, in add_posts
    p = await Posts.create(username='fantix')
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\gino\crud.py", line 445, in _create_without_instance
    return await cls(**values)._create(bind=bind, timeout=timeout)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\gino\crud.py", line 474, in _create
    await _query_and_update(bind, self, q, list(iter(cls)), opts)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\gino\crud.py", line 854, in _query_and_update
    raise NoSuchRowError()
gino.exceptions.NoSuchRowError

@oleeks
Copy link

oleeks commented Sep 11, 2020

Error again after adding two fields;
After looking at the test cases and other questions, I found that it supports datetime.now, but I made a mistake.

from fastapi import APIRouter

import shortuuid
from sqlalchemy import Column, String

router = APIRouter()


class Posts(db.Model):
    __tablename__ = "posts"
    id = Column(String(100), primary_key=True, default=shortuuid.uuid)
    username = Column(String(50))
    created = Column(DATETIME, default=datetime.now)
    updated =Column(DATETIME, default=datetime.now)

@router.post('/add')
async def add_posts():
    p = await Posts.create(username='fantix')
2020-09-11 16:10:05,537 INFO gino.engine._SAEngine INSERT INTO posts (id, username, created, updated) VALUES (%s, %s, %s, %s)
2020-09-11 16:10:05,537 INFO gino.engine._SAEngine ('Bjq5NMnSaaJXLrd9s7pPB5', 'fantix', datetime.datetime(2020, 9, 11, 16, 10, 5, 537142), datetime.datetime(2020, 9, 11, 16, 10, 5, 537142))
2020-09-11 16:10:05,537 INFO gino.engine._SAEngine COMMIT
2020-09-11 16:10:05,540 INFO gino.engine._SAEngine SELECT posts.id, posts.username, posts.created, posts.updated 
FROM posts 
WHERE posts.id = %s AND posts.username = %s AND posts.created = %s AND posts.updated = %s
2020-09-11 16:10:05,540 INFO gino.engine._SAEngine ('Bjq5NMnSaaJXLrd9s7pPB5', 'fantix', datetime.datetime(2020, 9, 11, 16, 10, 5, 537142), datetime.datetime(2020, 9, 11, 16, 10, 5, 537142))
INFO:     127.0.0.1:30712 - "POST /add HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 388, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\fastapi\applications.py", line 179, in __call__
    await super().__call__(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\applications.py", line 111, in __call__
    await self.middleware_stack(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\middleware\errors.py", line 181, in __call__
    raise exc from None
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\middleware\errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\gino_starlette.py", line 86, in __call__
    await self.app(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\exceptions.py", line 82, in __call__
    raise exc from None
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\exceptions.py", line 71, in __call__
    await self.app(scope, receive, sender)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\routing.py", line 566, in __call__
    await route.handle(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\routing.py", line 227, in handle
    await self.app(scope, receive, send)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\starlette\routing.py", line 41, in app
    response = await func(request)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\fastapi\routing.py", line 183, in app
    dependant=dependant, values=values, is_coroutine=is_coroutine
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\fastapi\routing.py", line 133, in run_endpoint_function
    return await dependant.call(**values)
  File "C:/Users/Admin/PycharmProjects/untitled2/post.py", line 24, in add_posts
    p = await Posts.create(username='fantix')
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\gino\crud.py", line 445, in _create_without_instance
    return await cls(**values)._create(bind=bind, timeout=timeout)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\gino\crud.py", line 474, in _create
    await _query_and_update(bind, self, q, list(iter(cls)), opts)
  File "C:\Users\Admin\.virtualenvs\untitled2-21uViUAW\lib\site-packages\gino\crud.py", line 854, in _query_and_update
    raise NoSuchRowError()
gino.exceptions.NoSuchRowError

Just convert datetime.now into a string 。
like

def time2str(sft='%Y-%m-%d %H:%M:%S'):
    return  datetime.now()strftime(sft)

class Posts(db.Model):
    __tablename__ = "posts"
    id = Column(String(100), primary_key=True, default=shortuuid.uuid)
    username = Column(String(50))
    created = Column(DATETIME, default=time2str)
    updated =Column(DATETIME, default=time2str)

@wwwjfy
Copy link
Member Author

wwwjfy commented Sep 11, 2020

Thanks for review. I plan to do cleanup, check autocommit and the errors raised above this weekend.

@wwwjfy
Copy link
Member Author

wwwjfy commented Sep 13, 2020

@oleeks Thanks for the report. It's fixed now.
The reason is that MySQL doesn't keep the fractional part of a date by default, i.e. the millisecond part, causing the condition can't be satisfied. The fix is to use primary key only to fetch the inserted row. It'll still fail if there is no primary key of the table, or datetime is the primary key.
I don't have a good solution. I see SQLAlchemy has the same problem.
I'm fine to have it as a known issue, because it's not a practical db design anyway.

None passing to aiomysql means it'll respect the server setup for
autocommit.
@python-gino python-gino deleted a comment from fantix Sep 13, 2020
@python-gino python-gino deleted a comment from fantix Sep 13, 2020
@oleeks
Copy link

oleeks commented Sep 14, 2020

it's me again .....

When updating the same data concurrently, "NoSuchRowError" appears. Is there any better suggestion?

ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/uvicorn/protocols/http/httptools_impl.py", line 390, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/fastapi/applications.py", line 179, in __call__
    await super().__call__(scope, receive, send)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/starlette/applications.py", line 111, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/starlette/middleware/errors.py", line 181, in __call__
    raise exc from None
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/starlette/middleware/errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/starlette/middleware/cors.py", line 78, in __call__
    await self.app(scope, receive, send)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/gino_starlette.py", line 86, in __call__
    await self.app(scope, receive, send)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/starlette/exceptions.py", line 82, in __call__
    raise exc from None
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/starlette/exceptions.py", line 71, in __call__
    await self.app(scope, receive, sender)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/starlette/routing.py", line 566, in __call__
    await route.handle(scope, receive, send)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/starlette/routing.py", line 227, in handle
    await self.app(scope, receive, send)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/starlette/routing.py", line 41, in app
    response = await func(request)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/fastapi/routing.py", line 182, in app
    raw_response = await run_endpoint_function(
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/fastapi/routing.py", line 133, in run_endpoint_function
    return await dependant.call(**values)
  File "/home/lh/works/admins/admin/src/routes/frontend/order.py", line 83, in create_order
    await account.update(balance=new_balance).apply()
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/gino/crud.py", line 173, in apply
    await _query_and_update(
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/gino/crud.py", line 858, in _query_and_update
    row = await _execute_and_fetch(conn, query)
  File "/home/lh/miniconda3/envs/admin/lib/python3.8/site-packages/gino/crud.py", line 828, in _execute_and_fetch
    raise NoSuchRowError()
gino.exceptions.NoSuchRowError

@wwwjfy
Copy link
Member Author

wwwjfy commented Sep 14, 2020

@oleeks is it related to concurrent updates? could you share the model and code you're using?
I can't tell if this can be caused by concurrent requests.

@oleeks
Copy link

oleeks commented Sep 14, 2020

@oleeks is it related to concurrent updates? could you share the model and code you're using?
I can't tell if this can be caused by concurrent requests.

okey.I am operating on business code,Initiating two operations at the same time, logically speaking, there should be no problems;
probably like this;

class Account(db.Model):
    __tablename__ = "account"
    id = db.Column(db.String(22),
                   default=shortuuid.uuid,
                   primary_key=True,
                   unique=True)
    uid = Column(String(22), nullable=False)
    balance = Column(DECIMAL(5, 2), nullable=False, default=0)


from fastapi import APIRouter
router = APIRouter()

@router.post('/create-order')
async def create_order():
    account = await Account.query.where(Account.uid == uid
                                        ).gino.first()
    # omit ....
   new_balance = ( account.balance- Decimal('0.01')).quantize(Decimal('0.00'))
   await account.update(balance=new_balance).apply()
import httpx
import asyncio


async def http(client):
    url = "http://127.0.0.1:8000/v1/create-order"
    headers = {
        "Accept": "application/json, text/plain, */*",
    }
    result = await client.post(url=url,headers=headers, timeout=30)
    print(result.text)


async def main():
    async with httpx.AsyncClient() as client:
        task_list = []
        for _ in range(2):
            req = http(client)
            task = asyncio.create_task(req)
            task_list.append(task)

        await asyncio.gather(*task_list)


if __name__ == "__main__":
    asyncio.run(main())

@oleeks
Copy link

oleeks commented Sep 14, 2020

@oleeks is it related to concurrent updates? could you share the model and code you're using?
I can't tell if this can be caused by concurrent requests.

Is the transaction enabled by default, or does it need to be added? What to do with additional addition

In MySQL result, affected rows means the number of rows that get
updated, but not the number of matched rows. So this can't be use as an
indicator that if the rows actually exist.
@wwwjfy
Copy link
Member Author

wwwjfy commented Sep 14, 2020

It's fixed. The reason is I misunderstood the meaning of affected_rows in the result of MySQL, which means how many rows actually get updated. So in your second request, the value is the same, causing the affected_rows to be 0.

For the transaction question, in MySQL, autocommit is set to true by default. If you need transactions, you'll need to explicitly wrap the block in a transaction.

@oleeks Thanks for your experiments for this PR and bug reports! Ideally I should apply it to my own project but sadly I don't have one now, so I miss a lot of cases. Really appreciate your help!

@python-gino python-gino deleted a comment from fantix Sep 14, 2020
@oleeks
Copy link

oleeks commented Sep 15, 2020 via email

@wwwjfy
Copy link
Member Author

wwwjfy commented Sep 15, 2020

@oleeks right, lock here is probably not a good idea.
You could take a look at transaction isolation level, or you can try to use optimistic lock with a version, where the affected rows can be useful.

@oleeks
Copy link

oleeks commented Sep 15, 2020

@oleeks right, lock here is probably not a good idea.
You could take a look at transaction isolation level, or you can try to use optimistic lock with a version, where the affected rows can be useful.
I searched, optimistic lock is used like this in django

goods = GoodsInfo.objects.filter(id=goods_id).first()
result = GoodsInfo.objects.filter(id=goods.id, stock=goods.origin_stock).update(stock=goods.origin_stock - count)

I don’t see any relevant examples in the gino documentation. Is this how to use it? Doesn't feel right

from sqlalchemy import and_
account = await Account.query.where(Account.uid == uid).gino.first()
await Account.update.values(balance=new_balance).where(and_(Account.id == account.id,
        Account.balance == balance)).gino.status()

@wwwjfy
Copy link
Member Author

wwwjfy commented Sep 15, 2020

isolation level is easier, but it needs to be set for each session if this is not applicable for other transactions.

For optimistic lock, I'd suggest to use a version instead of balance. To use it we'd need some improvements to show affected rows.
For example.

class Account:
  version = Column(Integer, nullable=False)

account = await Account.query.where(Account.uid == uid).gino.first()
await Account.update.values(balance=new_balance, version=account.version+1).where(and_(Account.id == account.id,
        Account.version == account.version)).gino.status()

The second update can't match Account.version == account.version because of the increment.

@oleeks
Copy link

oleeks commented Sep 16, 2020

isolation level is easier, but it needs to be set for each session if this is not applicable for other transactions.

For optimistic lock, I'd suggest to use a version instead of balance. To use it we'd need some improvements to show affected rows.
For example.

class Account:
  version = Column(Integer, nullable=False)

account = await Account.query.where(Account.uid == uid).gino.first()
await Account.update.values(balance=new_balance, version=account.version+1).where(and_(Account.id == account.id,
        Account.version == account.version)).gino.status()

The second update can't match Account.version == account.version because of the increment.

@wwwjfy thaks

@wwwjfy
Copy link
Member Author

wwwjfy commented Sep 17, 2020

I'll merge this first. Feel free to raise issues 🙏

@wwwjfy wwwjfy merged commit a703ce2 into python-gino:master Sep 17, 2020
@wwwjfy wwwjfy deleted the mysql-support branch September 17, 2020 04:11
@xnuinside
Copy link
Contributor

xnuinside commented Apr 15, 2021

Does MySQL works ok with Gino? Or exists any reason why info about it not in README.md in repo?:) and not in docs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants