Context and relations

I was thinking about relations spanning multiple context. Using natural example:

defmodule MyApp.Accounts.User do
  use Ecto.Schema

   schema "users" do
        field :name, :string
        field :email, :string
        field :password, :string
        
        has_many :posts, MyApp.Blog.Post
        has_many :comments, MyApp.Blog.Comment
        
        has_one :stats, MyApp.Metrics.Stats
   end
end

it doesn’t look well. We create god schema, which knows about every other context and it’s schemas.
Alternatively, we can repeat schemas for different contexts:

defmodule MyApp.Accounts.User do
   use Ecto.Schema

    schema "accounts_users" do # or "users"
       field :email, :string
       field :password, :string
   end
end

defmodule MyApp.Blog.User do
  use Ecto.Schema

  schema "blog_users" do # or "users"
      field :name, :string

       has_many :posts, MyApp.Blog.Post
       has_many :comments, MyApp.Blog.Comment   
  end
end 

Which provides nice separation, but doesn’t explain what to do when two contexts cross:

accounts_user = conn.assigns.current_user

# Accounts user know nothing about comments 
MyApp.Blog.list_comments_of_user(accounts_user)

Now, how should list_comments_of_user be implemented?
We can read id from user, and then use it on comments:

 def list_comments_of_user(%{id: user_id}) do
     from c in Comments, where: [user_id: ^user_id]
 end

which is fine for simple belongs_to association, but gets tricky in has_many through and many to many, or when foreign keys are not as obvious.
The best-case scenario would be to use Ecto.assoc, but Accounts.User knows nothing about posts.

The cleanest way would be to query db with user_id, load Blog.User and work with that.

I was thinking about different solution, namely:

 def list_comments_of_user(user) do
      cast_struct(user, to: %Blog.User{})
      |> Ecto.assoc(:comments)
  end
  
 def cast_struct(data, target_struct) do
      struct(target_struct, Map.from_struct(data))
 end

This way we can pass user from any context, be it Blog, Accounts or whatever, and we always get the benefit of associactions (and domain-specific fields), which are defined in the same context. We can even change every Blog.User to Blog.Author, user_id to author_id, and Blog part of the app becomes blisfully unaware of such things like users, passwords or emails.

What do you think about this approach?

1 Like