Ouroboros behaviour (ouroboros v0.1.3)
Allows you to paginate your Ecto results using cursors.
Usage
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :my_app
use Ouroboros
end
Options
Ouroboros
can take any options accepted by paginate/3
. This is useful when
you want to enforce some options globally across your project.
Example
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :my_app
use Ouroboros, limit: 10, limit_max: 100,
end
Note that these values can be still be overriden when paginate/3
is called.
Use without macros
If you wish to avoid use of macros or you wish to use a different name for the pagination function you can define your own function like so:
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :my_app
def my_paginate_function(query, opts \ [], repo_opts \ []) do
defaults = [limit: 10] # Default options of your choice here
opts = Keyword.merge(defaults, opts)
Ouroboros.paginate(query, opts, __MODULE__, repo_opts)
end
end
Summary
Callbacks
Fetches all the results matching the query within the cursors.
Functions
Generate a cursor for the supplied record, in the same manner as the
before
and after
cursors generated by paginate/3
.
Default function used to get the value of a cursor field from the supplied
map. This function can be overriden in the Ouroboros.Config
using the value_fun
key.
Default function used to get the value of a cursor field from the supplied
map. This function can be overriden in the Ouroboros.Config
using the value_fun
key.
Callbacks
@callback paginate(query :: Ecto.Query.t(), opts :: keyword(), repo_opts :: keyword()) :: Ouroboros.Page.t()
Fetches all the results matching the query within the cursors.
Options
:after
- fetch the records after this cursor;:before
- fetch the records before this cursor;:fields
- fields with sorting direction used to determine the cursor. In most cases, this should be the same fields as the ones used for sorting in the query. When you use named bindings in your query they can also be provided;:value_fun
- function of arity 2 to lookup cursor values on returned records. Defaults to&Ouroboros.value_fun_default/2
;:limit
- limits the number of records returned per page. Note that this number will be capped by:limit_max
. Defaults to50
;:limit_max
- sets a maximum cap for:limit
. This option can be useful when:limit
is set dynamically (e.g from a URL param set by a user) but you still want to enfore a maximum. Defaults to100
.
Repo options
This will be passed directly to Ecto.Repo.all/2
, as such any option supported
by this function can be used here.
Simple example
query = from(p in Post, order_by: [asc: p.inserted_at, asc: p.id], select: p)
Repo.paginate(query, fields: [:inserted_at, :id], limit: 50)
Example with using custom sort directions per field
query = from(p in Post, order_by: [asc: p.inserted_at, desc: p.id], select: p)
Repo.paginate(query, fields: [inserted_at: :asc, id: :desc], limit: 50)
Example with sorting on columns in joined tables
from p in Post, as: :posts,
join: a in assoc(p, :author), as: :author,
preload: [author: a],
select: p,
order_by: [asc: a.name, asc: p.id]
Repo.paginate(query, fields: [{{:author, :name}, :asc}, id: :asc], limit: 50)
When sorting on columns in joined tables it is necessary to use named bindings. In
this case we name it author
. In the fields
we refer to this named binding
and its column name.
To build the cursor Ouroboros uses the returned Ecto.Schema. When using a joined
column the returned Ecto.Schema won't have the value of the joined column
unless we preload it. E.g. in this case the cursor will be build up from
post.id
and post.author.name
. This presupposes that the named of the
binding is the same as the name of the relationship on the original struct.
One level deep joins are supported out of the box but if we join on a second
level, e.g. post.author.company.name
a custom function can be supplied to
handle the cursor value retrieval. This also applies when the named binding
does not map to the name of the relationship.
Example
from p in Post, as: :posts,
join: a in assoc(p, :author), as: :author,
join: c in assoc(a, :company), as: :company,
preload: [author: a],
select: p,
order_by: [
{:asc, a.name},
{:asc, p.id}
]
Repo.paginate(query,
fields: [{{:company, :name}, :asc}, id: :asc],
value_fun: fn
post, {{:company, name}, _} -> {:string, post.author.company.name}
post, field -> Ouroboros.value_fun_default(post, field)
end, limit: 50)
Functions
Generate a cursor for the supplied record, in the same manner as the
before
and after
cursors generated by paginate/3
.
For the cursor to be compatible with paginate/3
, fields
must have the same value as the fields
option passed to it.
Example
iex> Ouroboros.cursor_for_record(%Ouroboros.Customer{id: 1}, [:id])
"g2sAAQE"
iex> Ouroboros.cursor_for_record(%Ouroboros.Customer{id: 1, name: "Alice"}, [id: :asc, name: :desc])
"g2wAAAACYQFtAAAABUFsaWNlag"
Default function used to get the value of a cursor field from the supplied
map. This function can be overriden in the Ouroboros.Config
using the value_fun
key.
When using named bindings to sort on joined columns it will attempt to get the value of joined column by using the named binding as the name of the relationship on the original Ecto.Schema.
Example
iex> Ouroboros.type_fun_default(%Ouroboros.Customer{id: 1}, :id)
:id
iex> Ouroboros.type_fun_default(%Ouroboros.Customer{}, {:address, :city})
:string
iex> Ouroboros.type_fun_default(%Ouroboros.Customer{id: 1, address: %Ouroboros.Address{city: "London"}}, {:address, :city})
:string
iex> Ouroboros.type_fun_default(Ouroboros.Customer, :inserted_at)
:naive_datetime
Default function used to get the value of a cursor field from the supplied
map. This function can be overriden in the Ouroboros.Config
using the value_fun
key.
When using named bindings to sort on joined columns it will attempt to get the value of joined column by using the named binding as the name of the relationship on the original Ecto.Schema.
Example
iex> Ouroboros.value_fun_default(%Ouroboros.Customer{id: 1}, :id)
{:id, 1}
iex> Ouroboros.value_fun_default(%Ouroboros.Customer{id: 1, address: %Ouroboros.Address{city: "London"}}, {:address, :city})
{:string, "London"}