Lucky Logo

#Introduction

A Model is an object used to map a corresponding database table to a class. These objects model real-world objects to give you a better understanding on how they should interact within your application.

Models in Lucky allow you to define methods associated with each column in the table. These methods return the value set in that column.

Avram models also generate other classes you can use to save new records, and query existing ones.

#Generate a model

Lucky gives you a task for generating a model along with several other files that you will need for interacting with your database.

Use the lucky gen.model {ModelName} task to generate your model. If you’re generating a User model, you would run lucky gen.model User. Running this will generate a few files for you.

#Setting up a model

Once you run the model generator, you’ll have a file that looks like this

# src/models/user.cr
class User < BaseModel
  table :users do
    # You will define columns here. For example:
    # column name : String
  end
end

Your model will inherit from BaseModel which is just an abstract class. You can use this to define methods all of your models should have access to. Next you’ll see the table block that defines which table this model is connected to.

#Defining a column

#Default columns

By default, Lucky will add a few columns to your model for you.

#Changing the default columns

To change your defaults, define a macro called default_columns in your BaseModel.

abstract class BaseModel < Avram::Model
  macro default_columns
    # Defines a custom primary key
    primary_key custom_key : UUID

    # adds the `created_at` and `updated_at`
    timestamps
  end
end

If you don’t need the default columns for a specific model, call the skip_default_columns macro at the top of the model class.

class CustomModel < Avram::Model
  skip_default_columns
end

Now your model won’t define id, created_at, or updated_at fields. It will be up to you to specify your primary key field.

#Setting the primary key

The primary key is Int64 by default. If that’s what you need, then everything is already set for you. If you need Int32, Int16, UUID, or your own custom set one, you’ll need to update the primary_key.

Setting your primary key with the primary_key method works the same as you did in your migration.

# src/models/user.cr
class User < BaseModel
  # Sets the type for `id` to `UUID`
  table :users do
    primary_key id : UUID
  end
end

#Adding a column

Inside of the table block, you’ll add the columns your model will define using the column method.

table :users do
  column email : String
  column active : Bool
  # This column can be `nil` because the type ends in `?`
  column ip_address : String?
  column last_active_at : Time
end

#Column types

Avram supports several types that map to Postgres column types.

Any of your columns can also define “nillable” types by adding Crystal Nil Union ?. This is if your column allows for a NULL value. (e.g. column age : Int32? allows an int or NULL value).

#Additional postgres types

Postgres supports a lot more types than what Avram does out of the box. If you need access to a type that crystal-pg supports that isn’t listed above, you can add in support for your app.

Let’s take postgres’s double precision type for example. This currently maps to Float64, but Lucky maps numeric to Float64. To use the double precision type, create an alias.

alias Double = Float64

# then in your model

table :products do
  column price : Double
end

Avram is constantly being updated, and some types may not “patch” as easily. If you tried this method, and it doesn’t work for you, be sure to open an issue so we can get support for that as soon as possible.

#Model associations

In a RDBMS you may have tables that are related to each other. With Avram, we can associate two models to make some common queries a lot more simple.

All associations will be defined in the table block. You can use has_one, has_many, and belongs_to.

class User < BaseModel
  table :user do
    has_one supervisor : Supervisor
    has_many tasks : Task
    belongs_to company : Company
  end
end

#Belongs to

A belongs_to will assume you have a foreign key column related to the other model defined as {model_name}_id.

table :users do
  column name : String

  # gives you the `company_id`, and `company` methods
  belongs_to company : Company
end

When you create the migration, be sure you’ve set add_belongs_to.

You can preload these associations in your queries, and return the associated model.

# Will return the company associated with the User
UserQuery.new.preload_company.find(1).company

#Optional association

Sometimes associations are not required. To do that add a ? to the end of the type.

belongs_to company : Company?

Make sure to make the column nilable in your migration as well: add_belongs_to company : Company?

#Has one (one to one)

table :users do
  has_one supervisor : Supervisor
end

This would match up with the Supervisor having belongs_to.

table :supervisors do
  belongs_to user : User
end

#Has many (one to many)

table :users do
  has_many tasks : Task
end

The name of the association should be the plural version of the model’s name, and the type is the model. (e.g. Task model, tasks association)

#Has many through (many to many)

Let’s say we want to have many tags that can belong to any number of posts.

Here are the models:

# This is what will join the posts and tags together
class Tagging < BaseModel
  table :taggings do
    belongs_to tag : Tag
    belongs_to post : Post
  end
end

class Tag < BaseModel
  table :tags do
    column name : String
    has_many taggings : Tagging
    has_many posts : Post, through: :taggings
  end
end

class Post < BaseModel
  table :posts do
    column title : String
    has_many taggings : Tagging
    has_many tags : Tag, through: :taggings
  end
end

The associations must be declared on both ends (the Post and the Tag in this example), otherwise you will get a compile time error

#Polymorphic associations

Polymorphism describes the concept that objects of different types can be accessed through the same interface.

This allows us to have a single method to define an association, but that method can return many different types. A bit confusing, but best explained with some code!

class Photo < BaseModel
  table do
    has_many comments : Comment
  end
end

class Video < BaseModel
  table do
    has_many comments : Comment
  end
end

class Comment < BaseModel
  table do
    # Note that both these `belongs_to` *must* be nilable
    belongs_to photo : Photo?
    belongs_to video : Video?

    # Now `commentable` could be a `photo` or `video`
    polymorphic commentable, associations: [:photo, :video]
  end
end

And to use it

photo = SavePhoto.create!
comment = SaveComment.create!(phot_id: photo.id)

comment.commentable == photo

The Comment model now has a commentable method which could return a photo object or a video object depending on which was associated.

For each polymorphic association, you’ll need to add a belongs_to. This helps to keep our polymorphic associations type-safe! See migrations for add_belongs_to.

You’ll also note that the belongs_to has nillable models. This is required for the polymorphic association. Even though these are set as nilable, the association still requires at least 1 of the associations to exist. This means that commentable is never actually nil.

If you need this association to be fully optional where commentable could be nil, you’ll add the optional option.

# commentable can now be nil
polymorphic commentable, optional: true, associations: [:photo, :video]

#Preloading polymorphic associations

Since the polymorphic associations are just regular belongs_to associations with some sweet helper methods, all of the preloading still exists.

comment = CommentQuery.new.preload_commentable
comment.commentable #=> Safely access this association

To skip preloading the polymorphic association, just add a bang !.

comment = CommentQuery.first
comment.commentable! #=> no preloading required here