Giter Club home page Giter Club logo

query_builder's Introduction

Query Builder

Query Builder allows you to build and compose Ecto queries based on data.

User
|> QueryBuilder.where(firstname: "John")
|> QueryBuilder.where([{:age, :gt, 30}, city: "Anytown"])
|> QueryBuilder.order_by(asc: :lastname)
|> QueryBuilder.preload([:role, authored_articles: :comments])
|> QueryBuilder.offset(20)
|> QueryBuilder.limit(10)
|> Repo.all()

This allows writing queries more concisely, without having to deal with bindings and macros.

Its primary goal is to allow Context functions to receive a set of filters and options:

Blog.list_articles(preload: :comments, order_by: [asc: :title])
Blog.list_articles(preload: [:category, comments: :user])

This avoids having to create many different functions in the Context for every combination of filters and options, or to create one general function that does too much to satisfy all the consumers.

The calling code (e.g. the Controllers), can now retrieve the list of articles with different options. In some part of the application, the category is needed; in other parts it is not; sometimes the articles must be sorted based on their title; other times it doesn't matter, etc.

See QueryBuilder.from_list/2 below.

Examples

User
|> QueryBuilder.where(firstname: "John")
|> QueryBuilder.where([{:age, :gt, 30}, city: "Anytown"])
|> QueryBuilder.order_by(asc: :lastname)
|> QueryBuilder.preload([:role, authored_articles: :comments])
|> QueryBuilder.offset(20)
|> QueryBuilder.limit(10)
|> Repo.all()

Filtering on associations is supported:

User
|> QueryBuilder.where(:role, name@role: "admin")
|> Repo.all()
User
|> QueryBuilder.where([role: :permissions], name@permissions: "delete")
|> Repo.all()
Article
|> QueryBuilder.where(:author, id@author: author_id)
|> QueryBuilder.where([:author, :comments], {:logged_at@author, :lt, :inserted_at@comments})
|> QueryBuilder.preload(:comments)
|> Repo.all()

Usage

Add use QueryBuilder in your schema:

defmodule MyApp.User do
  use Ecto.Schema
  use QueryBuilder

  schema "users" do
    # code
  end

  # code
end

You may also specify the schema's associations to QueryBuilder in order to remedy some limitations when building queries:

defmodule MyApp.User do
  use Ecto.Schema
  use QueryBuilder, assoc_fields: [:role, :articles]

  schema "users" do
    # code
    belongs_to :role, MyApp.Role
    has_many :articles, MyApp.Article
  end

  # code
end

Currently, supported operations are:

QueryBuilder.paginate/3

%{paginated_entries: paginated_entries, pagination: pagination} =
  User
  |> QueryBuilder.order_by(asc: :nickname, desc: :email)
  |> QueryBuilder.paginate(Repo, page_size: 10)

%{
  cursor_direction: :after,
  cursor_for_entries_before: cursor_for_entries_before,
  cursor_for_entries_after: cursor_for_entries_after,
  has_more_entries: has_more_entries?,
  max_page_size: 10
} = pagination

# for subsequent request you may use the cursor:

User
|> QueryBuilder.order_by(asc: :nickname, desc: :email)
|> QueryBuilder.paginate(Repo, page_size: 10, cursor: cursor_for_entries_after, direction: :after)

You may configure a global default page size via config:

config :query_builder, :default_page_size, 100

QueryBuilder.where/2

QueryBuilder.where(query, firstname: "John")

QueryBuilder.where/4

QueryBuilder.where(query, [role: :permissions], name@permissions: :write)

Above where functions support different filter operations, for instance:

QueryBuilder.where(query, {:age, :greater_than, 18})

Supported filter operations are:

  • :equal_to (or :eq)
  • :other_than (or :ne)
  • :greater_than (or :gt)
  • :greater_than_or_equal_to (or :ge)
  • :less_than (or :lt)
  • :less_than_or_equal_to (or :le)
  • :like
  • :ilike
  • :starts_with
  • :ends_with
  • :contains

Array inclusion checking:

  • :in
  • :not_in
  • :include
  • :exclude

Note that :starts_with, :ends_with and :contains operations can be written using :like, but offer a more declarative style and are safer, as they escape the % and _ characters for you. You may also perform case insensitive searchs using these functions. Example:

QueryBuilder.where({:name, :starts_with, "jo"})
QueryBuilder.where({:name, :starts_with, "jo", case: :insensitive}) # `:i` will also work

When using :like or :ilike, make sure to escape % and _ characters properly.

You may also add OR clauses through QueryBuilder.where/4's fourth argument:

QueryBuilder.where(query, [], [name: "John"], or: [name: "Alice", age: 42], or: [name: "Bob"])

QueryBuilder.maybe_where/3 and QueryBuilder.maybe_where/5

query
|> QueryBuilder.maybe_where(some_condition, name: "Alice")

The above will run where/2 if the given condition is met.

QueryBuilder.order_by/2

QueryBuilder.order_by(query, asc: :lastname, asc: :firstname)

QueryBuilder.order_by/3

QueryBuilder.order_by(query, :articles, asc: :title@articles)

QueryBuilder.maybe_order_by/3 and QueryBuilder.maybe_order_by/4

query
|> QueryBuilder.maybe_order_by(some_condition, asc: :lastname, asc: :firstname)

The above will run order_by/2 if the given condition is met.

QueryBuilder.preload/2

QueryBuilder.preload(query, [role: :permissions, articles: [:stars, comments: :user]])

QueryBuilder.left_join/4

QueryBuilder.left_join(query, :articles, title@articles: "Foo", or: [title@articles: "Bar"])

QueryBuilder.offset/2

QueryBuilder.offset(query, 10)

QueryBuilder.limit/2

QueryBuilder.limit(query, 10)

QueryBuilder.from_list/2

QueryBuilder.from_list(query, [
  where: [name: "John", city: "Anytown"],
  preload: [articles: :comments],
  order_by: {:articles, asc: :title@articles},
  limit: 20,
  offset: 10
])

The QueryBuilder.from_list/2 function was the main motivation behind the writing of this library. As explained above, it allows to add querying options to the Context functions. Example:

defmodule MyApp.Blog do
  alias MyApp.Blog.Article

  def get_article_by_id(id, opts \\ []) do
    QueryBuilder.where(Article, id: id)
    |> QueryBuilder.from_list(opts)
    |> Repo.one!()
  end
end

The function can now be called as follows (for instance, from a Controller):

Blog.get_article_by_id(id, preload: [:comments])

Blog.get_article_by_id(id, preload: [:likes])

Blog.get_article_by_id(
  id,
  order_by: {:comments, desc: :inserted_at@comments},
  preload: [comments: :user]
)

Extending Query Functions

In the event that you want to extend QueryBuilder's functionality to include custom app specific query functions, there's the QueryBuilder.Extension module to facilitate that. You can create a module with your app specific query functionality and use the QueryBuilder.Extension module to inject all QueryBuilder functions into your custom module. Any custom query functions added to your custom module are included in QueryBuilder.from_list/2.

For example:

defmodule MyApp.QueryBuilder do
  use QueryBuilder.Extension

  defmacro __using__(opts) do
    quote do
      require QueryBuilder
      QueryBuilder.__using__(unquote(opts))
    end
  end

  # Add app specific query functions here...

  def where_initcap(query, field, value) do
    text_equals_condition = fn field, value, get_binding_fun ->
      {field, binding} = get_binding_fun.(field)
      Ecto.Query.dynamic([{^binding, x}], fragment("initcap(?)", ^value) == field(x, ^field))
    end

    query
    |> where(&text_equals_condition.(field, value, &1))
  end
end

defmodule MyApp.Accounts.User do
  use MyApp.QueryBuilder

  schema "users" do
    field :name, :string
    field :active, :boolean
  end
end

defmodule MyApp.Accounts do
  alias MyApp.QueryBuilder, as: Query

  def list_users(opts \\ []) do
    # Query list can include custom query functions as well:
    # [where_initcap: {:name, "john"}, where: {:active, true}]
    MyApp.Accounts.User
    |> Query.from_list(opts)
    |> Repo.all()
  end
end

Special Considerations

With the auto-binding functionality offered by Query Builder, you can specify field comparisons in queries using the special atom value syntax: :<field_name>@self. This way Query Builder understands the intent is to compare fields vs a raw value. For example:

users_where_name_matches_nickname =
  User
  |> QueryBuilder.where({:name, :eq, :nickname@self})
  |> Repo.all()

would resolve to a query along the following form:

select * from users where users.name = users.nickname

Installation

Add query_builder for Elixir as a dependency in your mix.exs file:

def deps do
  [
    {:query_builder, "~> 1.4.2"}
  ]
end

HexDocs

HexDocs documentation can be found at https://hexdocs.pm/query_builder.

query_builder's People

Contributors

jmillxyz avatar mathieuprog avatar onomated avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

query_builder's Issues

Atom values in where args treated as schema fields

The following query builder operation:

args_collected_somewhere = [auth_method: :email, auth_val: "[email protected]"]
User
|> QueryBuilder.where(args_collected_somewhere)
|> Repo.one!()

is resolving to the following sql and error:

SELECT a0.* FROM "users" AS a0 WHERE ((a0."auth_val" = $1) AND (a0."auth_method" = a0."email"))

** (Postgrex.Error) ERROR 42703 (undefined_column) column a0.email does not exist

This is problematic with values that are actually enums, particularly when using enum libraries such as Ecto Enum. This behavior differs from Ecto which works as expected i.e.

args_collected_somewhere = [auth_method: :email, auth_val: "[email protected]"]
User
|> Repo.get_by!(args_collected_somewhere)

resolves to the following SQL which executes with no errors

SELECT a0.* FROM "users" AS a0 WHERE ((a0."auth_val" = $1) AND (a0."auth_method" = "email"))

** (Postgrex.Error) ERROR 42703 (undefined_column) column a0.email does not exist

For more, see discussion in Elixir Forum

Perform a "where ... in ..." passing a list of possible matches

I would like to run a query that runs where(q, [u], u.status in ^status)
My status here would be a list of status say ["APPROVED", "SUSPENDED"]

None of the available where params seem to have the ability to have pass that structure, seems like the only possible way around is is to Enum and add or_where clauses.

Querybuilder is modifying original query.

Thanks a lot for this awesome package. It has simplified searching for us.

iex(9)> query = from c in Country, limit: 2
#Ecto.Query<from c0 in ClocheApi.Locations.Country, limit: ^2>

iex(10)> QueryBuilder.order_by(query, desc: :inserted_at)              
#Ecto.Query<from c0 in ClocheApi.Locations.Country,
 as: ClocheApi.Locations.Country, order_by: [desc: c0.inserted_at]>

โ˜๐Ÿผ As shown above, the original query has limit in it.
However when we use order_by it removes the limit and modifies the original query instead of enhancing it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.