Original source: https://github.com/elbow-jason/dgraph_ex
Dlex is a gRPC based client for the Dgraph database in Elixir. It uses the DBConnection behaviour to support transactions and connection pooling.
Small, efficient codebase. Aims for a full Dgraph support. Supports transactions (starting from Dgraph version: 1.0.9), delete mutations and low-level parameterized queries. DSL is planned.
Now supports the new Dgraph 21.03 Type System.
If available in Hex, the package can be installed
by adding dlex to your list of dependencies in mix.exs:
Preferred and more performant option is to use grpc:
def deps do
[
{:jason, "~> 1.0"},
{:dlex, "~> 1.0.0"}
]
endhttp transport:
def deps do
[
{:jason, "~> 1.0"},
{:castore, "~> 1.0.7", optional: true},
{:mint, "~> 1.5", optional: true},
{:dlex, "~> 1.0.0"}
]
end# try to connect to `localhost:9080` by default
{:ok, conn} = Dlex.start_link(name: :example)
# clear any data in the graph
Dlex.alter!(conn, %{drop_all: true})
# add a term index on the `name` predicate
{:ok, _} = Dlex.alter(conn, "name: string @index(term) .")
# add nodes, returning the uids in the response
mut = %{
"name" => "Alice",
"friends" => [%{"name" => "Betty"}, %{"name" => "Mark"}]
}
{:ok, %{json: %{"uid" => uid}}} = Dlex.mutate(conn, mut, return_json: true)
# use the nquad format for mutations instead if preferred
Dlex.mutate(conn, ~s|_:foo <name> "Bar" .|)
# basic query that shows Betty
by_name = "query by_name($name: string) {by_name(func: eq(name, $name)) {uid expand(_all_)}}"
Dlex.query(conn, by_name, %{"$name" => "Betty"})
# delete the Alice node
Dlex.delete(conn, %{"uid" => uid})Modification of schema supported with string and map form (which is returned by query_schema):
Dlex.alter(conn, "name: string @index(term, fulltext, trigram) @lang .")
# equivalent map form
Dlex.alter(conn, [
%{
"predicate" => "name",
"type" => "string",
"index" => true,
"lang" => true,
"tokenizer" => ["term", "fulltext", "trigram"]
}
])The example below uses Grepo instead of Repo to enable use of dgraph along with another database. If dgraph is the only database required for the project, then the changes can be made directly in repo.ex:
defmodule App.Grepo do
use Dlex.Repo,
otp_app: :app,
modules: [App.Courses.Course]
endIn application.ex:
def start(_type, _args) do
# List all child processes to be supervised
children = [
# Start the Ecto repository
App.Repo,
# Start the endpoint when the application starts
AppWeb.Endpoint,
# Starts a worker by calling: App.Worker.start_link(arg)
# {App.Worker, arg},
App.Grepo,
]
...
enddefmodule App.Courses.Course do
use Dlex.Node
@derive {Phoenix.Param, key: :uid}
import Ecto.Changeset
schema "courses" do
field(:title, :string, index: ["term"])
field(:description, :string)
field(:guidelines, :string)
field(:status, :integer)
field(:has_learning_path, :uid)
end
@doc false
def changeset(course, attrs) do
course
|> cast(attrs, [
:title,
:description,
:guidelines,
:status,
:has_learning_path
])
end
end$ iex -S mix
# To view the schema:
iex> Grepo.snapshot()
# To execute the update (alter_schema uses snapshot() by default and can be omitted)
iex> Grepo.snapshot() |> Grepo.alter_schema()
iex> new_schema = [
%{
"predicate" => "name",
"type" => "string",
"index" => true,
"tokenizer" => ["term"]
},
%{
"predicate" => "age",
"type" => "int"
},
%{
"predicate" => "friend",
"type" => "[uid]"
}
]
iex> Grepo.alter_schema(new_schema)This follows the same pattern as a regular phoenix app context with the above setup:
defmodule App.Courses do
@moduledoc """
The Courses context.
"""
alias App.Grepo
alias App.Courses.Course
@doc """
Returns an `%Ecto.Changeset{}` for tracking course changes.
## Examples
iex> change_course(course)
%Ecto.Changeset{source: %Course{}}
"""
def change_course(%Course{} = course) do
Course.changeset(course, %{})
end
@doc """
Creates a course.
## Examples
iex> create_course(%{field: value})
{:ok, %Course{}}
iex> create_course(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_course(attrs \\ %{}) do
%Course{}
|> Course.changeset(attrs)
|> Grepo.set()
end
def course_statuses() do
[
{"Published", 1},
{"Unpublished", 2}
]
end
enddefmodule AppWeb.Admin.CourseController do
use AppWeb, :controller
alias App.Grepo
alias App.Courses
alias App.Courses.Course
def index(conn, _params) do
query = "{courses(func: type(courses)) {uid expand(_all_)}}"
{:ok, %{"courses" => courses}} = Grepo.all(query)
render(conn, "index.html", courses: courses)
end
def show(conn, %{"id" => id}) do
course = Grepo.get!(id)
render(conn, "show.html", course: course)
end
def new(conn, _params) do
changeset = Courses.change_course(%Course{})
render(conn, "new.html",
changeset: changeset,
statuses: course_statuses_for_select()
)
end
def create(conn, %{"course" => course_params}) do
case Courses.create_course(course_params) do
{:ok, _course} ->
render_success(
conn,
"Course created successfully",
Routes.admin_course_path(conn, :index)
)
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html",
changeset: changeset,
statuses: course_statuses_for_select()
)
end
end
def edit(conn, %{"id" => id}) do
course = Grepo.get!(id)
changeset = Courses.change_course(course)
render(conn, "edit.html",
course: course,
changeset: changeset,
statuses: course_statuses_for_select()
)
end
def update(conn, %{"id" => id, "course" => course_params}) do
course = Grepo.get!(id)
case Courses.update_course(course, course_params) do
{:ok, course} ->
render_success(
conn,
"Course updated successfully",
Routes.admin_course_path(conn, :show, course)
)
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "edit.html",
course: course,
changeset: changeset,
statuses: course_statuses_for_select()
)
end
end
defp course_statuses_for_select, do: Courses.course_statuses()
endDefine a LiveView
defmodule MyAppWeb.UserLive.Index do
use MyAppWeb, :live_view
alias MyApp.Grepo
alias MyApp.User
@impl true
def mount(_params, _session, socket) do
{:ok, fetch_users(socket)}
end
defp fetch_users(socket) do
query = "{users(func: type(User)) {uid name age}}"
{:ok, %{"users" => users}} = Grepo.all(query)
assign(socket, users: users)
end
@impl true
def handle_event("delete", %{"uid" => uid}, socket) do
{:ok, _} = Grepo.delete(%{uid: uid})
{:noreply, fetch_users(socket)}
end
endDGraph data type
Dgraph supports various data types that allow for flexible and powerful schema definitions. Here is a list of the supported types:
-
Scalar Types:
string: A sequence of characters.int: An integer value.float: A floating-point number.bool: A boolean value (true or false).datetime: Date and time values.geo: Geospatial data (e.g., points, polygons).password: A hashed password string.
-
Collection Types:
list: A list of values of a specific type.set: A set of unique values of a specific type.
-
Uid Type:
uid: A unique identifier used to represent edges between nodes.
-
Default Types:
default: Used for predicates where the type is not explicitly specified. This type is inferred based on the data provided.
-
Other Types:
dateTime: Another notation for datetime values.default: This type is used when the type is not specified and will be inferred from the provided data.
type User {
username: string @index(exact) .
age: int @index(int) .
balance: float .
registered: datetime .
location: geo @index(geo) .
friends: [uid] @reverse .
password: password .
}- String:
name: string @index(term) .
- Int:
age: int @index(int) .
- Float:
balance: float .
- Bool:
isActive: bool .
- Datetime:
registered: datetime .
- Geo:
location: geo @index(geo) .
- Password:
password: password .
- Uid:
friend: uid @reverse .
For detailed information and updates, you can refer to the official Dgraph documentation on schema.
- Install dependencies
mix deps.get - Start the local dgraph server (requires Docker)
./start-server.shThis starts a local server bound to ports 9090 (GRPC) and 8090 (HTTP) - Run
mix test
NOTE: You may stop the server using ./stop-server.sh
- Install
protoc(cpp) here orbrew install protobufon MacOS. - Install protoc plugin
protoc-gen-elixirfor Elixir. NOTE: You have to make sureprotoc-gen-elixir(this name is important) is in your PATH.
mix escript.install hex protobuf- Generate Elixir code using protoc
protoc --elixir_out=plugins=grpc:. lib/api.proto-
Files
lib/api.pb.exwill be generated -
Rename
lib/api.pb.extolib/dlex/api.exand addalias Dlex.Apito be compliant with Elixir naming conventions
Inspired by exdgraph, but with significant differences to achieve goals of a small codebase, efficient natural abstraction, and fewer dependencies.
You can freely choose which pool implementation to use (poolboy or db_connection internal pool implementation) or which JSON adapter to use. Fewer dependencies.
It feels more natural to have API names closely matching actual query names.
For example, Dlex.mutate() instead of ExDgraph.set_map for JSON-based mutations. Dlex.mutate infers the type (JSON or nquads) from the data passed to a function.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.