How to preload associations in Ecto and then access fields from those associations?

Hi all,

I have a question regarding associations. I’m trying to preload some associations (a patient belongs_to cdoctor, gp and pharmacy) and then access fields from those associations.

The page is being used to show a summary of all patients in the system, listed as a table with, where each row has a patient, their cdoctor, their gp and their pharmacy.

Each patient holds a value for gp_id, pharm_id, cdoctor_id.

Currently the page shows the following (as expected):

I however want to show the GP name, Pharmacy name etc. instead of their ID, which I believe is possible by preloading the associations.

The question I have is how do you do this? I’ve had a read online, but I can’t understand some of the explanations, and none seem to demonstrate how to access those variables (my idea of how it works is dot-walking, perhaps like patient.gp_id.name).

Here’s my current code for preloading, which I’m almost certain is incorrect, but compiles successfully:

patients = Repo.all(Patient) |> Repo.preload([:gps, :pharms, :cdoctors])

And here’s the page displaying the data:

  <h2>Patients</h2>
  <div class="panel panel-primary">    
    <table class="sortable table">
      <thead>
        <tr>
          <th scope="col"><button type="button" class="btn btn-primary">Patient Name</button></th>
          <th scope="col"><button type="button" class="btn btn-primary">GP</button></th>
          <th scope="col"><button type="button" class="btn btn-primary">Clinic Doctor</button></th>
          <th scope="col"><button type="button" class="btn btn-primary">Pharmacy</button></th>
        </tr>
      </thead>
      <tbody>
        <%= for  p  <-  @patients  do %>
        <tr>
          <th scope="row"><%= link p.fname<>" "<>p.lname, to: patient_path(@conn, :show, p), class: "" %><br></th>
          <td><%= link p.gp_id, to: gp_path(@conn, :show, p.gp_id), class: "" %></td>
          <td><%= link p.cdoctor_id, to: c_doctor_path(@conn, :show, p.cdoctor_id), class: "" %></td>
          <td><%= link p.pharm_id, to: pharmacy_path(@conn, :show, p.pharm_id), class: "" %></td>
        </tr>
        <%= end %>
      </tbody>
    </table>
  </div>
  <script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>

Any helpful advice would be greatly appreciated. I can attach the code from the models if necessary, however this post is already long enough imo!

Thanks in advance

Your schema needs to reference the relationship and then you access via that defined relationship. For example, in your defined schema it appears that a patient has a single pharmacy. So in your schema definition for patient you should have:

has_one(:pharmacy, Pharmacy)

Then once you have a patient with the pharmacy preloaded you can do.

p.pharmacy.name.

The pharmacy is filled in with the instance of your Pharmacy row from the database. The relationship in the db itself is via the pharmacy_id but when you’re dealing with it in your code you want to deal with it as a full Pharmacy struct.

I hope this makes more sense. I think once you do a couple of these it’ll make sense to you.

https://hexdocs.pm/ecto/Ecto.Schema.html#has_one/3

1 Like

First of all, what @fmcgeough is saying is true. Before using preload you need to make sure the association is defined in the schema. In the case of your patient, the schema should have a:

belongs_to(:gp, GeneralPractitioner)
belongs_to(:pharm, Pharmacy)
belongs_to(:cdoctor, Doctor)

Once the association has been defined, you can call:

patients = Repo.all(Patient) |> Repo.preload([:gp, :pharm, :cdoctor])

Notice that I’m using singular names in the preload call, these names refer to the field name you defined with the belongs_to function.

Once the association is preloaded, you can access the gp name with patient.gp.name

Make sure you have a look at Phoenix Contexts as well, they might help you organize your code better.

Thanks for a helpful and informative reply!

As it stands, this is the current association state:

GP:
has_many :patients, Cmsv1.Patient
CDoctor:
has_many :patients, Cmsv1.Patient
Pharmacy:
has_many :patients, Cmsv1.Patient
Patient:

belongs_to :gps, Cmsv1.GP, foreign_key: :gp_id, type: :binary_id, references: :gp_id

belongs_to :cdoctors, Cmsv1.CDoctor, foreign_key: :cdoctor_id, type: :binary_id, references: :cdoctor_id

belongs_to :pharms, Cmsv1.Pharmacy, foreign_key: :pharm_id, type: :binary_id, references: :pharm_id

Does this seem correct?

EDIT: It seems to be correct, as the functionality is now working (I was referencing them wrong while dot-walking).

Thank you for all your help!

Thank you for the help, I will definitely have a look at that!

Just a minor detail, it reads better if your belongs_to relation names are defined in singular, like:

alias Cmsv1.CDoctor
...
belongs_to(:cdoctor, CDoctor, type: :binary_id)
...
1 Like