Replies: 2 comments 6 replies
-
Python lists are ordered (elements keep in insert order), by your description I assume you mean sorted. Right now there is no auto way. You can do it manually though.
Since it's exposing |
Beta Was this translation helpful? Give feedback.
-
So basically you want to order by additional column on through model (which is fine), but want Moreover your design specification is not well thought through and would fail in many cases. In example: alice = await Human(name="Alice").save()
spot = await Animal(name="Spot").save()
kitty = await Animal(name="Kitty").save()
noodle = await Animal(name="Noodle").save()
await alice.favoriteAnimals.add(noodle)
await alice.favoriteAnimals.add(spot)
await alice.favoriteAnimals.add(kitty)
# what if now user have animal that he hates, and will hate forever
# like a dog that attacted alice child and will be last forever
angry = await Animal(name="Angry Dog").save()
alice.favoriteAnimals.insert(99999, angry)
# Ops, python index out of bounds error (!) What if you loaded only selected animals and want to insert between them alice = await Human(name="Alice").save()
spot = await Animal(name="Spot").save()
kitty = await Animal(name="Kitty").save()
noodle = await Animal(name="Noodle").save()
max = await Animal(name="Max").save()
spike = await Animal(name="Spike").save()
await alice.favoriteAnimals.add(noodle)
await alice.favoriteAnimals.add(spot)
await alice.favoriteAnimals.add(kitty)
await alice.favoriteAnimals.add(max)
await alice.favoriteAnimals.add(spike)
# note that kitty has order 2 but index in list 0, and max has order 4 but index in list 1
await alice.favoriteAnimals.all(name__in=['Kitty', 'Spike'])
polly = await Animal(name="Polly").save()
# now you want to insert spike between them
alice.favoriteAnimals.insert(1, polly) # wrong order
alice.favoriteAnimals.insert(3, polly) # index out of bounds Select subset of models alice = await Human(name="Alice").save()
spot = await Animal(name="Spot").save()
kitty = await Animal(name="Kitty").save()
noodle = await Animal(name="Noodle").save()
max = await Animal(name="Max").save()
spike = await Animal(name="Spike").save()
await alice.favoriteAnimals.add(noodle)
await alice.favoriteAnimals.add(spot)
await alice.favoriteAnimals.add(kitty)
await alice.favoriteAnimals.add(max)
await alice.favoriteAnimals.add(spike)
# what if you select subset of related items
await alice.favoriteAnimals.all(name__in=['Noodle', 'Spot'])
polly = await Animal(name="Polly").save()
# now what should be the default?
alice.favoriteAnimals.insert(polly) # list len() is 2 <- wrong order! It would actually have to be something like Even if passing index would not cause index out of bounds error it's misleading: # as a python developer when i see it i expect to have a list with index 99999 as it's
# mimicking python list API
angry = await Animal(name="Angry Dog").save()
alice.favoriteAnimals.insert(99999, angry) <= even if inserted at index 3
angry_dog = alice.favouriteAnimals[99999] #<- now fails Not to mention that reordering in db is expensive and in most cases you do not care about the actual value of the order column but about the order itself. That's why in production systems sort order columns are often scarse, i.e. # VERY simplified example
animals_x_humans = [{"name": "Test 1", "sort_order": 100},
{"name": "Test 2", "sort_order": 200},
{"name": "Test 3", "sort_order": 300},
{"name": "Test 4", "sort_order": 400},
{"name": "Test 5", "sort_order": 500}]
# and now when you move animal 5 between 1 and 2 you set it based on function
# something floor((test1.sort_order+test2.sort_order)/2) = 150
animals_x_humans_reordered = [{"name": "Test 1", "sort_order": 100},
{"name": "Test 5", "sort_order": 150},
{"name": "Test 2", "sort_order": 200},
{"name": "Test 3", "sort_order": 300},
{"name": "Test 4", "sort_order": 400}]
# that way you avoid reordering tests 2,3 and 4, just change 1 row
# (of course in real life it can be hundreds or thousands of rows So long answer short - I don't thinks it's a general functionality and I won't implement this. from typing import List
from uuid import UUID, uuid4
import databases
import pytest
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class Animal(ormar.Model):
class Meta(BaseMeta):
tablename = "animals"
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.Text(default="")
# favoriteHumans
# provide explicit through table
class Link(ormar.Model):
class Meta(BaseMeta):
tablename = "link_table"
# note that you cannot define your own relations as of now in through model
id: int = ormar.Integer(primary_key=True)
animal_order: int = ormar.Integer(nullable=True)
human_order: int = ormar.Integer(nullable=True)
class Human(ormar.Model):
class Meta(BaseMeta):
tablename = "humans"
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.Text(default="")
favoriteAnimals: List[Animal] = ormar.ManyToMany(Animal, through=Link,
related_name="favoriteHumans",
)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
# this will be redundant with either relation or load_all order by
async def favorites(entity):
if (type(entity) == Human):
return list(map(lambda entity: entity.name,
await entity.favoriteAnimals.order_by(
'link__animal_order').all()))
else:
return list(map(lambda entity: entity.name,
await entity.favoriteHumans.order_by(
'link__human_order').all()))
@pytest.mark.asyncio
async def test_ordering_by_through():
async with database:
alice = await Human(name="Alice").save()
bob = await Human(name="Bob").save()
charlie = await Human(name="Charlie").save()
spot = await Animal(name="Spot").save()
kitty = await Animal(name="Kitty").save()
noodle = await Animal(name="Noodle").save()
# you need to add them in order anyway so can provide order explicitly
# if you have a lot of them a list with enumerate might be an option
await alice.favoriteAnimals.add(noodle, animal_order=0, human_order=0)
await alice.favoriteAnimals.add(spot, animal_order=1, human_order=0)
await alice.favoriteAnimals.add(kitty, animal_order=2, human_order=0)
# you dont have to reload queries on queryset clears the existing related
# alice = await alice.reload()
await favorites(alice)
assert [x.name for x in alice.favoriteAnimals] == ['Noodle', 'Spot', 'Kitty']
await bob.favoriteAnimals.add(noodle, animal_order=0, human_order=1)
await bob.favoriteAnimals.add(kitty, animal_order=1, human_order=1)
await bob.favoriteAnimals.add(spot, animal_order=2, human_order=1)
await favorites(bob)
assert [x.name for x in bob.favoriteAnimals] == ['Noodle', 'Kitty', 'Spot']
await charlie.favoriteAnimals.add(kitty, animal_order=0, human_order=2)
await charlie.favoriteAnimals.add(noodle, animal_order=1, human_order=2)
await charlie.favoriteAnimals.add(spot, animal_order=2, human_order=2)
await favorites(charlie)
# Expected: ['Kitty', 'Noodle', 'Spot']
# Current: ['Kitty', 'Noodle', 'Spot'] , correct on accident
animals = [noodle, kitty, spot]
for animal in animals:
await favorites(animal)
assert [x.name for x in animal.favoriteHumans] == ['Alice', 'Bob',
'Charlie']
zack = await Human(name="Zack").save()
# in your example here you expect all other than zack be updated in db
# otherwise alice already has order = 0 and order between zack and alice is
# inconclusive without additional ordering, which would have to shift by one all
# following items which is one of the reasons I won't implement it
async def reorder_humans(animal, new_ordered_humans):
# update only through models - they are normal models anyway
# similar thing can be done for other side
noodle_links = await Link.objects.filter(animal=animal).all()
for link in noodle_links:
link.human_order = next((
i for i, x in enumerate(new_ordered_humans) if
x.pk == link.human.pk),
None)
await Link.objects.bulk_update(noodle_links, columns=['human_order'])
await noodle.favoriteHumans.add(zack, animal_order=0, human_order=0)
# note you can also update through model check the docs
await reorder_humans(noodle, [zack, alice, bob, charlie])
await favorites(noodle)
assert [x.name for x in noodle.favoriteHumans] == ['Zack', 'Alice', 'Bob',
'Charlie']
await favorites(zack)
assert [x.name for x in zack.favoriteAnimals] == ['Noodle']
humans = noodle.favoriteHumans
humans.insert(1, humans.pop(0))
await reorder_humans(noodle, humans)
await favorites(noodle)
assert [x.name for x in noodle.favoriteHumans] == ['Alice', 'Zack', 'Bob',
'Charlie']
humans.insert(2, humans.pop(1))
await reorder_humans(noodle, humans)
await favorites(noodle)
assert [x.name for x in noodle.favoriteHumans] == ['Alice', 'Bob', 'Zack',
'Charlie']
humans.insert(3, humans.pop(2))
await reorder_humans(noodle, humans)
await favorites(noodle)
assert [x.name for x in noodle.favoriteHumans] == ['Alice', 'Bob',
'Charlie', 'Zack']
await kitty.favoriteHumans.remove(bob)
await favorites(kitty)
assert [x.name for x in kitty.favoriteHumans] == ['Alice', 'Charlie']
bob = await noodle.favoriteHumans.get(pk=bob.pk)
assert bob.link.human_order == 1
# remove charlie by order
await noodle.favoriteHumans.remove(
await noodle.favoriteHumans.filter(link__human_order=2).get())
await favorites(noodle)
assert [x.name for x in noodle.favoriteHumans] == ['Alice', 'Bob', 'Zack'] When writing specs for #129 I was already thinking about ordering through model, now I think also related_order_by should be on relation, and specified the order in which the order-by should be resolved |
Beta Was this translation helpful? Give feedback.
-
Is there a convenient way to order lists? Like, I'm imagining a flag set on e.g. a ManyToMany, and each db entry tracks the two ends of the relationship as well as where in each list it is, and calls to
.add()
and.remove()
etc. would update the ordering appropriately. Would there happen to be such a thing? (I didn't see anything about it in the docs, but figured I'd ask before opening a feature request.)Beta Was this translation helpful? Give feedback.
All reactions