Skip to content

Commit

Permalink
Merge pull request #194 from PhilippMDoerner/moar-examples
Browse files Browse the repository at this point in the history
Moar examples
  • Loading branch information
moigagoo authored Sep 11, 2023
2 parents 0449e01 + ac53362 commit 69ca1e0
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 130 deletions.
154 changes: 25 additions & 129 deletions book/tutorial/rows.nim
Original file line number Diff line number Diff line change
Expand Up @@ -136,107 +136,50 @@ nbCode:
for pet in petsFoo:
echo pet[]

nbText: """
### Selecting Many-To-One/One-To-Many relationships
Imagine you had a Many-To-One relationship between two models, like we have with `Customer` being the many-model and `User` being the one-model, where one user can have many customers.
If you have a user and wanted to query all of their customers, you couldn't do so by just making a query for the user, as that model doesn't have a "seq[Customer]" field that Norm could resolve.
You could query the users for a given customer separately using the mechanisms of a general select statement.
However, you can also query them separately using a convenience proc `selectOneToMany` to do all of that work for you.
Just provide the "one"-side of the relationship (user), a seq of the "many-model" (seq[Customer]) to populate as before and the name of the field on the "many-model" ("user" as that's the name of field on Customer pointing to User) that points to the "one-model" (User).
nbText"""
## Update Rows
If your "many-model" (Customer) only has a single field pointing to the one model (User) you can even forego providing the field-name, Norm will infer it for you!
To update a row, you just update the object and call `update` on it:
"""

nbCode:
# With explicitly provided field name
var customersFoo2 = @[newCustomer()]
dbConn.selectOneToMany(userFoo, customersFoo2, "user")

for customer in customersFoo2:
echo customer[]
customerBar.name = some "Saaam"
dbConn.update(customerBar)

# With inferred field name
var customersFoo3 = @[newCustomer()]
dbConn.selectOneToMany(userFoo, customersFoo3)

for customer in customersFoo3:
echo customer[]
echo()

nbText: """
Since customer references a user, to update a customer, we also need to update its user. Norm handles that automatically by generating two queries.
An additional benefit of using this `selectOneToMany` is that with it, Norm will validate whether this query is correct at compile time!
In the first approach, if Customer doesn't have a field called "user" or if that field does not have any model-type that points to the "User"-table, nor an fk-pragma to any such type, then the code will throw an error with a helpful message at compile-time.
In the second approach, if Customer doesn't have any field of type "User" or any other model-type that points to the same table as "User", it will also not compile while throwing a helpful error message.
Updating rows in bulk is also possible:
"""

### Selecting Many-To-Many relationships
nbCode:
for customer in customersFoo:
customer.name = some ("Mega" & get(customer.name))

Imagine if you had a Many-To-Many relationship between two models (e.g. Users and Groups) that is recorded on an "join-model" (e.g. UserGroup), where one user can be in many groups and a group can have many users.
dbConn.update(customersFoo)

If you have a user and want to query all of its groups, you can do so via the general select statement mechanism.
echo()

Similarly to `selectOneToMany` there is a helper proc `selectManyToMany` here for convenience.
nbText: """
Just provide the side whose model entry you have (e.g. User or Group), a seq of the join-model (e.g. UserGroup), a seq of the entries your trying to query (e.g. seq[Group] or seq[User]), the field name on the join-model pointing to the model entry you have (e.g. "user" or "group") and the field name on the join-model pointing to the model of the entries you're trying to query (e.g. "group" or "user").
## Delete Rows
As before, if your join-model (e.g. UserGroup) only has a single field pointing to each of the two many models (e.g. User and Group), you can forego the field names and let Norm infer them for you.
To delete a row, call `delete` on an object:
"""

nbCode:
type
Group* = ref object of Model
name*: string

UserGroup* = ref object of Model
user*: User
membershipGroup*: Group

func newGroup*(name = ""): Group = Group(name: name)

func newUserGroup*(user = newUser(), group = newGroup()): UserGroup = UserGroup(user: user, membershipGroup: group)

dbConn.createTables(newGroup())
dbConn.createTables(newUser())
dbConn.createTables(newUserGroup())

var
groupFoo = newGroup("groupFoo")
groupBar = newGroup("groupBar")

userFooGroupFooMembership = newUserGroup(userFoo, groupFoo)
userBarGroupFooMembership = newUserGroup(userBar, groupFoo)
userFooGroupBarMembership = newUserGroup(userFoo, groupBar)
dbConn.delete(sam)

with dbConn:
insert groupFoo
insert groupBar
insert userFooGroupFooMembership
insert userBarGroupFooMembership
insert userFooGroupBarMembership

# With explicitly provided fieldnames
var userFooGroupMemberships: seq[UserGroup] = @[newUserGroup()]
var userFooGroups: seq[Group] = @[newGroup()]
dbConn.selectManyToMany(userFoo, userFooGroupMemberships, userFooGroups, "user", "membershipGroup")

for group in userFooGroups:
echo group[]
echo()

# With inferred field names
var userFooGroupMemberships2: seq[UserGroup] = @[newUserGroup()]
var userFooGroups2: seq[Group] = @[newGroup()]
dbConn.selectManyToMany(userFoo, userFooGroupMemberships2, userFooGroups2)
nbText: """
After deletion, the object becomes `nil`:
"""

for group in userFooGroups2:
echo group[]
nbCode:
echo sam.isNil

nbText: """
## Count Rows
Expand Down Expand Up @@ -302,51 +245,4 @@ If you need to check if a row selected by a given condition exists, use `exists`
nbCode:
echo dbConn.exists(Customer, "name = ?", "Alice")


nbText"""
## Update Rows
To update a row, you just update the object and call `update` on it:
"""

nbCode:
customerBar.name = some "Saaam"
dbConn.update(customerBar)

echo()

nbText: """
Since customer references a user, to update a customer, we also need to update its user. Norm handles that automatically by generating two queries.
Updating rows in bulk is also possible:
"""

nbCode:
for customer in customersFoo:
customer.name = some ("Mega" & get(customer.name))

dbConn.update(customersFoo)

echo()

nbText: """
## Delete Rows
To delete a row, call `delete` on an object:
"""

nbCode:
dbConn.delete(sam)

echo()

nbText: """
After deletion, the object becomes `nil`:
"""

nbCode:
echo sam.isNil

nbSave

nbSave
71 changes: 71 additions & 0 deletions book/tutorial/rowsAdvanced.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import nimib, nimibook
import norm/sqlite
import ./tables
import std/[with, options]

nbInit(theme = useNimibook)

nbText: """
# More complex queries
Norm allows you to sort, limit use subqueries for complex where clauses and more.
To understand how, it helps to keep in mind that norm essentially generates SQL SELECT queries after the following pattern:
`SELECT <fields of model> FROM <table-name specified by model> WHERE <condition>`
This means that whatever pieces of SQL come after the WHERE keyword are thing you can freely specify if need be.
## Limiting the number of queried models
To limit the number of queried models, simply use SQL's Limit keyword.
Lets query our `Customer` table from earlier and query multiple entries, but only take the first entry.
"""

nbCode:
var
userFoo = newUser("[email protected]")
alice = newCustomer(some "Alice", userFoo)
bob = newCustomer(some "Bob", userFoo)
with dbConn:
insert userFoo
insert alice
insert bob

var customersFoo = @[newCustomer()]
dbConn.select(customersFoo, "User.email = ? LIMIT 1", "[email protected]")

assert customersFoo.len() == 1

echo()

nbText: """
`customersFoo` has only 1 entry, despite `alice` and `bob` both having the email address `"[email protected]"`, thanks to the LIMIT SQL keyword.
## Sorting model output
We can of course use ORDER BY just as we did LIMIT before:
"""

nbCode:
var sortedCustomersFoo = @[newCustomer()]
dbConn.select(sortedCustomersFoo, "User.email = ? ORDER BY name DESC", "[email protected]")

assert sortedCustomersFoo[0].name.get() == "Bob"
assert sortedCustomersFoo[1].name.get() == "Alice"

echo()


nbText: """
## Using Subqueries
Similarly as to ORDER BY, you can also use subqueries within the WHERE block:
"""
nbCode:
var subqueryCustomersFoo = @[newCustomer()]
const condition = """
Customer.id IN (SELECT Cust.id FROM Customer AS Cust WHERE Cust.id % 2 == 0)
"""
dbConn.select(subqueryCustomersFoo, condition)
assert subqueryCustomersFoo.len() == 1
assert subqueryCustomersFoo[0].id == 2

echo()
nbSave
115 changes: 115 additions & 0 deletions book/tutorial/rowsManyToX.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import std/with
import nimib, nimibook
import norm/[model, sqlite]

import ./tables

nbInit(theme = useNimibook)

nbText: """
### Selecting Many-To-One/One-To-Many relationships
Imagine you had a Many-To-One relationship between two models, like we have with `Customer` being the many-model and `User` being the one-model, where one user can have many customers.
If you have a user and wanted to query all of their customers, you couldn't do so by just making a query for the user, as that model doesn't have a "seq[Customer]" field that Norm could resolve.
You could query the users for a given customer separately using the mechanisms of a general select statement.
However, you can also query them separately using a convenience proc `selectOneToMany` to do all of that work for you.
Just provide the "one"-side of the relationship (user), a seq of the "many-model" (seq[Customer]) to populate as before and the name of the field on the "many-model" ("user" as that's the name of field on Customer pointing to User) that points to the "one-model" (User).
If your "many-model" (Customer) only has a single field pointing to the one model (User) you can even forego providing the field-name, Norm will infer it for you!
"""

nbCode:
var
userFoo = newUser("[email protected]")
userBar = newUser("[email protected]")

# With explicitly provided field name
var customersFoo2 = @[newCustomer()]
dbConn.selectOneToMany(userFoo, customersFoo2, "user")

for customer in customersFoo2:
echo customer[]

# With inferred field name
var customersFoo3 = @[newCustomer()]
dbConn.selectOneToMany(userFoo, customersFoo3)

for customer in customersFoo3:
echo customer[]

nbText: """
An additional benefit of using this `selectOneToMany` is that with it, Norm will validate whether this query is correct at compile time!
In the first approach, if Customer doesn't have a field called "user" or if that field does not have any model-type that points to the "User"-table, nor an fk-pragma to any such type, then the code will throw an error with a helpful message at compile-time.
In the second approach, if Customer doesn't have any field of type "User" or any other model-type that points to the same table as "User", it will also not compile while throwing a helpful error message.
### Selecting Many-To-Many relationships
Imagine if you had a Many-To-Many relationship between two models (e.g. Users and Groups) that is recorded on an "join-model" (e.g. UserGroup), where one user can be in many groups and a group can have many users.
If you have a user and want to query all of its groups, you can do so via the general select statement mechanism.
Similarly to `selectOneToMany` there is a helper proc `selectManyToMany` here for convenience.
Just provide the side whose model entry you have (e.g. User or Group), a seq of the join-model (e.g. UserGroup), a seq of the entries your trying to query (e.g. seq[Group] or seq[User]), the field name on the join-model pointing to the model entry you have (e.g. "user" or "group") and the field name on the join-model pointing to the model of the entries you're trying to query (e.g. "group" or "user").
As before, if your join-model (e.g. UserGroup) only has a single field pointing to each of the two many models (e.g. User and Group), you can forego the field names and let Norm infer them for you.
"""

nbCode:
type
Group* = ref object of Model
name*: string

UserGroup* = ref object of Model
user*: User
membershipGroup*: Group

func newGroup*(name = ""): Group = Group(name: name)

func newUserGroup*(user = newUser(), group = newGroup()): UserGroup = UserGroup(user: user, membershipGroup: group)

dbConn.createTables(newGroup())
dbConn.createTables(newUser())
dbConn.createTables(newUserGroup())

var
groupFoo = newGroup("groupFoo")
groupBar = newGroup("groupBar")

userFooGroupFooMembership = newUserGroup(userFoo, groupFoo)
userBarGroupFooMembership = newUserGroup(userBar, groupFoo)
userFooGroupBarMembership = newUserGroup(userFoo, groupBar)

with dbConn:
insert groupFoo
insert groupBar
insert userFooGroupFooMembership
insert userBarGroupFooMembership
insert userFooGroupBarMembership

# With explicitly provided fieldnames
var userFooGroupMemberships: seq[UserGroup] = @[newUserGroup()]
var userFooGroups: seq[Group] = @[newGroup()]
dbConn.selectManyToMany(userFoo, userFooGroupMemberships, userFooGroups, "user", "membershipGroup")

for group in userFooGroups:
echo group[]

# With inferred field names
var userFooGroupMemberships2: seq[UserGroup] = @[newUserGroup()]
var userFooGroups2: seq[Group] = @[newGroup()]
dbConn.selectManyToMany(userFoo, userFooGroupMemberships2, userFooGroups2)

for group in userFooGroups2:
echo group[]

nbSave
4 changes: 3 additions & 1 deletion nbook.nim
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ var book = initBookWithToc:
entry("Models 101", "models.nim")
section("Tutorial", "tutorial.nim"):
entry("Tables", "tutorial/tables.nim")
entry("Rows", "tutorial/rows.nim")
entry("Simple Queries", "tutorial/rows.nim")
entry("Advanced Queries", "tutorial/rowsAdvanced.nim")
entry("Many-To-One/Many Queries", "tutorial/rowsManyToX.nim")
entry("Raw SQL interactions", "tutorial/rawSelects.nim")
entry("Row Caveats", "tutorial/rowCaveats.nim")
entry("Transactions", "transactions.nim")
Expand Down

0 comments on commit 69ca1e0

Please sign in to comment.