SQLAlchemy 1.4 table query that includes first result of second table [duplicate] - sqlalchemy

I have a SQLAlchemy model with a one-to-many relationship between table x and table y. The record (if any) with the greatest id in table y where y.x_id = x.id is special. Class X and class Y map tables x and y.
I know how to define X.all_y (ORDER BY y.id). How do I define X.latest_y equivalent to X.all_y[-1]?

the purely relational way to do it requires using a subquery to get the "latest" or "max" value, correlated to the parent, then equating that with the members of the collection. It means you'll get best results if you put an index on the column that determines "the latest":
from sqlalchemy import *
from sqlalchemy.orm import *
engine = create_engine('sqlite:///:memory:', echo='debug')
m = MetaData()
parent = Table('parent', m,
Column('id', Integer, primary_key=True)
)
child = Table('child', m,
Column('id', Integer, primary_key=True),
Column('parent_id', Integer, ForeignKey('parent.id')),
Column('sortkey', Integer)
)
m.create_all(engine)
class Parent(object):
def __init__(self, children):
self.all_c = children
class Child(object):
def __init__(self, sortkey):
self.sortkey = sortkey
latest_c = select([func.max(child.c.sortkey)]).\
where(child.c.parent_id==parent.c.id).\
correlate(parent).\
as_scalar()
mapper(Parent, parent, properties={
'all_c':relation(Child),
'latest_c':relation(Child,
primaryjoin=and_(
child.c.sortkey==latest_c,
child.c.parent_id==parent.c.id
),
uselist=False
)
})
mapper(Child, child)
session = sessionmaker(engine)()
p1, p2, p3 = Parent([Child('a'), Child('b'), Child('c')]), \
Parent([Child('b'), Child('c')]),\
Parent([Child('f'), Child('g'), Child('c')])
session.add_all([p1, p2, p3])
session.commit()
assert p1.latest_c.sortkey == 'c'
assert p2.latest_c.sortkey == 'c'
assert p3.latest_c.sortkey == 'g'
Alternatively, you can on some platforms use LIMIT, which can produce faster results since you avoid the aggregation and can join the collection item on its primary key:
latest_c = select([child.c.id]).\
where(child.c.parent_id==parent.c.id).\
order_by(child.c.sortkey.desc()).\
limit(1).\
correlate(parent).\
as_scalar()
mapper(Parent, parent, properties={
'all_c':relation(Child),
'latest_c':relation(Child,
primaryjoin=and_(
child.c.id==latest_c,
child.c.parent_id==parent.c.id
),
uselist=False
)
})

Here is a version of #zzzeek's answer that uses declarative rather than imperative mapping, using declared_attr to insert the relationship into the parent's __mapper_args__.
import sqlalchemy as sa
from sqlalchemy import orm
Base = orm.declarative_base()
class Child(Base):
__tablename__ = 'children'
id = sa.Column(sa.Integer, primary_key=True)
sortkey = sa.Column(sa.Integer, nullable=False)
parent_id = sa.Column(sa.Integer, sa.ForeignKey('parents.id'))
parent = orm.relationship('Parent', back_populates='children')
class Parent(Base):
__tablename__ = 'parents'
id = sa.Column(sa.Integer, primary_key=True)
children = orm.relationship('Child', back_populates='parent')
#orm.declared_attr
def __mapper_args__(cls):
children = Child.__table__
most_recent_child = (
sa.select(children.c.id)
.where(children.c.parent_id == cls.id)
.order_by(children.c.sortkey.desc())
.limit(1)
.correlate(cls.__table__)
.scalar_subquery()
)
rel = orm.relation(
Child,
primaryjoin=sa.and_(
Child.id == most_recent_child, Child.parent_id == cls.id
),
uselist=False,
viewonly=True,
)
return {'properties': {'latest_child': rel}}
# Build and test.
engine = sa.create_engine('sqlite://', echo=True, future=True)
Base.metadata.create_all(engine)
Session = orm.sessionmaker(engine, future=True)
with Session.begin() as s:
children = [Child(sortkey=i) for i in range(1, 6)]
parent = Parent(children=children)
s.add(parent)
with Session() as s:
w = s.scalars(sa.select(Parent)).first()
assert w.latest_child.sortkey == 5, f'{w.latest_child.sortkey=}'
assert len(w.children) == 5, f'{len(w.children)=}'

Related

Android Room how to query related table?

First thing, my data is POKEMON!!! enjoy 😉
I need to do this on the database side, filtering and sorting the returned data isn't an option as using Paging...
I'm using Room I have my database working well but I now want to query the pokemonType list in my relation
Given this data class
data class PokemonWithTypesAndSpecies #JvmOverloads constructor(
#Ignore
var matches : Int = 0,
#Embedded
val pokemon: Pokemon,
#Relation(
parentColumn = Pokemon.POKEMON_ID,
entity = PokemonType::class,
entityColumn = PokemonType.TYPE_ID,
associateBy = Junction(
value = PokemonTypesJoin::class,
parentColumn = Pokemon.POKEMON_ID,
entityColumn = PokemonType.TYPE_ID
)
)
val types: List<PokemonType>,
#Relation(
parentColumn = Pokemon.POKEMON_ID,
entity = PokemonSpecies::class,
entityColumn = PokemonSpecies.SPECIES_ID,
associateBy = Junction(
value = PokemonSpeciesJoin::class,
parentColumn = Pokemon.POKEMON_ID,
entityColumn = PokemonSpecies.SPECIES_ID
)
)
val species: PokemonSpecies?
)
I can get my data with a simple query and even search it
#Query("SELECT * FROM Pokemon WHERE pokemon_name LIKE :search")
fun searchPokemonWithTypesAndSpecies(search: String): LiveData<List<PokemonWithTypesAndSpecies>>
But now what I want is to add filtering on pokemon types which as you can see is a list (which is probably a transaction under the hood) and is in a seperate table, so given a list of string called filters I want to:
only return pokemon that contain an item in filters
sort pokemon by both number of matching types and by ID
So I want my tests to look something like this
val bulbasaurSpeciesID = 1
val squirtleSpeciesID = 2
val charmanderSpeciesID = 3
val charizardSpeciesID = 4
val pidgeySpeciesID = 5
val moltresSpeciesID = 6
val bulbasaurID = 1
val squirtleID = 2
val charmanderID = 3
val charizardID = 4
val pidgeyID = 5
val moltresID = 6
val grassTypeID = 1
val poisonTypeID = 2
val fireTypeID = 3
val waterTypeID = 4
val flyingTypeID = 5
val emptySearch = "%%"
val allPokemonTypes = listOf(
"normal",
"water",
"fire",
"grass",
"electric",
"ice",
"fighting",
"poison",
"ground",
"flying",
"psychic",
"bug",
"rock",
"ghost",
"dark",
"dragon",
"steel",
"fairy",
"unknown",
)
#Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context, PokemonRoomDatabase::class.java,
).setTransactionExecutor(Executors.newSingleThreadExecutor())
.allowMainThreadQueries()
.build()
pokemonDao = db.pokemonDao()
speciesDao = db.pokemonSpeciesDao()
speciesJoinDao = db.pokemonSpeciesJoinDao()
pokemonTypeDao = db.pokemonTypeDao()
pokemonTypeJoinDao = db.pokemonTypeJoinDao()
}
#After
#Throws(IOException::class)
fun closeDb() {
db.close()
}
#Test
#Throws(Exception::class)
fun testFiltering() {
insertPokemonForFilterTest()
val pokemon =
pokemonDao.searchAndFilterPokemon(search = emptySearch, filters = allPokemonTypes)
.getValueBlocking(scope)
assertThat(pokemon?.size, equalTo(6)) // This fails list size is 9 with the current query
val pokemonFiltered =
pokemonDao.searchAndFilterPokemon(search = emptySearch, filters = listOf("fire", "flying"))
.getValueBlocking(scope)
assertThat(pokemon?.size, equalTo(4))
assertThat(pokemonFiltered!![0].pokemon.name, equalTo("charizard")) // matches 2 filters and ID is 4
assertThat(pokemonFiltered!![1].pokemon.name, equalTo("moltres")) // matches 2 filters and ID is 6
assertThat(pokemonFiltered!![2].pokemon.name, equalTo("charmander")) // matches one filter and ID is 3
assertThat(pokemonFiltered!![3].pokemon.name, equalTo("pidgey")) // mayches one filter and ID is 5
}
private fun insertPokemonForFilterTest() = runBlocking {
insertBulbasaur()
insertSquirtle()
insertCharmander()
insertCharizard()
insertMoltres()
insertPidgey()
}
private fun insertBulbasaur() = runBlocking {
val bulbasaur = bulbasaur()
val grassJoin = PokemonTypesJoin(pokemon_id = bulbasaurID, type_id = grassTypeID)
val poisonJoin = PokemonTypesJoin(pokemon_id = bulbasaurID, type_id = poisonTypeID)
val bulbasaurSpeciesJoin =
PokemonSpeciesJoin(pokemon_id = bulbasaurID, species_id = bulbasaurSpeciesID)
pokemonDao.insertPokemon(bulbasaur.pokemon)
speciesDao.insertSpecies(bulbasaur.species!!)
speciesJoinDao.insertPokemonSpeciesJoin(bulbasaurSpeciesJoin)
pokemonTypeDao.insertPokemonType(pokemonType = bulbasaur.types[0])
pokemonTypeDao.insertPokemonType(pokemonType = bulbasaur.types[1])
pokemonTypeJoinDao.insertPokemonTypeJoin(grassJoin)
pokemonTypeJoinDao.insertPokemonTypeJoin(poisonJoin)
}
private fun insertSquirtle() = runBlocking {
val squirtle = squirtle()
val squirtleSpeciesJoin =
PokemonSpeciesJoin(pokemon_id = squirtleID, species_id = squirtleSpeciesID)
val waterJoin = PokemonTypesJoin(pokemon_id = squirtleID, type_id = waterTypeID)
pokemonDao.insertPokemon(squirtle.pokemon)
speciesDao.insertSpecies(squirtle.species!!)
speciesJoinDao.insertPokemonSpeciesJoin(squirtleSpeciesJoin)
pokemonTypeDao.insertPokemonType(pokemonType = squirtle.types[0])
pokemonTypeJoinDao.insertPokemonTypeJoin(waterJoin)
}
private fun insertCharmander() = runBlocking {
val charmander = charmander()
val fireJoin = PokemonTypesJoin(pokemon_id = charmanderID, type_id = fireTypeID)
val charmanderSpeciesJoin =
PokemonSpeciesJoin(pokemon_id = charmanderID, species_id = charmanderSpeciesID)
pokemonDao.insertPokemon(charmander.pokemon)
speciesDao.insertSpecies(charmander.species!!)
speciesJoinDao.insertPokemonSpeciesJoin(charmanderSpeciesJoin)
pokemonTypeDao.insertPokemonType(pokemonType = charmander.types[0])
pokemonTypeJoinDao.insertPokemonTypeJoin(fireJoin)
}
private fun insertCharizard() = runBlocking {
val charizard = charizard()
val charizardSpeciesJoin =
PokemonSpeciesJoin(pokemon_id = charizardID, species_id = charizardSpeciesID)
val fireJoin = PokemonTypesJoin(pokemon_id = charizardID, type_id = fireTypeID)
val flyingJoin = PokemonTypesJoin(pokemon_id = charizardID, type_id = flyingTypeID)
pokemonDao.insertPokemon(charizard.pokemon)
speciesDao.insertSpecies(charizard.species!!)
speciesJoinDao.insertPokemonSpeciesJoin(charizardSpeciesJoin)
pokemonTypeDao.insertPokemonType(pokemonType = charizard.types[0])
pokemonTypeDao.insertPokemonType(pokemonType = charizard.types[1])
pokemonTypeJoinDao.insertPokemonTypeJoin(fireJoin)
pokemonTypeJoinDao.insertPokemonTypeJoin(flyingJoin)
}
private fun insertPidgey() = runBlocking {
val pidgey = pidgey()
val pidgeySpeciesJoin =
PokemonSpeciesJoin(pokemon_id = pidgeyID, species_id = pidgeySpeciesID)
val flyingJoin = PokemonTypesJoin(pokemon_id = pidgeyID, type_id = flyingTypeID)
pokemonDao.insertPokemon(pidgey.pokemon)
speciesDao.insertSpecies(pidgey.species!!)
speciesJoinDao.insertPokemonSpeciesJoin(pidgeySpeciesJoin)
pokemonTypeDao.insertPokemonType(pokemonType = pidgey.types[0])
pokemonTypeJoinDao.insertPokemonTypeJoin(flyingJoin)
}
private fun insertMoltres() = runBlocking {
val moltres = moltres()
val moltresSpeciesJoin =
PokemonSpeciesJoin(pokemon_id = moltresID, species_id = moltresSpeciesID)
val fireJoin = PokemonTypesJoin(pokemon_id = moltresID, type_id = fireTypeID)
val flyingJoin = PokemonTypesJoin(pokemon_id = moltresID, type_id = flyingTypeID)
pokemonDao.insertPokemon(moltres.pokemon)
speciesDao.insertSpecies(moltres.species!!)
speciesJoinDao.insertPokemonSpeciesJoin(moltresSpeciesJoin)
pokemonTypeDao.insertPokemonType(pokemonType = moltres.types[0])
pokemonTypeDao.insertPokemonType(pokemonType = moltres.types[1])
pokemonTypeJoinDao.insertPokemonTypeJoin(fireJoin)
pokemonTypeJoinDao.insertPokemonTypeJoin(flyingJoin)
}
fun bulbasaur(): PokemonWithTypesAndSpecies = PokemonWithTypesAndSpecies(
pokemon = Pokemon(id = bulbasaurID, name = "bulbasaur"),
species = PokemonSpecies(
id = bulbasaurSpeciesID,
species = "Seed pokemon",
pokedexEntry = "There is a plant seed on its back right from the day this Pokémon is born. The seed slowly grows larger."
),
types = listOf(
PokemonType(id = poisonTypeID, name = "poison", slot = 1),
PokemonType(id = grassTypeID, name = "grass", slot = 2)
)
)
fun squirtle(): PokemonWithTypesAndSpecies = PokemonWithTypesAndSpecies(
pokemon = Pokemon(id = squirtleID, name = "squirtle"),
species = PokemonSpecies(
id = squirtleSpeciesID,
species = "Turtle pokemon",
pokedexEntry = "Small shell pokemon"
),
types = listOf(PokemonType(id = waterTypeID, name = "water", slot = 1))
)
fun charmander(): PokemonWithTypesAndSpecies = PokemonWithTypesAndSpecies(
pokemon = Pokemon(id = charmanderID, name = "charmander"),
species = PokemonSpecies(
id = charmanderSpeciesID,
species = "Fire lizard pokemon",
pokedexEntry = "If the flame on this pokemon's tail goes out it will die"
),
types = listOf(PokemonType(id = fireTypeID, name = "fire", slot = 1))
)
fun charizard(): PokemonWithTypesAndSpecies = PokemonWithTypesAndSpecies(
pokemon = Pokemon(id = charizardID, name = "charizard"),
species = PokemonSpecies(
id = charizardSpeciesID,
species = "Fire flying lizard pokemon",
pokedexEntry = "Spits fire that is hot enough to melt boulders. Known to cause forest fires unintentionally"
),
types = listOf(
PokemonType(id = fireTypeID, name = "fire", slot = 1),
PokemonType(id = flyingTypeID, name = "flying", slot = 2)
)
)
fun moltres(): PokemonWithTypesAndSpecies = PokemonWithTypesAndSpecies(
pokemon = Pokemon(id = moltresID, name = "moltres"),
species = PokemonSpecies(
id = moltresSpeciesID,
species = "Fire bird pokemon",
pokedexEntry = "Known as the legendary bird of fire. Every flap of its wings creates a dazzling flash of flames"
),
types = listOf(
PokemonType(id = fireTypeID, name = "fire", slot = 1),
PokemonType(id = flyingTypeID, name = "flying", slot = 2)
)
)
fun pidgey(): PokemonWithTypesAndSpecies = PokemonWithTypesAndSpecies(
pokemon = Pokemon(id = pidgeyID, name = "pidgey"),
species = PokemonSpecies(
id = pidgeySpeciesID,
species = "Bird pokemon",
pokedexEntry = "Pidgey is a Flying Pokémon. Among all the Flying Pokémon, it is the gentlest and easiest to capture. A perfect target for the beginning Pokémon Trainer to test his Pokémon's skills."
),
types = listOf(PokemonType(id = flyingTypeID, name = "flying", slot = 1))
)
And the query would be
#Query("SELECT * FROM Pokemon INNER JOIN PokemonType, PokemonTypesJoin ON Pokemon.pokemon_id = PokemonTypesJoin.pokemon_id AND PokemonType.type_id = PokemonTypesJoin.type_id WHERE pokemon_name LIKE :search AND type_name IN (:filters) ORDER BY pokemon_id ASC")
fun searchAndFilterPokemon(search: String, filters: List<String>): LiveData<List<PokemonWithTypesAndSpecies>>
I'm guessing this doesn't work because at this point Room hasn't collected the types from the other table and it's probably not even querying a list, I think this part
type_name IN (:filters)
is checking a column against a list when what I want is a List against a list 🤷‍♂️ but honestly I'm happy to just say I've fallen and can't get up 🤣 can anyone help? any help appreciated
Maybe I could misuse some columns' names, but try this query:
#Query("SELECT pok.id, pok.name FROM Pokemon AS pok
INNER JOIN PokemonTypesJoin AS p_join ON pok.id = p_join.pokemon_id
INNER JOIN PokemonType AS pok_type ON pok_type.id = p_join.type_id
WHERE pok.name LIKE :search AND pok_type.name IN (:filters)
GROUP BY pok.id, pok.name ORDER BY count(*) DESC, pok.id ASC")
Have you compared Room to Cmobilecom-JPA for android? JPA is very good at query relationships. The advantage of using JPA (standard) is obvious, making your code reusable on android, server side java, or swing project.
Thanks to #serglytikhonov my query works and now looks like this
#Query("""SELECT * FROM Pokemon
INNER JOIN PokemonTypesJoin
ON Pokemon.pokemon_id = PokemonTypesJoin.pokemon_id
INNER JOIN PokemonType
ON PokemonType.type_id = PokemonTypesJoin.type_id
WHERE pokemon_name LIKE :search AND type_name IN (:filters)
GROUP BY Pokemon.pokemon_id, Pokemon.pokemon_name
ORDER BY count(*) DESC, pokemon_id ASC""")
fun searchAndFilterPokemon(search: String, filters: List<String>): LiveData<List<PokemonWithTypesAndSpecies>>
the main piece being this count(*) and group by many thanks

How to add extra fields in ValueQuerySet (Django)?

Basically, I want to convert the query_set to JSON. But I also want to add one more field something like size = some number in the query_set which is not present in the query_set attributes (it is computed attribute). Can you tell me how to do it?
query_set = PotholeCluster.objects.all().values('bearing', 'center_lat', 'center_lon', 'grid_id')
return JsonResponse(list(query_set), safe=False)
I tried the code below. It works, but I would like to know if there is any cleaner way to do this.
query_set = PotholeCluster.objects.all()
response_list = []
for pc in query_set:
d = {}
d['bearing'] = pc.get_bearing()
d['center_lat'] = pc.center_lat
d['center_lon'] = pc.center_lat
d['grid_id'] = pc.grid_id
d['size'] = pc.pothole_set.all().count()
response_list.append(d)
serialized = json.dumps(response_list)
return HttpResponse(serialized, content_type='application/json')
class PotholeCluster(models.Model):
center_lat = models.FloatField(default=0)
center_lon = models.FloatField(default=0)
snapped_lat = models.FloatField(default=0)
snapped_lon = models.FloatField(default=0)
size = models.IntegerField(default=-1)
# avgspeed in kmph
speed = models.FloatField(default=-1)
# in meters
accuracy = models.FloatField(default=-1)
# avg bearing in degree
bearing = models.FloatField(default=-1)
grid = models.ForeignKey(
Grid,
on_delete=models.SET_NULL,
null=True,
blank=True
)
def __str__(self):
raw_data = serialize('python', [self])
output = json.dumps(raw_data[0]['fields'])
return "pk = {}|{}".format(self.id, output)
def get_bearing(self):
if self.bearing != -1:
return self.bearing
potholes = self.pothole_set.all()
bearings = [pothole.location.bearing for pothole in potholes]
bearings.sort()
i = 0
if bearings[-1] >= 350:
while bearings[-1] - bearings[i] >= 340:
if bearings[i] <= 10:
bearings[i] += 360
i += 1
self.bearing = sum(bearings) / len(bearings) % 360
self.save()
return self.bearing
def get_size(self):
if self.size != -1:
return self.size
self.size = len(self.pothole_set.all())
self.save()
return self.size

How do I insert field having default value in Slick

Given mapping having NOT NULL field str with a default value:
case class Tst(id: Option[Int] = None, ii: Int, str: String)
class Tsts(tag: Tag) extends Table[Tst](tag, "tsts") {
def id = column[Option[Int]]("id", O.PrimaryKey, O.AutoInc)
def ii = column[Int]("ii")
def str = column[String]("str", O.Default("ddd"))
def * = (id, ii, str) <> (Tst.tupled, Tst.unapply)
}
How do I insert object specifying the field value if I have it:
Tst(ii = 1, str = "aaa")
and skipping it if I don't:
Tst(ii = 1)
Yes, I know the last statement will not compile.
I tried using Option[String] and other things. It ends up with either inserting null or failing with can't be null error
The compiler depends on you putting default values at the end, like:
scala> case class TST(ii: Int, str: String = "aaa", id: Option[Int] = None)
defined class TST
scala> new TST(3)
res0: TST = TST(3,aaa,None)
Edit: Just realized I didn't answer completely:
scala> new TST(3, id = Some(1))
res1: TST = TST(3,aaa,Some(1))
scala> new TST(3, str = "bbb")
res2: TST = TST(3,bbb,None)

How to filter the values of two fields as one?

How to aggregate the results of the two fields and show as one in django-filter?
Let's take, for example, such a model:
class Animal(object):
LEGS_CHOICES = (2, 4, 8)
legs = models.PositiveSmallIntegerField(choices=LEGS_CHOICES)
class Dog(Animal):
pass
class Spider(Animal):
pass
My django-filter filter class:
class AnimalFilterSet(django_filters.FilterSet):
legs = django_filters.MultipleChoiceFilter(choices=Animal.LEGS_CHOICES, widget=forms.CheckboxSelectMultiple())
class Meta:
model = Animal
fields = ['legs']
I would like to filter this two models by the same fields and display them as one.
Using queryset I can do it like this:
Animal.objects.filter(Q(dog__legs = 4) | Q(spider__legs = 4))
I wrote my own Filter
class MultiMultipleChoiceFilter(django_filters.Filter):
"""
This filter preforms an OR query on the selected options for defined fields.
"""
field_class = forms.MultipleChoiceField
def __init__(self, fields, *args, **kwargs):
super(MultiMultipleChoiceFilter, self).__init__(*args, **kwargs)
self.fields = fields
def filter(self, qs, value):
value = value or ()
if len(value) == len(self.field.choices):
return qs
q = Q()
for v in value:
for f in self.fields:
q |= Q(**{f: v})
return qs.filter(q).distinct()
Example of use. As a parameter, enter a list of fields.
class AnimalFilterSet(django_filters.FilterSet):
legs = django_filters.MultiMultipleChoiceFilter(['dog__legs', 'spider__legs'], choices=Animal.LEGS_CHOICES, widget=forms.CheckboxSelectMultiple())
class Meta:
model = Animal
fields = ['legs']

Adjacency list + Abstract Base Class Inheritance used in relationship

Following is a example for Adjacency List + Inheritance. This works as expected but if i try to use it in a another Model Mammut as a relationship it throws me this error:
Traceback (most recent call last):
File "bin/py", line 73, in <module>
exec(compile(__file__f.read(), __file__, "exec"))
File "../adjacency_list.py", line 206, in <module>
create_entries(IntTreeNode)
File "../adjacency_list.py", line 170, in create_entries
mut.nodes.append(node)
File "/home/xxx/.buildout/eggs/SQLAlchemy-0.9.8-py3.4-linux-x86_64.egg/sqlalchemy/orm/dynamic.py", line 304, in append
attributes.instance_dict(self.instance), item, None)
File "/home/xxx/.buildout/eggs/SQLAlchemy-0.9.8-py3.4-linux-x86_64.egg/sqlalchemy/orm/dynamic.py", line 202, in append
self.fire_append_event(state, dict_, value, initiator)
File "/home/xxx/.buildout/eggs/SQLAlchemy-0.9.8-py3.4-linux-x86_64.egg/sqlalchemy/orm/dynamic.py", line 99, in fire_append_event
value = fn(state, value, initiator or self._append_token)
File "/home/xxx/.buildout/eggs/SQLAlchemy-0.9.8-py3.4-linux-x86_64.egg/sqlalchemy/orm/attributes.py", line 1164, in emit_backref_from_collection_append_event
child_impl.append(
AttributeError: '_ProxyImpl' object has no attribute 'append'
The Code:
from sqlalchemy import (Column, ForeignKey, Integer, String, create_engine,
Float)
from sqlalchemy.orm import (Session, relationship, backref, joinedload_all)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.ext.declarative import declared_attr, AbstractConcreteBase
Base = declarative_base()
class Mammut(Base):
__tablename__ = "mammut"
id = Column(Integer, primary_key=True)
nodes = relationship(
'TreeNode',
backref='mammut',
lazy='dynamic',
cascade="all, delete-orphan",
#viewonly=True
)
class TreeNode(AbstractConcreteBase, Base):
id = Column(Integer, primary_key=True)
name = Column(String(50), nullable=False)
depth = Column(Integer, default=0)
data_type = Column(String(50))
#declared_attr
def mammut_id(cls):
return Column(Integer, ForeignKey('mammut.id'))
#declared_attr
def __tablename__(cls):
return cls.__name__.lower()
#declared_attr
def __mapper_args__(cls):
ret = {}
if cls.__name__ != "TreeNode":
ret = {'polymorphic_identity': cls.__name__,
'concrete': True,
# XXX redundant makes only sense if we use one table
'polymorphic_on': cls.data_type}
return ret
#declared_attr
def parent_id(cls):
_fid = '%s.id' % cls.__name__.lower()
return Column(Integer, ForeignKey(_fid))
#declared_attr
def children(cls):
_fid = '%s.id' % cls.__name__
return relationship(cls.__name__,
# cascade deletions
cascade="all, delete-orphan",
# many to one + adjacency list - remote_side
# is required to reference the 'remote'
# column in the join condition.
backref=backref("parent", remote_side=_fid),
# children will be represented as a dictionary
# on the "name" attribute.
collection_class=attribute_mapped_collection(
'name'),
)
def get_path(self, field):
if self.parent:
return self.parent.get_path(field) + [getattr(self, field)]
else:
return [getattr(self, field)]
#property
def name_path(self):
# XXX there is no way to query for it except we add a function with a
# cte (recursive query) to our database see [1] for it
# https://stackoverflow.com/questions/14487386/sqlalchemy-recursive-hybrid-property-in-a-tree-node
return '/'.join(self.get_path(field='name'))
def __init__(self, name, value=None, parent=None):
self.name = name
self.parent = parent
self.depth = 0
self.value = value
if self.parent:
self.depth = self.parent.depth + 1
def __repr__(self):
ret = "%s(name=%r, id=%r, parent_id=%r, value=%r, depth=%r, " \
"name_path=%s data_type=%s)" % (
self.__class__.__name__,
self.name,
self.id,
self.parent_id,
self.value,
self.depth,
self.name_path,
self.data_type
)
return ret
def dump(self, _indent=0):
return " " * _indent + repr(self) + \
"\n" + \
"".join([
c.dump(_indent + 1)
for c in self.children.values()]
)
class IntTreeNode(TreeNode):
value = Column(Integer)
class FloatTreeNode(TreeNode):
value = Column(Float)
miau = Column(String(50), default='zuff')
def __repr__(self):
ret = "%s(name=%r, id=%r, parent_id=%r, value=%r, depth=%r, " \
"name_path=%s data_type=%s miau=%s)" % (
self.__class__.__name__,
self.name,
self.id,
self.parent_id,
self.value,
self.depth,
self.name_path,
self.data_type,
self.miau
)
return ret
if __name__ == '__main__':
engine = create_engine('sqlite:///', echo=True)
def msg(msg, *args):
msg = msg % args
print("\n\n\n" + "-" * len(msg.split("\n")[0]))
print(msg)
print("-" * len(msg.split("\n")[0]))
msg("Creating Tree Table:")
Base.metadata.create_all(engine)
session = Session(engine)
def create_entries(Cls):
node = Cls('rootnode', value=2)
Cls('node1', parent=node)
Cls('node3', parent=node)
node2 = Cls('node2')
Cls('subnode1', parent=node2)
node.children['node2'] = node2
Cls('subnode2', parent=node.children['node2'])
msg("Created new tree structure:\n%s", node.dump())
msg("flush + commit:")
# XXX this throws the error
mut = Mammut()
mut.nodes.append(node)
session.add(mut)
session.add(node)
session.commit()
msg("Tree After Save:\n %s", node.dump())
Cls('node4', parent=node)
Cls('subnode3', parent=node.children['node4'])
Cls('subnode4', parent=node.children['node4'])
Cls('subsubnode1', parent=node.children['node4'].children['subnode3'])
# remove node1 from the parent, which will trigger a delete
# via the delete-orphan cascade.
del node.children['node1']
msg("Removed node1. flush + commit:")
session.commit()
msg("Tree after save:\n %s", node.dump())
msg("Emptying out the session entirely, "
"selecting tree on root, using eager loading to join four levels deep.")
session.expunge_all()
node = session.query(Cls).\
options(joinedload_all("children", "children",
"children", "children")).\
filter(Cls.name == "rootnode").\
first()
msg("Full Tree:\n%s", node.dump())
# msg("Marking root node as deleted, flush + commit:")
# session.delete(node)
# session.commit()
create_entries(IntTreeNode)
create_entries(FloatTreeNode)
nodes = session.query(TreeNode).filter(
TreeNode.name == "rootnode").all()
for idx, n in enumerate(nodes):
msg("Full (%s) Tree:\n%s" % (idx, n.dump()))
concrete inheritance can be very difficult, and AbstractConcreteBase itself has bugs in 0.9 which get in the way of elaborate mappings like this from being used.
Using 1.0 (not released, use git master), I can get the major elements going as follows:
from sqlalchemy import Column, String, Integer, create_engine, ForeignKey, Float
from sqlalchemy.orm import Session, relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.ext.declarative import declared_attr, AbstractConcreteBase
Base = declarative_base()
class Mammut(Base):
__tablename__ = "mammut"
id = Column(Integer, primary_key=True)
nodes = relationship(
'TreeNode',
lazy='dynamic',
back_populates='mammut',
)
class TreeNode(AbstractConcreteBase, Base):
id = Column(Integer, primary_key=True)
name = Column(String)
#declared_attr
def __tablename__(cls):
if cls.__name__ == 'TreeNode':
return None
else:
return cls.__name__.lower()
#declared_attr
def __mapper_args__(cls):
return {'polymorphic_identity': cls.__name__, 'concrete': True}
#declared_attr
def parent_id(cls):
return Column(Integer, ForeignKey(cls.id))
#declared_attr
def mammut_id(cls):
return Column(Integer, ForeignKey('mammut.id'))
#declared_attr
def mammut(cls):
return relationship("Mammut", back_populates="nodes")
#declared_attr
def children(cls):
return relationship(
cls,
back_populates="parent",
collection_class=attribute_mapped_collection('name'),
)
#declared_attr
def parent(cls):
return relationship(
cls, remote_side="%s.id" % cls.__name__,
back_populates='children')
class IntTreeNode(TreeNode):
value = Column(Integer)
class FloatTreeNode(TreeNode):
value = Column(Float)
miau = Column(String(50), default='zuff')
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
session = Session(e)
root = IntTreeNode(name='root')
IntTreeNode(name='n1', parent=root)
n2 = IntTreeNode(name='n2', parent=root)
IntTreeNode(name='n2n1', parent=n2)
m1 = Mammut()
m1.nodes.append(n2)
m1.nodes.append(root)
session.add(root)
session.commit()
session.close()
root = session.query(TreeNode).filter_by(name='root').one()
print root.children