mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
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!
|
||||
|
||||
Learning about Django's queryset language is very useful once you start doing more advanced things
|
||||
in Evennia. But it's not strictly needed out the box and can be a little overwhelming for a first
|
||||
reading. So if you are new to Python and Evennia, feel free to just skim this lesson and refer
|
||||
back to it later when you've gained more experience.
|
||||
Learning about Django's query language is very useful once you start doing more
|
||||
advanced things in Evennia. But it's not strictly needed out the box and can be
|
||||
a little overwhelming for a first reading. So if you are new to Python and
|
||||
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.
|
||||
|
|
@ -14,7 +15,7 @@ But sometimes you need to be more specific:
|
|||
- You want to find all `Characters` ...
|
||||
- ... who are in Rooms tagged as `moonlit` ...
|
||||
- ... _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
|
||||
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()
|
||||
|
||||
Note that `Weapon` and `Cannon` are different typeclasses. You won't find any `Cannon` instances in
|
||||
the `all_weapon` result above, confusing as that may sound. To get instances of a Typeclass _and_ the
|
||||
instances of all its children classes you need to use `_family`:
|
||||
Note that `Weapon` and `Cannon` are _different_ typeclasses. This means that you
|
||||
won't find any `Weapon`-typeclassed results in `all_cannons`. Vice-versa, you
|
||||
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
|
||||
|
||||
The all_family, filter_family etc is an Evennia-specific
|
||||
thing. It's not part of regular Django.
|
||||
The all_family, filter_family etc is an Evennia-specific
|
||||
thing. It's not part of regular Django.
|
||||
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
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.
|
||||
|
||||
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")
|
||||
|
||||
Only until we actually try to examine the result will the database be called. Here it's called when we
|
||||
try to loop over the queryset:
|
||||
It's important to note that we haven't called the database yet! Not until we
|
||||
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:
|
||||
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
|
||||
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
|
||||
Evennia uses `db_` in front of every database 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.
|
||||
Note how we use `db_key` and `db_location`. This is the actual names of these
|
||||
database fields. By convention Evennia uses `db_` in front of every database
|
||||
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:
|
||||
|
||||
|
|
@ -108,7 +130,7 @@ so it would not find `"Rose"`.
|
|||
# the i means it's case-insensitive
|
||||
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.
|
||||
|
||||
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"))
|
||||
|
||||
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")
|
||||
|
||||
|
|
@ -141,7 +164,7 @@ For more field lookups, see the
|
|||
## Get that werewolf ...
|
||||
|
||||
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:
|
||||
|
||||
|
|
@ -153,9 +176,9 @@ possible.
|
|||
|
||||
```{sidebar} Line breaks
|
||||
|
||||
Note the way of writing this code. It would have been very hard to read if we just wrote it in
|
||||
one long line. But since we wrapped it in `(...)` we can spread it out over multiple lines
|
||||
without worrying about line breaks!
|
||||
Note the way of writing this code. It would have been very hard to read if we
|
||||
just wrote it in one long line. But since we wrapped it in `(...)` we can spread
|
||||
it out over multiple lines without worrying about line breaks!
|
||||
```
|
||||
|
||||
```python
|
||||
|
|
@ -166,21 +189,23 @@ will_transform = (
|
|||
.filter(
|
||||
db_location__db_tags__db_key__iexact="moonlit",
|
||||
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.
|
||||
- **Line 4** - We start to filter ...
|
||||
- **Line 5**
|
||||
- We want to find `Character`s, so we access `.objects` on the `Character` typeclass.
|
||||
- We start to filter ...
|
||||
-
|
||||
- ... 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
|
||||
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).
|
||||
- **Line 6** - ... 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.
|
||||
- ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"`
|
||||
- ... 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
|
||||
`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
|
||||
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
|
||||
with a certain level of `lycanthrophy`. Now we also want the full moon to immediately transform people who were
|
||||
recently bitten, even if their `lycantrophy` level is not yet high enough (more dramatic this way!). Let's say there is
|
||||
a Tag "recently_bitten" that controls this.
|
||||
Let us expand our original werewolf query. Not only do we want to find all
|
||||
Characters in a moonlit room with a certain level of `lycanthrophy`. Now we also
|
||||
want the full moon to immediately transform people who were recently bitten,
|
||||
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:
|
||||
|
||||
|
|
@ -259,21 +286,24 @@ will_transform = (
|
|||
|
||||
```{sidebar} SQL
|
||||
|
||||
These Python structures are internally converted to SQL, the native language of the database.
|
||||
If you are familiar with SQL, these are many-to-many tables joined with `LEFT OUTER JOIN`,
|
||||
which may lead to multiple merged rows combining the same object with different relations.
|
||||
These Python structures are internally converted to SQL, the native language of
|
||||
the database. If you are familiar with SQL, these are many-to-many tables
|
||||
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
|
||||
than two _or_ which has the Tag `recently_bitten`". With an OR-query like this it's possible to find the
|
||||
same 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.
|
||||
This reads as "Find all Characters in a moonlit room that either has the
|
||||
Attribute `lycantrophy` higher than two, _or_ which has the Tag
|
||||
`recently_bitten`". With an OR-query like this it's possible to find the same
|
||||
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
|
||||
|
||||
What if we wanted to filter on some condition that isn't represented easily by a field on the
|
||||
object? Maybe we want to find rooms only containing five or more objects?
|
||||
What if we wanted to filter on some condition that isn't represented easily by a
|
||||
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!):
|
||||
|
||||
|
|
@ -288,12 +318,14 @@ We *could* do it like this (don't actually do it this way!):
|
|||
rooms_with_five_objects.append(room)
|
||||
```
|
||||
|
||||
Above we get all rooms and then use `list.append()` to keep adding the right rooms
|
||||
to an ever-growing list. This is _not_ a good idea, once your database grows this will
|
||||
be unnecessarily computing-intensive. The database is much more suitable for this.
|
||||
Above we get all rooms and then use `list.append()` to keep adding the right
|
||||
rooms to an ever-growing list. This is _not_ a good idea, once your database
|
||||
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
|
||||
then access from other parts of the query. Let's do the same example as before directly in the database:
|
||||
_Annotations_ allow you to set a 'variable' inside the query that you can then
|
||||
access from other parts of the query. Let's do the same example as before
|
||||
directly in the database:
|
||||
|
||||
```python
|
||||
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
|
||||
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
|
||||
use `num_objects__gte=5` which means that `num_objects` should be greater than 5. This is a little
|
||||
harder to get one's head around but much more efficient than lopping over all objects in Python.
|
||||
Next we filter on this annotation, using the name `num_objects` as something we
|
||||
can filter for. We use `num_objects__gte=5` which means that `num_objects`
|
||||
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
|
||||
|
||||
What if we wanted to compare two dynamic parameters against one another in a query? For example, what if
|
||||
instead of having 5 or more objects, we only wanted objects that had a bigger inventory than they had
|
||||
tags (silly example, but ...)? 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.
|
||||
What if we wanted to compare two dynamic parameters against one another in a
|
||||
query? For example, what if instead of having 5 or more objects, we only wanted
|
||||
objects that had a bigger inventory than they had tags (silly example, but ...)?
|
||||
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.
|
||||
|
||||
```python
|
||||
from django.db.models import Count, F
|
||||
|
|
@ -390,8 +424,9 @@ in a format like the following:
|
|||
|
||||
## Conclusions
|
||||
|
||||
We have covered a lot of ground in this lesson and covered several more complex topics. Knowing how to
|
||||
query using Django is a powerful skill to have.
|
||||
We have covered a lot of ground in this lesson and covered several more complex
|
||||
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
|
||||
to understand how to plan what our tutorial game will be about.
|
||||
This concludes the first part of the Evennia starting tutorial - "What we have".
|
||||
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