proteanhq / protean

Pragmatic Framework for Ambitious Applications
https://docs.proteanhq.com/
BSD 3-Clause "New" or "Revised" License
20 stars 10 forks source link

Support for Referenced Objects #75

Closed subhashb closed 5 years ago

subhashb commented 5 years ago

Proposal:

Support relationships via the following two concepts:

Foreign Keys

Virtual Linkages

The thought behind these concepts is to:

subhashb commented 5 years ago

Example:

class Account(Entity):
    posts = field.Linked(Post, via='Post.author_id', many=True)   # Virtual Field
    created_by = field.Integer(references='account.id')  # Self-referencing Foreign Key attribute
    updated_by = field.Integer(references='account.id')

class Post(Entity):
    comments = field.Linked(Comment, via='Comment.post_id', many=True)  # Virtual Field
    author = field.Linked(Author, via='created_by')  # Virtual Field, created on an attribute in the same entity
    created_by = field.Integer(references='account.id')
    updated_by = field.Integer(references='account.id')

class Comment(Entity):
    post_id = field.Integer(references='post.id')  # Foreign Key attribute
    post = field.Linked(Post, via='post_id')  # Virtual Field
    created_by = field.Integer(references='account.id')
    updated_by = field.Integer(references='account.id')
subhashb commented 5 years ago

Pros of automatic two-way field creation based on relationship definitions:

Cons:

subhashb commented 5 years ago

Also, all references are lazy-loaded whenever accessed. We can probably give an eager_load option in the future.

abhishek-ram commented 5 years ago

I think your idea is good in the case of Linked field, but in the case of references its not clear what is happening. I was thinking of something like this:

class Account(Entity):
    posts = field.ReverseRelated(Post, key='created_by', many=True)   # Virtual Field
    created_by = field.Related('Account')  # Self-referencing Foreign Key attribute, will automatically create field called created_by_id
    updated_by = field.Related('Account')

class Post(Entity):
    comments = field.ReverseRelated(Comment, key='post', many=True)  # Virtual Field
    created_by = field.Related(Account) 
    updated_by = field.Related(Account) 

class Comment(Entity):
    post = field.Related(Post)  # Virtual Field
    created_by = field.Related(Account)
    updated_by = field.Related(Account)
subhashb commented 5 years ago

I think this is a good idea as well. For me, it distills the linkages even further and makes them even more explicit.

A few cons that come to mind:

What say you?

subhashb commented 5 years ago

Notes:

subhashb commented 5 years ago

A different approach, combining your input and further discovery:

class Account(Entity):
    username = field.String(max_length=50)
    password = field.String(max_length=255)
    last_logged_in = field.DateTime(default=datetime.utcnow)
    author = field.HasOne(Author)
    created_by = field.BelongsTo(Account)
    updated_by = field.BelongsTo(Account)

class Author(Entity):
    account = field.BelongsTo(Account)
    bio = field.Text()
    posts = field.HasMany(Post)

class Post(Entity):
    title = field.String(max_length=255)
    author = field.BelongsTo(Author)
    content = field.Text()
    comments = field.HasMany(Comment)
    tags = field.HasAndBelongsToMany(Tag, through=TagMap)
    created_at = field.DateTime(default=datetime.utcnow)
    updated_at = field.DateTime(default=datetime.utcnow)

class Comment(Entity):
    content = field.Text()
    post = field.BelongsTo(Post)
    created_at = field.DateTime(default=datetime.utcnow)

class Tag(Entity):
    name = field.String(max_length=50)
    posts = field.HasAndBelongsToMany(Post, through=TagMap)

class TagMap(Entity):
    tag = field.BelongsTo(Tag)
    post = field.BelongsTo(Post)
    valid_until = field.DateTime()

Example usage:

admin = Account.find_by(username='email')
account = Account.create(username='subhash', password='password', created_by=admin, updated_by=admin)
author = Author.create(account=account, bio='Blah Blah')
post1 = Post.create(title='Title Non-comprendo', author=author, content='Loren Ipsum...')
author.posts.create(title='Title Somewhat-comprendo', content='Loren Ipsum Redefined') # Single step save

tag1 = Tag.create(name='History')
tag2 = Tag.create(name='Sociology')
posts.tags.add(tag1, tag2)
posts.save() #  Two-step save

Notes:

Let me know what you think.