Clean up the last part of the beginner tutorial part 1
This commit is contained in:
parent
a40d66847b
commit
9ee97d9b6c
1 changed files with 96 additions and 61 deletions
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
```{important} More advanced lesson!
|
```{important} More advanced lesson!
|
||||||
|
|
||||||
Learning about Django's queryset language is very useful once you start doing more advanced things
|
Learning about Django's query language is very useful once you start doing more
|
||||||
in Evennia. But it's not strictly needed out the box and can be a little overwhelming for a first
|
advanced things in Evennia. But it's not strictly needed out the box and can be
|
||||||
reading. So if you are new to Python and Evennia, feel free to just skim this lesson and refer
|
a little overwhelming for a first reading. So if you are new to Python and
|
||||||
back to it later when you've gained more experience.
|
Evennia, feel free to just skim this lesson and refer back to it later when
|
||||||
|
you've gained more experience.
|
||||||
```
|
```
|
||||||
|
|
||||||
The search functions and methods we used in the previous lesson are enough for most cases.
|
The search functions and methods we used in the previous lesson are enough for most cases.
|
||||||
|
|
@ -14,7 +15,7 @@ But sometimes you need to be more specific:
|
||||||
- You want to find all `Characters` ...
|
- You want to find all `Characters` ...
|
||||||
- ... who are in Rooms tagged as `moonlit` ...
|
- ... who are in Rooms tagged as `moonlit` ...
|
||||||
- ... _and_ who has the Attribute `lycantrophy` with a level higher than 2 ...
|
- ... _and_ who has the Attribute `lycantrophy` with a level higher than 2 ...
|
||||||
- ... because they'll should immediately transform to werewolves!
|
- ... because they should immediately transform to werewolves!
|
||||||
|
|
||||||
In principle you could achieve this with the existing search functions combined with a lot of loops
|
In principle you could achieve this with the existing search functions combined with a lot of loops
|
||||||
and if statements. But for something non-standard like this, querying the database directly will be
|
and if statements. But for something non-standard like this, querying the database directly will be
|
||||||
|
|
@ -35,27 +36,34 @@ only wanted the cannons, we would do
|
||||||
|
|
||||||
all_cannons = Cannon.objects.all()
|
all_cannons = Cannon.objects.all()
|
||||||
|
|
||||||
Note that `Weapon` and `Cannon` are different typeclasses. You won't find any `Cannon` instances in
|
Note that `Weapon` and `Cannon` are _different_ typeclasses. This means that you
|
||||||
the `all_weapon` result above, confusing as that may sound. To get instances of a Typeclass _and_ the
|
won't find any `Weapon`-typeclassed results in `all_cannons`. Vice-versa, you
|
||||||
instances of all its children classes you need to use `_family`:
|
won't find any `Cannon`-typeclassed results in `all_weapons`. This may not be
|
||||||
|
what you expect.
|
||||||
|
|
||||||
|
If you want to get all entities with typeclass `Weapon` _as well_ as all the
|
||||||
|
subclasses of `Weapon`, such as `Cannon`, you need to use the `_family` type of
|
||||||
|
query:
|
||||||
|
|
||||||
```{sidebar} _family
|
```{sidebar} _family
|
||||||
|
|
||||||
The all_family, filter_family etc is an Evennia-specific
|
The all_family, filter_family etc is an Evennia-specific
|
||||||
thing. It's not part of regular Django.
|
thing. It's not part of regular Django.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
really_all_weapons = Weapon.objects.all_family()
|
really_all_weapons = Weapon.objects.all_family()
|
||||||
|
|
||||||
This result now contains both `Weapon` and `Cannon` instances.
|
This result now contains both `Weapon` and `Cannon` instances (and any other
|
||||||
|
entities whose typeclasses inherit at any distance from `Weapon`, like `Musket` or
|
||||||
|
`Sword`).
|
||||||
|
|
||||||
To limit your search by other criteria than the Typeclass you need to use `.filter`
|
To limit your search by other criteria than the Typeclass you need to use `.filter`
|
||||||
(or `.filter_family`) instead:
|
(or `.filter_family`) instead:
|
||||||
|
|
||||||
roses = Flower.objects.filter(db_key="rose")
|
roses = Flower.objects.filter(db_key="rose")
|
||||||
|
|
||||||
This is a queryset representing all objects having a `db_key` equal to `"rose"`.
|
This is a queryset representing all flowers having a `db_key` equal to `"rose"`.
|
||||||
Since this is a queryset you can keep adding to it; this will act as an `AND` condition.
|
Since this is a queryset you can keep adding to it; this will act as an `AND` condition.
|
||||||
|
|
||||||
local_roses = roses.filter(db_location=myroom)
|
local_roses = roses.filter(db_location=myroom)
|
||||||
|
|
@ -68,8 +76,10 @@ We can also `.exclude` something from results
|
||||||
|
|
||||||
local_non_red_roses = local_roses.exclude(db_key="red_rose")
|
local_non_red_roses = local_roses.exclude(db_key="red_rose")
|
||||||
|
|
||||||
Only until we actually try to examine the result will the database be called. Here it's called when we
|
It's important to note that we haven't called the database yet! Not until we
|
||||||
try to loop over the queryset:
|
actually try to examine the result will the database be called. Here the
|
||||||
|
database is called when we try to loop over it (because now we need to actually
|
||||||
|
get results out of it to be able to loop):
|
||||||
|
|
||||||
for rose in local_non_red_roses:
|
for rose in local_non_red_roses:
|
||||||
print(rose)
|
print(rose)
|
||||||
|
|
@ -78,9 +88,21 @@ From now on, the queryset is _evaluated_ and we can't keep adding more queries t
|
||||||
create a new queryset if we wanted to find some other result. Other ways to evaluate the queryset is to
|
create a new queryset if we wanted to find some other result. Other ways to evaluate the queryset is to
|
||||||
print it, convert it to a list with `list()` and otherwise try to access its results.
|
print it, convert it to a list with `list()` and otherwise try to access its results.
|
||||||
|
|
||||||
Note how we use `db_key` and `db_location`. This is the actual names of these database fields. By convention
|
Note how we use `db_key` and `db_location`. This is the actual names of these
|
||||||
Evennia uses `db_` in front of every database field. When you use the normal Evennia search helpers and objects
|
database fields. By convention Evennia uses `db_` in front of every database
|
||||||
you can skip the `db_` but here we are calling the database directly and need to use the 'real' names.
|
field. When you use the normal Evennia search helpers and objects you can skip
|
||||||
|
the `db_` but here we are calling the database directly and need to use the
|
||||||
|
'real' names.
|
||||||
|
|
||||||
|
```{sidebar} database fields
|
||||||
|
Each database table have only a few fields. For `Objects`, the most common ones
|
||||||
|
are `db_key`, `db_location` and `db_destination`. When accessing them they are
|
||||||
|
normally accessed just as `obj.key`, `obj.location` and `obj.destination`. You
|
||||||
|
only need to remember the `db_` when using them in database queries. The object
|
||||||
|
description, `obj.db.desc` is not such a hard-coded field, but one of many
|
||||||
|
arbitrary Attributes attached to the Object.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
Here are the most commonly used methods to use with the `objects` managers:
|
Here are the most commonly used methods to use with the `objects` managers:
|
||||||
|
|
||||||
|
|
@ -108,7 +130,7 @@ so it would not find `"Rose"`.
|
||||||
# the i means it's case-insensitive
|
# the i means it's case-insensitive
|
||||||
roses = Flower.objects.filter(db_key__iexact="rose")
|
roses = Flower.objects.filter(db_key__iexact="rose")
|
||||||
|
|
||||||
The Django field query language uses `__` in the same way as Python uses `.` to access resources. This
|
The Django field query language uses `__` similarly to how Python uses `.` to access resources. This
|
||||||
is because `.` is not allowed in a function keyword.
|
is because `.` is not allowed in a function keyword.
|
||||||
|
|
||||||
roses = Flower.objects.filter(db_key__icontains="rose")
|
roses = Flower.objects.filter(db_key__icontains="rose")
|
||||||
|
|
@ -120,7 +142,8 @@ comparisons (same for `__lt` and `__le`). There is also `__in`:
|
||||||
|
|
||||||
swords = Weapons.objects.filter(db_key__in=("rapier", "two-hander", "shortsword"))
|
swords = Weapons.objects.filter(db_key__in=("rapier", "two-hander", "shortsword"))
|
||||||
|
|
||||||
One also uses `__` to access foreign objects like Tags. Let's for example assume this is how we identify mages:
|
One also uses `__` to access foreign objects like Tags. Let's for example assume
|
||||||
|
this is how we have identified mages:
|
||||||
|
|
||||||
char.tags.add("mage", category="profession")
|
char.tags.add("mage", category="profession")
|
||||||
|
|
||||||
|
|
@ -141,7 +164,7 @@ For more field lookups, see the
|
||||||
## Get that werewolf ...
|
## Get that werewolf ...
|
||||||
|
|
||||||
Let's see if we can make a query for the werewolves in the moonlight we mentioned at the beginning
|
Let's see if we can make a query for the werewolves in the moonlight we mentioned at the beginning
|
||||||
of this section.
|
of this lesson.
|
||||||
|
|
||||||
Firstly, we make ourselves and our current location match the criteria, so we can test:
|
Firstly, we make ourselves and our current location match the criteria, so we can test:
|
||||||
|
|
||||||
|
|
@ -153,9 +176,9 @@ possible.
|
||||||
|
|
||||||
```{sidebar} Line breaks
|
```{sidebar} Line breaks
|
||||||
|
|
||||||
Note the way of writing this code. It would have been very hard to read if we just wrote it in
|
Note the way of writing this code. It would have been very hard to read if we
|
||||||
one long line. But since we wrapped it in `(...)` we can spread it out over multiple lines
|
just wrote it in one long line. But since we wrapped it in `(...)` we can spread
|
||||||
without worrying about line breaks!
|
it out over multiple lines without worrying about line breaks!
|
||||||
```
|
```
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
@ -166,21 +189,23 @@ will_transform = (
|
||||||
.filter(
|
.filter(
|
||||||
db_location__db_tags__db_key__iexact="moonlit",
|
db_location__db_tags__db_key__iexact="moonlit",
|
||||||
db_attributes__db_key="lycantrophy",
|
db_attributes__db_key="lycantrophy",
|
||||||
db_attributes__db_value__gt=2)
|
db_attributes__db_value__gt=2
|
||||||
|
)
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Line 3** - We want to find `Character`s, so we access `.objects` on the `Character` typeclass.
|
- We want to find `Character`s, so we access `.objects` on the `Character` typeclass.
|
||||||
- **Line 4** - We start to filter ...
|
- We start to filter ...
|
||||||
- **Line 5**
|
-
|
||||||
- ... by accessing the `db_location` field (usually this is a Room)
|
- ... by accessing the `db_location` field (usually this is a Room)
|
||||||
- ... and on that location, we get the value of `db_tags` (this is a _many-to-many_ database field
|
- ... and on that location, we get the value of `db_tags` (this is a _many-to-many_ database field
|
||||||
that we can treat like an object for this purpose; it references all Tags on the location)
|
that we can treat like an object for this purpose; it references all Tags on the location)
|
||||||
- ... and from those `Tags`, we looking for `Tags` whose `db_key` is "monlit" (non-case sensitive).
|
- ... and from those `Tags`, we looking for `Tags` whose `db_key` is "monlit" (non-case sensitive).
|
||||||
- **Line 6** - ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"`
|
- ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"`
|
||||||
- **Line 7** - ... at the same time as the `Attribute`'s `db_value` is greater-than 2.
|
- ... at the same time as the `Attribute`'s `db_value` is greater-than 2.
|
||||||
|
|
||||||
Running this query makes our newly lycantrrophic Character appear in `will_transform`. Success!
|
Running this query makes our newly lycantrophic Character appear in `will_transform` so we
|
||||||
|
know to transform it. Success!
|
||||||
|
|
||||||
> Don't confuse database fields with [Attributes](../../../Components/Attributes.md) you set via `obj.db.attr = 'foo'` or
|
> Don't confuse database fields with [Attributes](../../../Components/Attributes.md) you set via `obj.db.attr = 'foo'` or
|
||||||
`obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not
|
`obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not
|
||||||
|
|
@ -218,10 +243,12 @@ works like `NOT`.
|
||||||
Would get all Characters that are either named "Dalton" _or_ which is _not_ in prison. The result is a mix
|
Would get all Characters that are either named "Dalton" _or_ which is _not_ in prison. The result is a mix
|
||||||
of Daltons and non-prisoners.
|
of Daltons and non-prisoners.
|
||||||
|
|
||||||
Let us expand our original werewolf query. Not only do we want to find all Characters in a moonlit room
|
Let us expand our original werewolf query. Not only do we want to find all
|
||||||
with a certain level of `lycanthrophy`. Now we also want the full moon to immediately transform people who were
|
Characters in a moonlit room with a certain level of `lycanthrophy`. Now we also
|
||||||
recently bitten, even if their `lycantrophy` level is not yet high enough (more dramatic this way!). Let's say there is
|
want the full moon to immediately transform people who were recently bitten,
|
||||||
a Tag "recently_bitten" that controls this.
|
even if their `lycantrophy` level is not yet high enough (more dramatic this
|
||||||
|
way!). When you get bitten, you'll get a Tag `recently_bitten` put on you to
|
||||||
|
indicate this.
|
||||||
|
|
||||||
This is how we'd change our query:
|
This is how we'd change our query:
|
||||||
|
|
||||||
|
|
@ -259,21 +286,24 @@ will_transform = (
|
||||||
|
|
||||||
```{sidebar} SQL
|
```{sidebar} SQL
|
||||||
|
|
||||||
These Python structures are internally converted to SQL, the native language of the database.
|
These Python structures are internally converted to SQL, the native language of
|
||||||
If you are familiar with SQL, these are many-to-many tables joined with `LEFT OUTER JOIN`,
|
the database. If you are familiar with SQL, these are many-to-many tables
|
||||||
which may lead to multiple merged rows combining the same object with different relations.
|
joined with `LEFT OUTER JOIN`, which may lead to multiple merged rows combining
|
||||||
|
the same object with different relations.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This reads as "Find all Characters in a moonlit room that either has the Attribute `lycantrophy` higher
|
This reads as "Find all Characters in a moonlit room that either has the
|
||||||
than two _or_ which has the Tag `recently_bitten`". With an OR-query like this it's possible to find the
|
Attribute `lycantrophy` higher than two, _or_ which has the Tag
|
||||||
same Character via different paths, so we add `.distinct()` at the end. This makes sure that there is only
|
`recently_bitten`". With an OR-query like this it's possible to find the same
|
||||||
one instance of each Character in the result.
|
Character via different paths, so we add `.distinct()` at the end. This makes
|
||||||
|
sure that there is only one instance of each Character in the result.
|
||||||
|
|
||||||
## Annotations
|
## Annotations
|
||||||
|
|
||||||
What if we wanted to filter on some condition that isn't represented easily by a field on the
|
What if we wanted to filter on some condition that isn't represented easily by a
|
||||||
object? Maybe we want to find rooms only containing five or more objects?
|
field on the object? Maybe we want to find rooms only containing five or more
|
||||||
|
objects?
|
||||||
|
|
||||||
We *could* do it like this (don't actually do it this way!):
|
We *could* do it like this (don't actually do it this way!):
|
||||||
|
|
||||||
|
|
@ -288,12 +318,14 @@ We *could* do it like this (don't actually do it this way!):
|
||||||
rooms_with_five_objects.append(room)
|
rooms_with_five_objects.append(room)
|
||||||
```
|
```
|
||||||
|
|
||||||
Above we get all rooms and then use `list.append()` to keep adding the right rooms
|
Above we get all rooms and then use `list.append()` to keep adding the right
|
||||||
to an ever-growing list. This is _not_ a good idea, once your database grows this will
|
rooms to an ever-growing list. This is _not_ a good idea, once your database
|
||||||
be unnecessarily computing-intensive. The database is much more suitable for this.
|
grows this will be unnecessarily computing-intensive. The database is much more
|
||||||
|
suitable for this.
|
||||||
|
|
||||||
_Annotations_ allow you to set a 'variable' inside the query that you can
|
_Annotations_ allow you to set a 'variable' inside the query that you can then
|
||||||
then access from other parts of the query. Let's do the same example as before directly in the database:
|
access from other parts of the query. Let's do the same example as before
|
||||||
|
directly in the database:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typeclasses.rooms import Room
|
from typeclasses.rooms import Room
|
||||||
|
|
@ -315,17 +347,19 @@ that will count the number of results inside the database.
|
||||||
> Note the use of `location_set` in that `Count`. The `*_set` is a back-reference automatically created by
|
> Note the use of `location_set` in that `Count`. The `*_set` is a back-reference automatically created by
|
||||||
Django. In this case it allows you to find all objects that *has the current object as location*.
|
Django. In this case it allows you to find all objects that *has the current object as location*.
|
||||||
|
|
||||||
Next we filter on this annotation, using the name `num_objects` as something we can filter for. We
|
Next we filter on this annotation, using the name `num_objects` as something we
|
||||||
use `num_objects__gte=5` which means that `num_objects` should be greater than 5. This is a little
|
can filter for. We use `num_objects__gte=5` which means that `num_objects`
|
||||||
harder to get one's head around but much more efficient than lopping over all objects in Python.
|
should be greater than or equal to 5. This is a little harder to get one's head
|
||||||
|
around but much more efficient than lopping over all objects in Python.
|
||||||
|
|
||||||
## F-objects
|
## F-objects
|
||||||
|
|
||||||
What if we wanted to compare two dynamic parameters against one another in a query? For example, what if
|
What if we wanted to compare two dynamic parameters against one another in a
|
||||||
instead of having 5 or more objects, we only wanted objects that had a bigger inventory than they had
|
query? For example, what if instead of having 5 or more objects, we only wanted
|
||||||
tags (silly example, but ...)? This can be with Django's
|
objects that had a bigger inventory than they had tags (silly example, but ...)?
|
||||||
[F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions).
|
This can be with Django's [F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions).
|
||||||
So-called F expressions allow you to do a query that looks at a value of each object in the database.
|
So-called F expressions allow you to do a query that looks at a value of each
|
||||||
|
object in the database.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django.db.models import Count, F
|
from django.db.models import Count, F
|
||||||
|
|
@ -390,8 +424,9 @@ in a format like the following:
|
||||||
|
|
||||||
## Conclusions
|
## Conclusions
|
||||||
|
|
||||||
We have covered a lot of ground in this lesson and covered several more complex topics. Knowing how to
|
We have covered a lot of ground in this lesson and covered several more complex
|
||||||
query using Django is a powerful skill to have.
|
topics. Knowing how to query using Django is a powerful skill to have.
|
||||||
|
|
||||||
This concludes the first part of the Evennia starting tutorial - "What we have". Now we have a good foundation
|
This concludes the first part of the Evennia starting tutorial - "What we have".
|
||||||
to understand how to plan what our tutorial game will be about.
|
Now we have a good foundation to understand how to plan what our tutorial game
|
||||||
|
will be about.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue