Ruby rails: models

From wikinotes

Rails models contain buiness logic, and abstract your database.

In code, they are represented as ActiveRecord::Base subclasses.

If you want to change an existing model, see ruby rails: migrations.

Documentation

Docs
activerecord docs https://guides.rubyonrails.org/active_record_basics.html
activemodel docs https://guides.rubyonrails.org/active_model_basics.html
association docs https://guides.rubyonrails.org/association_basics.html
validation docs https://guides.rubyonrails.org/active_record_validations.html
query docs https://guides.rubyonrails.org/active_record_querying.html
callback docs https://guides.rubyonrails.org/active_record_callbacks.html
API Docs
ActiveRecord
(See "Included Modules" for options)
https://api.rubyonrails.org/classes/ActiveRecord/Base.html
column types https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_column

Locations

{project}/config/database.yml database connection details
{project}/app/models/*.rb models stored here

Example

Most model functionality is provided by ApplicationRecord (getters, setters, queries, etc). Normally, methods defined on models are for manipulating data of it's type.

  • id column is created automatically
  • A large portion of the model metaprogramming the model itself
    • how/when to validate
    • translating between database/ruby types
    • associations between other ActiveRecord classes (has_many, etc)
class User < ApplicationRecord
  # ===============
  # metaprogramming
  # ===============

  # convert 'legal_voting_age' to boolean when accessed, 
  # from boolean on write.
  attribute :legal_voting_age, :boolean, 
    default: false
  
  # on save (before INSERT/UPDATE), field will be validated
  # ``self.errors[]`` attribute will hold validation errors
  validates :firstname, 
    format: {with: /[a-zA-Z]+/},
    length: 3

  # 'keyboards' attribute lists all 'keyboards' 
  # table-columns associated with this user.
  #
  # you can also create keyboard objects from it, with 'user'
  # field already assigned: ``user.keyboards.create(...)``
  has_many :keyboards

  # =======
  # methods
  # =======
  def calculate_age_in_dog_years
    # ...
  end
end

You may also find ruby rails: migrations useful.

Commandline

NOTE:

You can automatically create a model and CRUD views/controllers using scaffolds

# create model 'User', 
# db-migration to create table 'users'
rails generate model User \
  `# 'id' as bigint primary key is automatic` \
  first_name:string \
  last_name:string \
  bio:text

# run migration
rails db:migrate
rails db:migrate RAILS_ENV=test

# revert last migration
rails db:rollback

Configuration

  • config/database.yml configures database connections
  • db/seeds.rb defines data to be created with database

See ruby rails: configuration section on databases for details.

Column Datatypes

List of all common datatypes.
Specific database types may implement additional types.

# misc
:primary_key 
:boolean
:binary

# text
:string 
:text 

# numbers
:integer 
:bigint 
:float 
:decimal 
:numeric

# dates
:datetime 
:time 
:date

See https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_column

Instantiate/Create

YourObject.new(name: 'will')     # instantiate (not saved to db)
YourObject.create(name: 'will')  # create (saved to db)
YourObject.build(name: 'will')   # build (not saved to db)

instance.valid?  # run validations, return true/false if valid
instance.errors  # array of validation errors (must validate first)

You can also create through associations:

instance.associated_model.create(name: 'will')
instance.associated_model.build(name: 'will')

instance.create(associated_model: { ... })
instance.build(assiciated_model: { ... })

instance.create_{association_name}( ... )
instance.build_{association_name}( ... )

Tables/Classes

  • activerecord classes are PascalCase their table is snake_case
  • activerecord classes are singular, their table is plural
  • subclasses refer to their parent's table
class User < ActiveRecord::Base             # table: 'users'
class UserPermission  < ActiveRecord::Base  # table: 'user_permissions'

class SpecialUser < User                    # table: 'users'

# override table name
class UserGroupPermissions < ActiveRecord::Base
  self.table_name = 'group_permissions'
end

Get Table Info

User.columns       # columns, type info, etc.
User.column_names  # column names

Columns/Attributes

Rails automatically creats a getter/setter for every column in the table.


Example Class


The table:

CREATE TABLE users (
  id          INT NOT NULL PRIMARY_KEY AUTO_INCREMENT,
  first_name  VARCHAR(50),
  last_name   VARCHAR(50)
);

Referred to by:

class User < ActiveRecord::Base
end

Is magically made into something like

class User < Active
  def first_name=(val)
    self[:first_name] = val
  end

  def first_name
    self[:first_name]
  end

  def last_name=(val)
    self[:last_name] = val
  end

  def last_name
    self[:last_name]
  end

  def id=(val)
    self[:id] = val
  end

  def id
    self[:id]
  end

Custom Getters/Setters


  • rails automatically assigns a getter/setter for each field
  • you may customize them, using this syntax
  • this is implemented by overriding write_attribute() and read_attribute methods.
class User < ActiveRecord::Base

  # rails setter
  def name=(val)
    self[:name] = val
  end

  # rails getter
  def name
    self[:name]
  end

end

Attribute Types (and non-column attributes)


See https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute

  • Translate between a database-column type, and one that is meaningful to your application
  • Create attributes that are not database columns
CREATE TABLE users (
    id         INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
    first_name TINYTEXT,
    age        FLOAT,  # <-- float, not int
);
class User < ActiveRecord::Base
  attribute :age, :integer  # <-- convert to int
end

This article may also be useful (describing various uses for virtual attributes) https://jtway.co/rails-virtual-attributes-use-cases-cf33bd45e2a4

Reflection

Short Info

User.reflect_on_all_associations.map(&:name)  # list associated tables
User.column_names                             # list column names

Detailed Info

User.reflect_on_all_associations  # list all associations
User.columns                      # list all columns

Associations

Associations can map associations between database tables.

one to one

class Employee < ActiveRecord::Base
  has_one :office
end
class Office < ActiveRecord::Base
  belongs_to :employee    # foreign key - employee_id
end

one to many


class Manager < ActiveRecord::Base
  has_many :employees
end

class Employee < ActiveRecord::Base
  belongs_to :manager     # foreign key - manager_id
end

variations

has_many :authored_posts, foreign_key: "author_id", class_name: "Post"
has_many :edited_posts, foreign_key: "editor_id", class_name: "Post"


many to many


class Assignment < ActiveRecord::Base
  belongs_to :programmer  # foreign key - programmer_id
  belongs_to :project     # foreign key - project_id
end
class Programmer < ActiveRecord::Base
  has_many :assignments
  has_many :projects, through: :assignments
end
class Project < ActiveRecord::Base
  has_many :assignments
  has_many :programmers, through: :assignments
end

variations

has_many :authored_posts, foreign_key: "author_id", class_name: "Post"
has_many :edited_posts, foreign_key: "editor_id", class_name: "Post"

Queries

See ruby rails: queries

Validation

validates

class TitleValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors.add attribute, (options[:message] || "is not an email") unless
      value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
  end
end


class UserModel < ActiveRecord::Base
  # NOTE: @username is a column

  validates :username, title: true   # 'title:' refers to above 'TitleValidator'
end

Save/Load

yourobject.save    # commit changes to database
yourobject.reload  # reload from database

Errors

See https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-generate_message

user = User.new
user.errors.add(:my_attribute, :not_implemented)
user.errors.full_messages

Custom errors

errors = ActiveModel::Errors.new(SomeClass)
errors.add(:my_attr, "must be assigned")

Hooks / Callbacks

There are various hooks/callbacks that you can use to alter model behaviour, perform validation, etc.

before_save :method_name