Do you want to pick up from where you left of?
Take me there

Συσχετισμοί

Σε αυτό τον τομέα θα μάθουμε πως να χρησιμοποιήσουμε το Ecto για να ορίσουμε και να εργαστούμε με συσχετισμούς ανάμεσα στα σχήματά μας.

Στήσιμο

Θα ξεκινήσουμε με την ίδια εφαρμογή Friends από τα προηγούμενα μαθήματα. Μπορείτε να δείτε το στήσιμο εδώ για μια γρήγορη ανασκόπηση.

Τύποι Συσχετισμών

Υπάρχουν τρεις τύποι συσχετισμών που μπορούμε να ορίσουμε μεταξύ των σχημάτων μας. Θα δούμε τι είναι και πως να υλοποιήσουμε κάθε τύπο σχέσης.

Ανήκει Σε / Έχει Πολλά

Θα προσθέσουμε μερικές νέες οντότητες στην εφαρμογή μας Friends ώστε να μπορέσουμε να καταχωρήσουμε τις αγαπημένες μας ταινίες. Θα ξεκινήσουμε με δύο σχήματα: Τα Movie και Character. Θα υλοποιήσουμε μια σχέση “έχει πολλά/ανήκει σε” ανάμεσα σε αυτά τα δύο σχήματα: Μια ταινία έχει πολλούς χαρακτήρες και ένας χαρακτήρας ανήκει σε μια ταινία.

Η Μετατροπή για το Έχει Πολλά

Ας δημιουργήσουμε μια μετατροπή για την Movie:

mix ecto.gen.migration create_movies

Ανοίξτε το πρόσφατα δημιουργημένο αρχείο μετατροπής και ορίστε τη συνάρτηση change ώστε να δημιουργήσετε τον πίνακα movies με μερικά χαρακτηριστικά:

# priv/repo/migrations/*_create_movies.exs
defmodule Friends.Repo.Migrations.CreateMovies do
  use Ecto.Migration

  def change do
    create table(:movies) do
      add :title, :string
      add :tagline, :string
    end
  end
end

Το σχήμα για το Έχει Πολλά

Θα προσθέσουμε ένα σχήμα που ορίζει τη σχέση “έχει πολλά” ανάμεσα στις ταινίες και τους χαρακτήρες της.

# lib/friends/movie.ex
defmodule Friends.Movie do
  use Ecto.Schema

  schema "movies" do
    field :title, :string
    field :tagline, :string
    has_many :characters, Friends.Character
  end
end

Η μακροεντολή has_many/3 δεν προσθέτει τίποτα στην ίδια τη βάση δεδομένων. Αυτό που κάνει είναι να χρησιμοποιεί το ξένο κλειδί στο συσχετισμένο σχήμα, characters ώστε να κάνει διαθέσιμους τους σχετικούς χαρακτήρες της ταινίας. Αυτό είναι που θα μας δώσει τη δυνατότητα να καλέσουμε την movie.characters.

Η Μετατροπή για την Ανήκει Σε

Τώρα είμαστε έτοιμοι να χτίσουμε τη μετατροπή και το σχήμα για το Character. Ένας χαρακτήρας ανήκει σε μια ταινία, έτσι θα ορίσουμε μια μετατροπή και ένα σχήμα που ορίζει αυτή τη σχέση.

Αρχικά, δημιουργήστε τη μετατροπή:

mix ecto.gen.migration create_characters

Για να ορίσουμε ότι ένας χαρακτήρας ανήκει σε μια ταινία, θα χρειαστούμε ο πίνακας characters να έχει μια στήλη movie_id. Θέλουμε αυτή η στήλη να λειτουργεί σαν ξένο κλειδί. Μπορούμε να το καταφέρουμε αυτό με την παρακάτω γραμμή στη συνάρτησή μας create_table/1:

add :movie_id, references(:movies)

Έτσι η μετατροπή μας πρέπει να δείχνει ως εξής:

# priv/migrations/*_create_characters.exs
defmodule Friends.Repo.Migrations.CreateCharacters do
  use Ecto.Migration

  def change do
    create table(:characters) do
      add :name, :string
      add :movie_id, references(:movies)
    end
  end
end

Το Σχήμα της Ανήκει Σε

Παρόμοια το σχήμα μας θα πρέπει να ορίζει τη σχέση “ανήκει σε” ανάμεσα σε ένα χαρακτήρα και την ταινία του.

# lib/friends/character.ex

defmodule Friends.Character do
  use Ecto.Schema

  schema "characters" do
    field :name, :string
    belongs_to :movie, Friends.Movie
  end
end

Ας ρίξουμε μια κοντινότερη ματιά στο τι κάνει η μακροεντολή belongs_to/3 για εμάς. Τοποθετεί το ξένο κλειδί movie_id στο σχήμα μας και μας δίνει τη δυνατότητα να έχουμε πρόσβαση στο συσχετιζόμενο σχήμα movies μέσα από το characters. Χρησιμοποιεί το ξένο κλειδί για να κάνει διαθέσιμη τη συσχετιζόμενη ταινία ενός χαρακτήρα όταν πραγματοποιήσουμε ένα ερώτημα για αυτούς. Αυτό θα μας επιτρέψει να καλέσουμε την character.movie.

Τώρα είμαστε έτοιμοι να τρέξουμε τις μετατροπές μας:

mix ecto.migrate

Ανήκει Σε / Έχει ´Ενα

Ας πούμε ότι μια ταινία έχει έναν διανομέα, για παράδειγμα το Netflix είναι ο διανομέας του πρωτότυπου φίλμ τους “Bright”.

Θα ορίσουμε τη μετατροπή και το σχήμα Distributor με τη σχέση “ανήκει σε”. Αρχικά, ας δημιουργήσουμε τη μετατροπή:

mix ecto.gen.migration create_distributors

Θα πρέπει να προσθέσουμε ένα ξένο κλειδί movie_id στη μετατροπή πίνακα distributors που μόλις δημιουργήσαμε, όπως επίσης και ένα μοναδικό δείκτη για να βεβαιωθούμε ότι η ταινία έχει μόνο ένα διανομέα:

# priv/repo/migrations/*_create_distributors.exs

defmodule Friends.Repo.Migrations.CreateDistributors do
  use Ecto.Migration

  def change do
    create table(:distributors) do
      add :name, :string
      add :movie_id, references(:movies)
    end
    
    create unique_index(:distributors, [:movie_id])
  end
end

Και το Distributor σχήμα θα πρέπει να χρησιμοποιήσει τη μακροεντολή belongs_to/3 για να μας επιτρέψει να καλέσουμε την distributor.movie και να ψάξουμε τη συσχετιζόμενη ταινία ενός διανομέα χρησιμοποιώντας αυτό το ξένο κλειδί.

# lib/friends/distributor.ex

defmodule Friends.Distributor do
  use Ecto.Schema

  schema "distributors" do
    field :name, :string
    belongs_to :movie, Friends.Movie
  end
end

Στη συνέχεια, ας προσθέσουμε τη σχέση “has_one” στο σχήμα της Movie:

# lib/friends/movie.ex

defmodule Friends.Movie do
  use Ecto.Schema

  schema "movies" do
    field :title, :string
    field :tagline, :string
    has_many :characters, Friends.Character
    has_one :distributor, Friends.Distributor # I'm new!
  end
end

Η μακροεντολή has_one/3 λειτουργεί ακριβώς όπως η has_many/3. Χρησιμοποιεί το ξένο κλειδί του συσχετιζόμενου σχήματος για να ψάξει και να αποκαλύψει τον διανομέα της ταινίας. Αυτό θα μας επιτρέψει να καλέσουμε την movie.distributor.

Είμαστε έτοιμοι να τρέξουμε τη μετατροπή μας:

mix ecto.migrate

Πολλά Προς Πολλά

Ας πούμε ότι μια ταινία έχει πολλούς ηθοποιούς και ένας ηθοποιός ανήκει σε περισσότερες από μια ταινίες. Θα χτίσουμε ένα πίνακα σύνδεσης που αναφέρεται στις ταινίες και τους ηθοποιούς για να υλοποιήσει αυτή τη σχέση.

Αρχικά, θα δημιουργήσουμε τη μετατροπή για τους Actors:

mix ecto.gen.migration create_actors

Ορίστε τη μετατροπή:

# priv/migrations/*_create_actors.ex

defmodule Friends.Repo.Migrations.CreateActors do
  use Ecto.Migration

  def change do
    create table(:actors) do
      add :name, :string
    end
  end
end

Ας δημιουργήσουμε τη μετατροπή μας για το πίνακα σύνδεσης:

mix ecto.gen.migration create_movies_actors

Θα ορίσουμε τη μετατροπή μας ώστε να έχει δύο ξένα κλειδιά. Θα προσθέσουμε επίσης ένα μοναδικό δείκτη για να βεβαιωθούμε ότι το ταίριασμα ταινίας και ηθοποιού είναι μοναδικό:

# priv/migrations/*_create_movies_actors.ex

defmodule Friends.Repo.Migrations.CreateMoviesActors do
  use Ecto.Migration

  def change do
    create table(:movies_actors) do
      add :movie_id, references(:movies)
      add :actor_id, references(:actors)
    end

    create unique_index(:movies_actors, [:movie_id, :actor_id])
  end
end

Στη συνέχεια, ας προσθέσουμε τη μακροεντολή many_to_many στο Movie σχήμα μας:

# lib/friends/movie.ex

defmodule Friends.Movie do
  use Ecto.Schema

  schema "movies" do
    field :title, :string
    field :tagline, :string
    has_many :characters, Friends.Character
    has_one :distributor, Friends.Distributor
    many_to_many :actors, Friends.Actor, join_through: "movies_actors" # I'm new!
  end
end

Τελικά, θα ορίσουμε το Actor σχήμα μας με την ίδια μακροεντολή many_to_many.

# lib/friends/actor.ex

defmodule Friends.Actor do
  use Ecto.Schema

  schema "actors" do
    field :name, :string
    many_to_many :movies, Friends.Movie, join_through: "movies_actors"
  end
end

Είμαστε έτοιμοι να τρέξουμε τις μετατροπές μας:

mix ecto.migrate

Αποθήκευση Συσχετιζομένων Δεδομένων

Ο τρόπος με τον οποίο αποθηκεύουμε εγγραφές μαζί με τα σχετικά δεδομένα τους εξαρτάται από τη φύση της σχέσης μεταξύ των εγγραφών. Ας ξεκινήσουμε με τη σχέση “Ανήκει σε/Έχει πολλά”.

Ανήκει Σε

Αποθήκευση με την Ecto.build_assoc/3

Με τη σχέση “ανήκει σε”, μπορούμε να χρησιμοποιήσουμε τη συνάρτηση του Ecto build_assoc/3.

Η build_assoc/3 δέχεται τρία ορίσματα:

Ας αποθηκεύσουμε μια ταινία και το συσχετιζόμενο χαρακτήρα. Αρχικά, ας δημιουργήσουμε μια εγγραφή ταινίας:

iex> alias Friends.{Movie, Character, Repo}
iex> movie = %Movie{title: "Ready Player One", tagline: "Something about video games"}

%Friends.Movie{
  __meta__: %Ecto.Schema.Metadata<:built, "movies">,
  actors: %Ecto.Association.NotLoaded<association :actors is not loaded>,
  characters: %Ecto.Association.NotLoaded<association :characters is not loaded>,
  distributor: %Ecto.Association.NotLoaded<association :distributor is not loaded>,
  id: nil,
  tagline: "Something about video games",
  title: "Ready Player One"
}

iex> movie = Repo.insert!(movie)

Τώρα θα χτίσουμε ένα συσχετιζόμενο χαρακτήρα και θα τον εισάγουμε στη βάση δεδομένων:

iex> character = Ecto.build_assoc(movie, :characters, %{name: "Wade Watts"})
%Friends.Character{
  __meta__: %Ecto.Schema.Metadata<:built, "characters">,
  id: nil,
  movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
  movie_id: 1,
  name: "Wade Watts"
}
iex> Repo.insert!(character)
%Friends.Character{
  __meta__: %Ecto.Schema.Metadata<:loaded, "characters">,
  id: 1,
  movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
  movie_id: 1,
  name: "Wade Watts"
}

Παρατηρήστε ότι από τη στιγμή που η μακροεντολή has_many/3 του σχήματος Movie ορίζει ότι η ταινία έχει πολλούς :characters, το όνομα του συσχετισμού που περνάμε σαν δεύτερο όρισμα στην build_assoc/3 είναι ακριβώς αυτό: :characters. Μπορούμε να δούμε ότι δημιουργήσαμε ένα χαρακτήρα του οποίου η ιδιότητά movie_id ορίστηκε σωστά στο ID της συσχετιζομένης ταινίας.

Για να μπορέσουμε να αποθηκεύσουμε το διανομέα μιας ταινίας με τη χρήση της build_assoc/3, θα έχουμε την ίδια προσέγγιση περνώντας το όνομα της σχέσης της ταινίας με το διανομέα σαν δεύτερο όρισμα της build_assoc/3:

iex> distributor = Ecto.build_assoc(movie, :distributor, %{name: "Netflix"})
%Friends.Distributor{
  __meta__: %Ecto.Schema.Metadata<:built, "distributors">,
  id: nil,
  movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
  movie_id: 1,
  name: "Netflix"
}
iex> Repo.insert!(distributor)
%Friends.Distributor{
  __meta__: %Ecto.Schema.Metadata<:loaded, "distributors">,
  id: 1,
  movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
  movie_id: 1,
  name: "Netflix"
}

Πολλά προς Πολλά

Αποθήκευση με την Ecto.Changeset.put_assoc/4

Η προσέγγιση με την build_assoc/3 δεν θα δουλέψει για τη σχέση μας πολλά-προς-πολλά. Αυτό συμβαίνει γιατί κανένας εκ των πινάκων ταινίας και ηθοποιού δεν περιέχει ένα ξένο κλειδί. Αντίθετα, θα χρειαστεί να χρησιμοποιήσουμε τα Σετ Αλλαγών του Ecto και τη συνάρτηση put_assoc/4.

Υποθέτοντας ότι δημιουργήσαμε ήδη μια εγγραφή ταινίας πιο πάνω, ας δημιουργήσουμε μια εγγραφή ηθοποιού:

iex> alias Friends.Actor
iex> actor = %Actor{name: "Tyler Sheridan"}
%Friends.Actor{
  __meta__: %Ecto.Schema.Metadata<:built, "actors">,
  id: nil,
  movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
  name: "Tyler Sheridan"
}
iex> actor = Repo.insert!(actor)
%Friends.Actor{
  __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
  id: 1,
  movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
  name: "Tyler Sheridan"
}

Τώρα είμαστε έτοιμοι να συσχετίσουμε την ταινία και τον ηθοποιό μας μέσα από τον πίνακα σύνδεσης.

Αρχικά, σημειώστε ότι για να εργαστούμε με τα σετ αλλαγών, πρέπει να βεβαιωθούμε ότι η δομή movie μας έχει προφορτωθεί με τα συσχετιζόμενα δεδομένα. Θα μιλήσουμε περισσότερο για την προφόρτωση δεδομένων σε λίγο. Για τώρα, είναι αρκετό να καταλάβετε ότι μπορούμε να προφορτώσουμε τους συσχετισμούς μας ως εξής:

iex> movie = Repo.preload(movie, [:distributor, :characters, :actors])
%Friends.Movie{
 __meta__: #Ecto.Schema.Metadata<:loaded, "movies">,
  actors: [],
  characters: [
    %Friends.Character{
      __meta__: #Ecto.Schema.Metadata<:loaded, "characters">,
      id: 1,
      movie: #Ecto.Association.NotLoaded<association :movie is not loaded>,
      movie_id: 1,
      name: "Wade Watts"
    }
  ],
  distributor: %Friends.Distributor{
    __meta__: #Ecto.Schema.Metadata<:loaded, "distributors">,
    id: 1,
    movie: #Ecto.Association.NotLoaded<association :movie is not loaded>,
    movie_id: 1,
    name: "Netflix"
  },
  id: 1,
  tagline: "Something about video game",
  title: "Ready Player One"
}

Στη συνέχεια, θα δημιουργήσουμε ένα σετ αλλαγών για την εγγραφή της ταινίας μας:

iex> movie_changeset = Ecto.Changeset.change(movie)
%Ecto.Changeset<action: nil, changes: %{}, errors: [], data: %Friends.Movie<>,
 valid?: true>

Τώρα θα περάσουμε το σετ αλλαγών μας σαν πρώτο όρισμα στην Ecto.Changeset.put_assoc/4:

iex> movie_actors_changeset = movie_changeset |> Ecto.Changeset.put_assoc(:actors, [actor])
%Ecto.Changeset<
  action: nil,
  changes: %{
    actors: [
      %Ecto.Changeset<action: :update, changes: %{}, errors: [],
       data: %Friends.Actor<>, valid?: true>
    ]
  },
  errors: [],
  data: %Friends.Movie<>,
  valid?: true
>

Αυτό μας δίνει ένα νέο σετ αλλαγών που αναπαριστά την ακόλουθη αλλαγή: προσθήκη των ηθοποιών στη λίστα ηθοποιών της δοθείσας εγγραφής ταινίας.

Τελικά, θα αναβαθμίσουμε τη δοθείσα ταινία και τις εγγραφές ηθοποιών της με το τελευταίο μας σετ αλλαγών:

iex> Repo.update!(movie_actors_changeset)
%Friends.Movie{
  __meta__: #Ecto.Schema.Metadata<:loaded, "movies">,
  actors: [
    %Friends.Actor{
      __meta__: #Ecto.Schema.Metadata<:loaded, "actors">,
      id: 1,
      movies: #Ecto.Association.NotLoaded<association :movies is not loaded>,
      name: "Tyler Sheridan"
    }
  ],
  characters: [
    %Friends.Character{
      __meta__: #Ecto.Schema.Metadata<:loaded, "characters">,
      id: 1,
      movie: #Ecto.Association.NotLoaded<association :movie is not loaded>,
      movie_id: 1,
      name: "Wade Watts"
    }
  ],
  distributor: %Friends.Distributor{
    __meta__: #Ecto.Schema.Metadata<:loaded, "distributors">,
    id: 1,
    movie: #Ecto.Association.NotLoaded<association :movie is not loaded>,
    movie_id: 1,
    name: "Netflix"
  },
  id: 1,
  tagline: "Something about video game",
  title: "Ready Player One"
}

Μπορούμε να δούμε ότι αυτό μας δίνει μια εγγραφή ταινίας με το νέο ηθοποιό σωστά συσχετισμένο και ήδη προφορτωμένο για εμάς στην movie.actors.

Μπορούμε να χρησιμοποιήσουμε αυτή την προσέγγιση για να δημιουργήσουμε ένα νέο ηθοποιό που συσχετίζεται με τη δοθείσα ταινία. Αντί να περάσουμε μια αποθηκευμένη δομή ηθοποιού στην put_assoc/4, απλά περνάμε ένα χάρτη με τα στοιχεία νέου ηθοποιού που θέλουμε να δημιουργήσουμε:

iex> changeset = movie_changeset |> Ecto.Changeset.put_assoc(:actors, [%{name: "Gary"}])
%Ecto.Changeset<
  action: nil,
  changes: %{
    actors: [
      %Ecto.Changeset<
        action: :insert,
        changes: %{name: "Gary"},
        errors: [],
        data: %Friends.Actor<>,
        valid?: true
      >
    ]
  },
  errors: [],
  data: %Friends.Movie<>,
  valid?: true
>
iex>  Repo.update!(changeset)
%Friends.Movie{
  __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
  actors: [
    %Friends.Actor{
      __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
      id: 2,
      movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
      name: "Gary"
    }
  ],
  characters: [],
  distributor: nil,
  id: 1,
  tagline: "Something about video games",
  title: "Ready Player One"
}

Μπορούμε να δούμε ότι ο νέος ηθοποιόος δημιουργήθηκε με το ID “2” και τα χαρακτηριστικά που του ορίσαμε.

Στην επόμενη ενότητα, θα μάθουμε πως να δημιουργούμε ερωτήματα για τις συσχετιζόμενες εγγραφές μας.

Έπιασες λάθος ή θέλεις να συνεισφέρεις στο μάθημα; Επεξεργαστείτε αυτό το μάθημα στο GitHub!