Exploring Phoenix: Umbrella Project Structures,Ecto & More

Avatar

By squashlabs, Last Updated: June 21, 2023

Exploring Phoenix: Umbrella Project Structures,Ecto & More

Effective Umbrella Project Structures

When building complex applications with Phoenix, organizing your project structure is crucial for maintainability and scalability. The umbrella project structure is a recommended approach for structuring larger applications that consist of multiple smaller applications or services.

In an umbrella project, you have a top-level application that acts as a container for multiple child applications. Each child application can have its own context, controllers, views, and other components. This modular approach allows you to separate concerns and manage dependencies more effectively.

To create an umbrella project in Phoenix, you can use the mix command with the new flag and specify the --umbrella option. Here's an example:

mix new my_app --umbrella

This will create a new umbrella project with a top-level my_app application and an apps directory where you can add your child applications.

Let's say we want to create a blog application with separate modules for authentication, user management, and blog posts. We can create three child applications within our umbrella project:

cd my_app/apps
mix phx.new auth --no-ecto
mix phx.new users --no-ecto
mix phx.new blog --no-ecto

In this example, we used the --no-ecto flag to exclude Ecto, as we'll cover advanced Ecto techniques in a later section.

Each child application will have its own router, context, controllers, views, and other necessary components. You can also share common code and dependencies between the child applications by adding them to the top-level mix.exs file.

The umbrella project structure provides several advantages:

1. Modularity: Each child application can be developed, tested, and deployed independently, allowing for easier collaboration and scalability.

2. Separation of concerns: By separating different modules into individual applications, you can maintain a cleaner codebase and easily manage dependencies.

3. Code organization: With the umbrella project structure, it's easier to locate and navigate through the different parts of your application, making maintenance and debugging more efficient.

Related Article: Optimizing Database Queries with Elixir & Phoenix

Example: Adding a new child application

Let's say we want to add a new child application called comments to our blog umbrella project. We can use the following command:

cd my_app/apps
mix phx.new comments --no-ecto

This will create a new child application called comments within the apps directory. You can then configure the router, context, controllers, and views for the comments application as needed.

Example: Sharing code between child applications

To share code between child applications, you can add dependencies and common modules to the top-level mix.exs file. For example, let's say we want to share a utility module called Blog.Utils between the users and blog applications.

In the top-level mix.exs file, add the :users and :blog applications as dependencies and specify the :only option to include only the necessary modules:

defp deps do
  [
    {:users, in_umbrella: true, only: [:utils]},
    {:blog, in_umbrella: true, only: [:utils]}
  ]
end

Now, you can use the Blog.Utils module in both the users and blog applications.

LiveView Optimizations

Phoenix LiveView is a useful library that allows you to build interactive, real-time web applications using server-rendered HTML. However, as your application grows in complexity and user traffic increases, you may encounter performance issues. Here are some best practices and optimizations to improve the performance of your LiveView applications.

1. Minimize server round-trips: LiveView leverages Phoenix Channels to establish a bidirectional connection between the client and the server. Each time a user interacts with a LiveView component, such as submitting a form or clicking a button, the client sends a message to the server, which in turn updates the LiveView state and renders the changes. To minimize server round-trips, use the phx-debounce attribute to delay the transmission of user events. By batching multiple events together, you can reduce the number of server requests and improve performance.

<button>Click Me</button>

In this example, the phx-debounce attribute delays the phx-click event by 300 milliseconds, allowing time to collect other potential events before making a server request.

2. Use Phoenix PubSub for interactivity: Phoenix PubSub is a publish-subscribe system built into Phoenix that allows you to broadcast events to LiveView processes. Instead of relying solely on server round-trips, you can use PubSub to send real-time updates to connected LiveView components. This can significantly reduce the number of server requests and improve interactivity. For example, you can use PubSub to broadcast updates to all connected users when a new comment is posted.

def handle_event("new_comment", %{"content" =&gt; content}, socket) do
  # Save the comment to the database

  Phoenix.PubSub.broadcast(MyApp.PubSub, "new_comment", %{
    "content" =&gt; content
  })

  {:noreply, socket}
end

In this example, the handle_event/3 function broadcasts a "new_comment" event using PubSub. Connected LiveView components can subscribe to this event and update their state accordingly.

3. Optimize rendering with diffs: LiveView uses a virtual DOM diffing algorithm to update the client-side HTML efficiently. By default, LiveView compares the entire rendered HTML to the new HTML after a server update. However, you can optimize this process by using the phx-update attribute to specify which parts of the HTML to update. This can reduce the amount of DOM manipulation and improve rendering performance.

<div>
  <!-- Content that doesn't need to be updated -->
</div>

<div>
  <!-- Content that should be appended to the existing DOM -->
</div>

<div>
  <!-- Content that should replace the existing DOM -->
</div>

In this example, the phx-update attribute is used to specify different update strategies for different parts of the HTML.

Related Article: Building Real-Time Apps with Phoenix Channels & WebSockets

Example: Debouncing user input

To debounce user input in a LiveView component, use the phx-debounce attribute with the desired delay in milliseconds. For example, let's debounce a text input field to delay the transmission of user input by 500 milliseconds:


In this example, the phx-value attribute binds the input field to the name property in the LiveView state. The phx-debounce attribute delays the transmission of user input by 500 milliseconds.

Example: Broadcasting updates with PubSub

To broadcast updates to connected LiveView components using Phoenix PubSub, you can use the Phoenix.PubSub.broadcast/3 function. For example, let's broadcast a "new_comment" event with the comment content to all connected users:

def handle_event("new_comment", %{"content" =&gt; content}, socket) do
  # Save the comment to the database

  Phoenix.PubSub.broadcast(MyApp.PubSub, "new_comment", %{
    "content" =&gt; content
  })

  {:noreply, socket}
end

In this example, the handle_event/3 function broadcasts a "new_comment" event using the Phoenix.PubSub.broadcast/3 function. The event payload includes the comment content.

Advanced Ecto Techniques

Ecto is the database layer in Phoenix that provides a useful and flexible way to interact with databases. In this section, we'll explore some advanced Ecto techniques that can enhance your application's functionality.

Ecto Multi-Tenancy

Multi-tenancy is a common requirement in applications where multiple clients or organizations share the same software instance but have their own isolated data. Ecto provides several techniques to implement multi-tenancy.

One approach is to use a separate database schema for each tenant. This allows you to isolate the data and apply tenant-specific configurations. To dynamically switch between tenant schemas, you can use Ecto's Repo module and the Ecto.Schema macro.

Here's an example of how you can implement multi-tenancy with separate schemas:

defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :my_app
end

defmodule MyApp.TenantRepo do
  alias MyApp.Repo

  defdelegate all(tenant), to: Repo, as: :all
  defdelegate get(tenant, id), to: Repo, as: :get
  defdelegate insert(tenant, params), to: Repo, as: :insert

  defp schema_prefix(tenant) do
    "tenant_#{tenant}_"
  end

  defmacro __using__(tenant) do
    quote do
      use Ecto.Schema, prefix: unquote(schema_prefix(tenant))
      import Ecto.Changeset

      @tenant unquote(tenant)

      schema "table_name" do
        # define your schema fields and associations
      end

      def changeset(struct, params \\ %{}) do
        struct
        |&gt; cast(params, @required_fields, @optional_fields)
        |&gt; validate_required(@required_fields)
      end
    end
  end
end

defmodule MyApp.Tenant do
  use MyApp.TenantRepo, "my_tenant"

  # define tenant-specific functions and validations
end

In this example, we define a MyApp.TenantRepo module that delegates database operations to the main MyApp.Repo module but prefixes the schema with the tenant name. We also define a MyApp.Tenant module that uses the MyApp.TenantRepo module and can be used to perform CRUD operations on the tenant-specific schema.

Related Article: Implementing Enterprise Features with Phoenix & Elixir

Ecto Aggregates

Aggregates in Ecto allow you to perform complex queries that involve calculations, grouping, and aggregations on your database. Ecto provides a set of functions and macros to work with aggregates, such as count, sum, avg, group_by, and more.

Here's an example of how you can use aggregates in Ecto:

defmodule MyApp.User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :age, :integer

    timestamps()
  end
end

In this example, we have a users table with name and age columns.

defmodule MyApp.UserStats do
  alias Ecto.Query

  def age_stats do
    from(u in User,
      select: %{average_age: avg(u.age), min_age: min(u.age), max_age: max(u.age)}
    )
    |&gt; Repo.one()
  end
end

In this example, the age_stats function calculates the average, minimum, and maximum age of all users in the database using the avg, min, and max aggregate functions. The result is a map with the calculated statistics.

Ecto CTEs

Common Table Expressions (CTEs) allow you to define temporary named result sets within a query and reference them multiple times. This can be useful for complex queries that involve subqueries or recursive queries.

Ecto supports CTEs through its Ecto.Query.API.with_cte/3 function. Here's an example of how you can use CTEs in Ecto:

defmodule MyApp.User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :manager_id, :integer

    timestamps()
  end
end

In this example, we have a users table with name and manager_id columns.

defmodule MyApp.UserHierarchy do
  alias Ecto.Query

  def manager_subordinates(manager_id) do
    query =
      from u in User,
        with_cte subordinates_query &lt;- from(u in User, where: u.manager_id == ^manager_id),
        select: u.name,
        where: u.manager_id == ^manager_id or u.id in subordinates_query

    Repo.all(query)
  end
end

In this example, the manager_subordinates function retrieves all users who directly report to a given manager, as well as their subordinates. It uses a CTE named subordinates_query to define a subquery that retrieves the subordinates of a given manager.

Phoenix LiveView Best Practices

Phoenix LiveView is a useful tool for building real-time, interactive web applications. To make the most out of LiveView, here are some best practices to follow:

1. Keep the LiveView state minimal: The LiveView state represents the current state of the UI on the server. It's important to keep the state minimal to minimize the amount of data that needs to be sent back and forth between the client and the server. Avoid storing unnecessary data in the LiveView state and only include data that is required for rendering and interactivity.

2. Use LiveView hooks for client-side interactivity: LiveView hooks allow you to add client-side interactivity to LiveView components. Hooks are JavaScript functions that run on the client-side and can be used to perform DOM manipulation, event handling, and other client-side operations. Use hooks to enhance the interactivity of your LiveView components while keeping the server-side logic focused on data management.

3. Leverage the phx-change event for data synchronization: The phx-change event is triggered when an input field in a LiveView component changes. You can use this event to synchronize the data between the client and the server. For example, you can send the updated field value to the server and update the LiveView state accordingly. This ensures that the server and the client are always in sync.

4. Handle server-side validation and error handling: LiveView provides built-in support for server-side validation and error handling. Instead of relying solely on client-side validation, perform server-side validation to ensure data integrity and security. Use the phx-feedback-for function to display validation errors and other feedback from the server.

5. Optimize rendering with LiveView assigns: LiveView assigns allow you to pass data from the server to the client for rendering. When updating the LiveView state, only update the necessary assigns to minimize the amount of data that needs to be sent to the client. This can improve rendering performance and reduce network traffic.

6. Use phx-throttle to limit server requests: The phx-throttle attribute allows you to limit the number of server requests sent by LiveView components. By setting a throttle interval, you can prevent excessive server requests caused by rapid user interactions. This can help optimize performance and prevent unnecessary server load.

7. Test LiveView components thoroughly: LiveView components should be tested thoroughly to ensure their correctness and reliability. Use the built-in testing utilities provided by Phoenix to write tests for your LiveView components. Test both the server-side logic and the client-side interactivity to cover all aspects of your LiveView components.

Example: Using a LiveView hook for client-side interactivity

To add client-side interactivity to a LiveView component, you can use a LiveView hook. Here's an example of a hook that adds a click event listener to a button in a LiveView component:

// assets/js/app.js

import { LiveSocket } from "phoenix_live_view";

let liveSocket = new LiveSocket("/live");
liveSocket.connect();

let Hooks = {};

Hooks.ClickButton = {
  mounted() {
    let button = this.el.querySelector("button");

    button.addEventListener("click", () =&gt; {
      // Perform client-side logic
    });
  }
};

liveSocket.hook(Hooks);

In this example, the ClickButton hook adds a click event listener to the button element in the LiveView component. When the button is clicked, the client-side logic inside the event listener is executed.

To use the hook in your LiveView component, add the phx-hook attribute to the button element:

<button>Click Me</button>

Related Article: Elixir’s Phoenix Security: Token Auth & CSRF Prevention

Example: Handling server-side validation and error handling

To handle server-side validation and error handling in a LiveView component, you can use the phx-feedback-for function. Here's an example:

  <ul>
    &lt;%= for {field, message} 
      &lt;li phx-feedback-for=&quot;"&gt;</li>
    
  </ul>

In this example, if @errors is not empty, a list of error messages is rendered. Each error message is associated with a specific field using the phx-feedback-for function. This allows LiveView to display the error message next to the corresponding field.

To trigger server-side validation and error handling, you can use the phx-change event in an input field:

&lt;input type=&quot;text&quot; phx-change=&quot;validateField&quot; phx-value=&quot;"&gt;

In this example, the phx-change event is triggered when the input field changes. The validateField function in the LiveView handles the event and performs server-side validation. The error messages are then assigned to the @errors assign for rendering.

You May Also Like

Phoenix Design Patterns: Actor Model, Repositories, and Events

Design patterns are essential for building robust and scalable applications. In this article, we will explore various design patterns in Phoenix, suc… read more

Deployment Strategies and Scaling for Phoenix Apps

Shipping, scaling, and monitoring Phoenix applications can be a complex task. This article provides proven techniques and best practices for deployin… read more

Integrating Phoenix Web Apps with Payment, Voice & Text

Integrate Phoenix web apps with payment platforms and communication functionalities using Elixir. Learn how to set up voice and SMS functionalities, … read more

Phoenix Core Advanced: Contexts, Plugs & Telemetry

Delve into advanced Phoenix Core concepts with this article, which explores plug constructs, domain-driven design contexts, and custom telemetry even… read more

Internationalization & Encoding in Elixir Phoenix

The article delves into the management of Unicode and encodings in Elixir Phoenix. It explores how Phoenix handles localization and offers best pract… read more

Phoenix with Bootstrap, Elasticsearch & Databases

Enhance your Phoenix applications by integrating with Bootstrap, Elasticsearch, and various databases. From configuring Phoenix with Ecto for MySQL, … read more