Using Models with Custom Primary Keys - TanYewWei/ecto GitHub Wiki

Developer-Assigned Model Primary Keys

In the "Models" section of the README, we saw the following example:

defmodule Weather do
  use Ecto.Model

  # weather is the DB table
  schema "weather" do
    field :city,    :string
    field :temp_lo, :integer
    field :temp_hi, :integer
    field :prcp,    :float, default: 0.0
  end
end

This is great for most purposes, but assumes that primary keys are of :integer type, and will be generated by the database when the Model is inserted into the database. What if you wanted to use Developer-Assigned random :string primary keys instead?

This can easily be done by using the @schema_defaults module declaration:

defmodule Weather do
  use Ecto.Model
  @schema_defaults [primary_key: {:id, :string, []}]

  # weather is the DB table
  schema "weather" do
    field :city,    :string
    field :temp_lo, :integer
    field :temp_hi, :integer
    field :prcp,    :float, default: 0.0
  end
end

Now any Weather models will require a :string primary key, which has no default value. It is then up to you the developer to assign an appropriate primary key as needed. Assigning a primary key MUST be done using the Ecto.Model.put_primary_key/2 function.

Using the put_primary_key/2 function is important to ensure that any associations of a model are properly set when the primary key changes.

Example:

weather = %Weather{temp_lo: 30}
weather = Ecto.Model.put_primary_key(weather, "some-new-id")

Declaring Defaults Across All Your Models

Having to type @schema_defaults in each of your models gets tiresome after awhile :frowning: So it's common to have another module that declares your @schema_defaults, and then use that module instead of Ecto.Model

defmodule MyModel do
  defmacro __using__(opts) do
    # declare that all models use :string primary keys, and 
    # all associations should assume :string foreign keys as well
    @schema_defaults [primary_key: {:id, :string, []},
                      foreign_key_type: :string]

    # Make sure to use the appropriate Ecto modules
    use Ecto.Model.Schema
    use Ecto.Model.Validations
  end
end

Then we can declare the following modules:

defmodule Post do
  use MyModel  # instead of Ecto.Model

  schema "posts" do
    field    :title,    :string
    field    :body,     :string
    has_many :comments, Comment
  end
end
defmodule Comment do
  use MyModel

  schema "comments" do
    field      :body, :string
    belongs_to :post, Post
  end
end

And then use create some Comments and Posts like so (assuming Repo is a valid Ecto.Repo):

# Create Post
post = %Post{title: "Post 1", body: "Hello!"}
  |> Ecto.Model.put_primary_key("post_1")
  |> Repo.insert()

# Assign comments
c0 = %Comment{body: "I made a comment", post_id: post.id}
  |> Ecto.Model.put_primary_key("comment_0")
  |> Repo.insert()
c1 = %Comment{body: "I made a comment", post_id: post.id}
  |> Ecto.Model.put_primary_key("comment_1")
  |> Repo.insert()

# Fetch post and comments
# don't forget to `import Ecto.Query`
get_post = from(x in Post,
  where: x.id == ^post.id,
  preload: :comments)
  |> Repo.all()
comments = get_post.comments.all
c0 in comments  #=> true
c1 in comments  #=> true

Notes

  • See the Ecto.Model.Schema module docs for more information about @schema_defaults (TODO: update link to v0.2.0 docs instead of source code once released)