From 2a20523fae8cdf92efefee2f03611fa03e8da255 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 12 Feb 2022 17:14:40 +0100 Subject: [PATCH] Clean up the last part of the beginner tutorial part 1 --- .../Beginner-Tutorial/Part1/Django-queries.md | 157 +++++++++++------- 1 file changed, 96 insertions(+), 61 deletions(-) diff --git a/docs/source/Howtos/Beginner-Tutorial/Part1/Django-queries.md b/docs/source/Howtos/Beginner-Tutorial/Part1/Django-queries.md index 4378aedccd..2119886f23 100644 --- a/docs/source/Howtos/Beginner-Tutorial/Part1/Django-queries.md +++ b/docs/source/Howtos/Beginner-Tutorial/Part1/Django-queries.md @@ -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.