# Django Models Part 3 (Model Manager,  QuerySet,  Lookups)

### Model Manager
- A `Manager` is the interface through which we perform ORMs in Django models
- By Default, Django automatically adds a `Manager` with a name of `objects`. This is the reason we are able to use `objects` and query to our Models `eg: Model.objects.all()`
- We can always create a new Manager and add to our model. We can customize ORM methods or add some more functionality in our Manager for data query
- Atleast one Manager should exist in every models

Example to change rename default `objects` in our model
```py
from django.db import models

class Info(models.Model):
    name = models.CharField(max_length=100)

    data = models.Manager()

# Info.objects.all() will not work but Info.data.all() will work
```

### QuerySet
- QuerySet can be defined as a collection of Objects (model objects)
- Until and unless you do something to evaluate a QuerySet `(loop, slice with increments,...)`, it doesn't hit your database. So, we call it by saying QuerSet is Lazy


#### Let's take an example of basic model which we will be using in this tutorial
- All, methods, customizations will be done on basis of this model below

```py
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()
```

#### QuerySet Methods
1. filter()
   - Returns a new QuerySet, list of objects that matches the given lookup criteria
   - To return list of `users` whose `age`=10, we can do `User.objects.filter(age=10)`;
   ```py
qs = User.objects.filter(age=10)
for u in qs:
    print(u.name, u.age)
   ```
   - Multiple arguments are joined by `AND` clause when crafting an SQL query
   - So, `User.objects.filter(age=10, name="John")` will return list of `users` whose `age` is `10` `AND` whose `name` is `John`
   - If you need to join arguments with `OR` you can use `Q` objects. e.g. This example will return list of users whose name is `John` `OR` `Harry` 
   ```py
from django.db.models import Q
qs = User.objects.filter(Q(name='John') | Q(name='Harry'))
```

2. exclude()
    - Returns a new Queryset, list of objects that do not match the given lookup criteria
    - Same like `filter()` multiple arguments/parameters are joined by `AND` clause
    - `User.objects.exclude(age=20)` will return list of users whose `age` is not `20`

3. order_by()
    - Used to perform some ordering for the return QuerySet list
    - If you have set default ordering for your models, you don't even have to use this method to order your `QuerySet`, because the results returned by `QuerySet` are ordered automatically by the ordering tuple given by the `ordering` option in models `Meta`
    ```py
class User(models.Model):
        name = models.CharField(max_length=100)
        age = models.IntegerField()

        class Meta:
            ordering = ['-age'] # - negative sign for descending order and
                            # no sign for asc order
    ```
    - `User.objects.filter(age=10)` on this model will return the list of users whose `age` is `10` in descending order of `age`
    - `User.objects.filter(age=10).order_by('age')` on this model will return the list of users whose `age` is `10` in ascending order of `age`
    - `User.objects.order_by('-name')` will return list of users in descending order


4. none()
    - Calling `none()` will create a `QuerySet` that will never return any objects
    - This doesn't return any data, so this can be used in place where we need a valid `QuerySet` but no data
    - calling `none()` will not hit database
    - e.g `User.objects.none()`

5. all()
    - This method is the simplest way to retrieve all objects from the model
    - `User.objects.all()` will return list of every users/objects

#### Database ORM methods which do not return a QuerySet
1. get()
    - `User.objects.get(id=1)` will return the row/object whose `id` is `1`
2. create()
    - `User.objects.create(name="Peter",age=25)` will create a new object/record in database and return that object
3. first()
    - `User.objects.filter(id=10).first()` will return the first row/object whose `id` is `10`
4. last()
    - `User.objects.filter(id=10).last()` will return the last row/object whose `id` is `10`
5. delete()
    - `User.objects.filter(id=10).delete()` will delete objects/rows whose `id` is `10`. This works like a bulk delete
    - You can also perform `delete()` on model single objects like the objects obtained from `get()` `first()` `last()` methods. This works like a single object delete
6. count()
    - `User.objects.filter(id=10).count()` will return total number of users/objects whose `id` is `10`

### Field Lookups
- Field lookups are used to specify meaning to the SQL `WHERE` clause
- These lookups are crafted and attached with the fields when performing an ORM queries
- It looks like
    - `field_[LookupType]=value` [note the double underscores]

#### Lookups Types
1. exact (also a default lookup)
    - An exact Match
    - `User.objects.filter(name__exact="Peter")` would generate `SQL: select .... where name="Peter"` and returns the objects/rows whose name is `Peter`
    > If we do not provide a lookup like in the above `QuerySet` method examples, Django automatically uses `exact` lookup. This is the reason our ORMs were working even when we didn't pass any lookups
    - Thus, `User.objects.filter(name__exact="Peter")` and `User.objects.filter(name="Peter")` QuerySet is practically the same

2. iexact
    - Case insensitive match
    - `User.objects.filter(name__iexact="peter")` would return QuerySet of the list of users whose name is `peter(case insensitive)` i.e: `peter`, `Peter`, `PeTeR`, `peteR` e.t.c

3. contains
    - Case sensitive containment
    - `User.objects.filter(name__contains="a")` would generate `SQL: select ..... where name like '%a%'` and would return QuerySet of list of users whose `name` contains/includes `a`

4. icontains
     - Similar to `contains` but case-insensitive

5. gt
    - greater than 
    - `User.objects.filter(age_gt=10)` would return QuerySet of list of users whose age is greater than 10 i.e 11,12,13,.......

6. gte
    - greater than or equal to
    - - `User.objects.filter(age_gte=10)` would return QuerySet of list of users whose age is greater than or equals to 10 i.e 10,11,12,13,.......

7. lt
    - less than 
    - `User.objects.filter(age_lt=10)` would return QuerySet of list of users whose age is less than 10 i.e 9,8,7,......

8. lte
    - less than or equal to
    - - `User.objects.filter(age_lte=10)` would return QuerySet of list of users whose age is less than or equals to 10 i.e 10,9,8,7,.....

> There are other plenty of inbuilts lookups. To name some of them; `in` `startswith` `istartswith` `range` 
