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
What about relationship? #13
Comments
我建议不要实现relationship, 应该优先实现标量子查询(scalar subqueries):
我的所有生产项目中都使用这一方法从关联表中获取数据,从来不使用外键关联。 |
Django中有能很优雅地使用标量子查询,例如:
|
因为 GINO 本质上是一个 SQLAlchemy core 的 asyncpg 方言,所以理论上 SQLAlchemy core 可以做到的查询,GINO 都是可以的,这里就包含了 scalar subquery,比如这种用法。 其实这里更重要的问题是,要不要把数据库关系——不管是有外键约束还是无外键约束——通过专门的代码来表示和操作,而非通过通用的 SQLAlchemy 查询语句,因为通用的 SQLAlchemy 语句 GINO 已经支持了。以及如果要做的话,怎么做,要不要强制外键约束,是否支持多对多,等等。 |
Generally we are talking about using scalar subquery and how to deal with object-level relationship. |
能否在文档中指明差异在哪里?我正在补课学习SQLAlchemy core. |
嗯,可以的,我先在这里尝试找一下。SQLAlchemy 的设计是,用尽量通用的 Clause 代码来写查询语句,然后用不同的 dialect 去翻译及适配不同的数据库,也包括同一种数据库的不同连接程序,比如 psycopg2,或者 asyncpg。因此,通过 GINO 使用 SQLAlchemy 时的差异就在于,dialect 翻译和适配之后的结果不同,比如 psycopg2 使用 |
What is the current status for this? |
Thanks for the suggestion! As the refactor of #59 and some personal distractions, relationships may have to get a lower priority for me, apologizes for that. However I do appreciate if someone would like to push it forward by PRs PoCs or even suggestions :) I’d be more than willing to review them, just need some time. |
Hey, @fantix nice work, glad to see v0.6 (aka 1.0) being tagged. |
Your help is much appreciated! I've been also thinking about how this can be done in any possible way. Looking into I think we can divide this big question into smaller ones:
First of all, I think we should not define the column (usually with foreign key constraint) for the user - that is just too implicit. Second I think user should define relationship on both models, not using the class Parent(db.Model):
__tablename__ = 'parents'
id = db.Column(db.BigInteger(), primary_key=True)
@relationship
async def children(self):
# details about how to load children? For a very conceptual example:
return await Child.query.where(Child.parent_id == self.id).gino.all()
class Child(db.Model):
__tablename__ = 'children'
id = db.Column(db.BigInteger(), primary_key=True)
parent_id = db.Column(db.ForeignKey('parents.id'))
@relationship
async def parent(self):
# details about how to load parent? For a very conceptual example:
return await Parent.get(self.parent_id) And yes I prefer not to depend on information from a foreign key constraint, and let the user decide how to get connected. For example, some users just prefer not to have any foreign key constraints at all.
Let me try to summarize the ways loading relationshipts:
Then the question becomes, should we support only one way, or several optional ways?
To get started, I think we can experiment on making this:
By the way, GINO 0.6 is 1.0b2, 0.7 will be 1.0b3, I'll hope 0.8 could be 1.0rc, then 1.0 final. |
I've made a few attempts in different directions, it looks like "unpacking rows from database into expected objective structure" might be an essential feature for CRUD. For example to achieve the same as: await User.query.gino.all() We are actually wishing to have a list of await User.query.gino.get([User]) Then await User.query.gino.get(User) And await User.query.gino.get(User.id) Or to have a list of user IDs: await User.query.gino.get([User.id]) Or tuples: await User.query.gino.get([(User.id, User.name)]) Load only a part of await User.query.gino.get([User.load('id', 'name')]) Load a many-to-one relationship: await User.outerjoin(Department).select().gino.get([User.load(dept=Department)]) Two levels of many-to-one relationships: await User.outerjoin(Department).outerjoin(Company).select().gino.get(
[User.load(dept=Department.load(company=Company))]) Adjacency list: Manager = User.alias()
await User.outerjoin(Manager, User.manager_id == Manager.id).select().gino.get(
[User.load(manager=Manager)]) Alternatively, use Manager = User.alias()
def unpacker(result):
rv = result[User]
rv.manager = result[Manager]
return rv
await User.outerjoin(Manager, User.manager_id == Manager.id).select().gino.map(unpacker) Might also have (Proposal Only) |
I fall behind a lot, but as far as I see, there are no radical changes in the relationship. So here are the two cents, hope it's not too late. I expect GINO to
The proposal LGTM, a few comments though:
Looking forward to the implementation. I just started a little project that let me try GINO in real life. |
托尼睡的够晚的啊哈哈,我大概看明白了,意见基本一致,明早详细回复 |
There's no concrete implementation yet, so it is a good time. I cannot agree more on your two expectations, I'll try to make up an example about unpacking rows into objects generated by raw textual SQL. For the two comments:
|
A bit surprised by that SQLAlchemy core is actually quite ORM-ready, so this PR isn't that tough after all. Based on this PR, it should be much easier to implement relationships. |
Just added a feature for loader to populate distinct values, works for subloaders too. That means one-to-many and many-to-many relationships can be achieved in a manual way. Example of one-to-many: class Parent(db.Model):
__tablename__ = 'parents'
id = db.Column(db.BigInteger(), primary_key=True)
def __init__(self, **kw):
super().__init__(**kw)
self._children = set()
@property
def children(self):
return self._children
@children.setter
def add_child(self, child):
self._children.add(child)
# optionally set backref here manually
child.parent = self
class Child(db.Model):
__tablename__ = 'children'
id = db.Column(db.BigInteger(), primary_key=True)
parent_id = db.Column(db.ForeignKey('parents.id'))
async def main():
query = Child.outerjoin(Parent).select()
loader = Parent.distinct(Parent.id).load(add_child=Child)
parents = await query.gino.load(loader).all()
for parent in parents:
print(f'{parent} has {len(parent.children)} children') Here the setter property in parent is unusually named to let loader work without confusion ( Example of many-to-many: class Parent(db.Model):
__tablename__ = 'parents'
id = db.Column(db.BigInteger(), primary_key=True)
def __init__(self, **kw):
super().__init__(**kw)
self._children = set()
@property
def children(self):
return self._children
@children.setter
def add_child(self, child):
self._children.add(child)
# optionally set backref here manually
child.parents.add(self)
class Child(db.Model):
__tablename__ = 'children'
id = db.Column(db.BigInteger(), primary_key=True)
def __init__(self, **kw):
super().__init__(**kw)
self._parents = set()
@property
def parents(self):
return self._parents
@parents.setter
def add_parent(self, parent):
self._parents.add(parent)
# optionally set backref here manually
parent.children.add(self)
class Association(db.Model):
__tablename__ = 'associations'
parent_id = db.Column(db.ForeignKey(Parent.id))
child_id = db.Column(db.ForeignKey(Child.id))
async def main():
query = Parent.outerjoin(Association).outerjoin(Child).select()
parents_loader = Parent.distinct(Parent.id).load(add_child=Child.distinct(Child.id))
parents = await query.gino.load(parents_loader).all()
for parent in parents:
print(f'Parent<{parent.id}> has {len(parent.children)} children: {[c.id for c in parent.children]}')
query = Child.outerjoin(Association).outerjoin(Parent).select()
children_loader = Child.distinct(Child.id).load(add_parent=Parent.distinct(Parent.id))
children = await query.gino.load(children_loader).all()
for child in children:
print(f'Child<{child.id}> has {len(child.parents)} parents: {[p.id for p in child.parents]}') By far I think the infrastructure is ready, what is left for relationship is all kinds of reasonable shortcuts for convenience. I'll leave this issue open until 0.8 is out. |
Is there anyway to use this loaders inside execution_options? for complex query with several joins |
Is what you want included in https://python-gino.readthedocs.io/en/latest/relationship.html? |
I think that it is about
Relations are: i'd like to have inside ModelOne some property with all its children and in ModelTwo to have parent property filled with ModelThree |
Then the one-to-many relationship for
Finally you just need to write the query right:
Please find the definition of |
I have found a bug when you trying to select data from subquery and add outerjoin to it(like eagerjoin strategy in sqlalchemy), Loader can't handle it properly, because part of model names can overlap, like |
@jekel If not too much trouble, could you show an example? The usage, the expected result and the actual result. I want to understand better in which situation you met this bug. About the distinct part, it's not related to this relationship issue, right? If so, could you create another pull request, to avoid confusion? Thanks :) |
@wwwjfy for now you can look in the tests part where it is covered - result from one table overlaps another - |
@jekel I've checked the test, only that it's using alias subquery while in the loader it uses the model |
@wwwjfy my use case is more complex, where i have one base query like: aliased (Model, Aggregate function), + some joins, filter and one global outerjoin, and as result i have aliased query with column names like: |
@wwwjfy i have made PR for Columns object loader support |
Thanks for those PRs. Allow me some time to investigate while I have a few doubts in mind. |
fix the issue that Alias methods like outjoin invoked Model, which caused the loader couldn't find corresponding columns.
Is the current state of relationships satisfactory for the maintainers? When I reached the docs parts on how to deal with relationships that was a major deal breaker, it's way too complex and honestly I could barely visualize how it works, it seems more complicated than just executing a raw SQL with a join. In my opinion it should be automatic on access or upon passing a parameter that the relationship should be returned. |
First of all, I believe there is room to improve the overall experience of utilizing relationships, but it shall be a v1.1.x enhancement, unlikely in v0.8.x and v1.0.x. I think it is the nature that handling relationship is complex between RDBMS and application code. Making the API simpler would have to necessarily hide some details, thus against the rule of being explicit. Actually, I do believe having raw SQL occasionally is not a bad thing in the context of using RDBMS directly - as far as you can easily load its result. Pls lmk if that explains your concerns or you have any ideas. |
@fantix In the One-to-many scenario is it possible to get a parent even if no children are pointing at him? |
@Dulex123 If you are talking about this particular one-to-many scenario, the answer is no because of the |
I'm closing this issue now, as the loader could provide manual relationship support. We're still open to any proposals or new designs to support relationships at a higher abstraction level. |
Honestly I have no idea yet.Update:
GINO now has a loader mechanism to load a result matrix into objects on need, please see examples below. It allows relationship be implemented in a primitive way. Next, we'll try to introduce some high level relationship encapsulation.
The text was updated successfully, but these errors were encountered: