activerecord single table inheritance on ruby 3 - ruby

I have the following classes mapped with STI:
class Employee < ActiveRecord::Base
end
class StudentEmployee < Employee
# I'd like to keep university only to StudentEmployee...
end
#Just to make this example easier to understand, not using migrations
ActiveRecord::Schema.define do
create_table :employees do |table|
table.column :name, :string
table.column :salary, :integer
table.column :university, :string # Only Students
end
end
emp = Employee.create(:name=>"Joe",:salary=>20000,:university=>"UCLA")
I'd like to prevent the setting of the university field for Employees, but allow it for StudentEmployees. I tried to use attr_protected, but it will only prevent mass setting:
class Employee < ActiveRecord::Base
attr_protected :university
end
class StudentEmployee < Employee
attr_accessible :university
end
#This time, UCLA will not be assigned here
emp = Employee.create(:name=>"Joe",:salary=>20000,:university=>"UCLA")
emp.university = "UCLA" # but this will assign university to any student...
emp.save
puts "only Students should have univesities, but this guy has one..."+emp.university.to_s
The problem here is that it will insert in the database a university for simple employees.
Another problem is that I think it would be better to say in the StudentEmployee class that university is an attribute, and not to say in the Employee that university "is not" a a visible attribute... it just goes in the inverse direction of natural abstraction.
Thanks.

I would try something like this:
class Employee < ActiveRecord::Base
validate :no_university, unless: lambda { |e| e.type === "StudentEmployee" }
def no_university
errors.add :university, "must be empty" unless university.nil?
end
end
It isn't the prettiest, but it should to work.

Related

I do not understand a many_to_one <=> one_to_one model association

how to say? I do not understand what the sequel documentation tries to tell me about associations in case of a two models linked over a foreign key in one model A being a primary key in the other in a may_to_one case.
I always thought: If it is many_to _one in one direction it has to be one_to_many in the other... but the sequel provides a confusing chapter meant to clarify the topic with in addition an example I cannot follow.
It says in
"Differences Between many_to_one and one_to_one"
If you want to setup a 1-1 relationship between two models, where the foreign > key in one table references the associated table directly, you have to use
many_to_one in one model, and one_to_one in the other model. How do
you know which to use in which model? The simplest way to remember is
that the model whose table has the foreign key uses many_to_one, and
the other model uses one_to_one"
And continues to provide this strange example:
# Database schema:
# artists albums
# :id <----\ :id
# :name \----- :artist_id
# :name
class Artist
one_to_one :album
end
class Album
many_to_one :artist
end
In albums I may find several rows pointing to same artist... why shouldn't the artist point back to all his/her albums?
The sequel docu is crazy hard to read in many cases but this chapter reads easy but makes no sense for me:(
Same issue for me.
require "logger"
require "sequel"
db = Sequel.connect "postgres://localhost/postgres", :logger => Logger.new(STDOUT)
db.drop_table :artists, :cascade => true if db.table_exists?(:artists)
db.create_table :artists do
primary_key :id
foreign_key :album_id, :albums
end
db.drop_table :albums, :cascade => true if db.table_exists?(:albums)
db.create_table :albums do
primary_key :id
foreign_key :artist_id, :artists
end
class Artist < Sequel::Model(db[:artists])
one_to_one :album
end
class Album < Sequel::Model(db[:albums])
one_to_one :artist
end
artist_1 = Artist.create
album_1 = Album.create
artist_1.update :album => album_1
album_1.reload
puts album_1.artist.nil?
artist_2 = Artist.create
album_2 = Album.create
album_2.update :artist => artist_2
artist_2.reload
puts artist_2.album.nil?
We can fix this example by replacing any of one_to_one into many_to_one.
class Album
many_to_one :artist
end
In this case artist.album_id won't be used.
class Artist
many_to_one :albums
end
In this case album.artist_id won't be used.
The problem is that method names one_to_one and many_to_one were selected by underlying sequel logic and they are not user friendly.
You can create user friendly aliases for these methods. I prefer just to use it with comments. For example:
db.create_table :artists do
primary_key :id
foreign_key :album_id, :albums
end
db.create_table :albums do
primary_key :id
end
class Artist < Sequel::Model(db[:artists])
many_to_one :album # I have album_id foreign key
end
class Album < Sequel::Model(db[:albums])
one_to_one :artist # I don't have artist_id foreign key
end

Data in a many-to-many relationship

I want to store extra data in a many-to-many relationship, similar to this Django example, in Sequel and Postgres.
In my example, each person has a place they work, and what hours they work there. Each person can work different hours at the same place. For example...
Joe works at The Bar Mon, Wed & Fri 9-5pm.
Joe also works at The Bar Saturday noon-midnight.
Joe works at Some Pub Sunday 12-5.
Joe started working at The Bar 2013-12-07
Joe started working at Some Pub 2014-12-23
I'm wondering how to set up the tables and models to make this as not awkward as possible. Currently I have a schema like this.
create_table(:people) do
primary_key :id
String :name
end
create_table(:works) do
primary_key :id
String :name
end
create_table(:employments) do
primary_key :id
foreign_key :work_id, :work, null: false,
foreign_key :person_id, :people, null: false,
Date :started
end
create_table(:hours) do
primary_key :id
foreign_key :employment_id, :employments, null: false
String :dow, null: false,
Time :start, null: false, only_time: true
Time :end, null: false, only_time: true
end
How should I create the model associations to join it all together? Ideally I would like to be able to say something like...
joes_hours_at_some_pub = joe.employment("Some Pub").hours
...without having to go through the intermediate join table.
Sequel's advanced association doc has an example of this. Define an Employment model and use :join_table to create a relationship through it.
class Person < Sequel::Model
one_to_many :employments,
many_to_many :works, join_table: :employments
# This allows joe.employment(work).hours
def employment(work)
employments_dataset.where(work_id: work.id)
end
end
class Work < Sequel::Model
one_to_many :employments,
many_to_many :people, join_table: :employments
end
class Employment < Sequel::Model
many_to_one :person
many_to_one :work
one_to_many :hours
end
class Hours < Sequel::Model
many_to_one :practice
end

has_and_belongs_to_many model creation needed?

I have orders and items table. I also have a third table called orders_items. Which I learned on creating from the following link (2nd graph) http://www.tutorialspoint.com/ruby-on-rails/rails-models.htm
models/order.rb
class Order < ActiveRecord::Base
has_and_belongs_to_many :items, through: :item_order
end
models/item.rb
class Item < ActiveRecord::Base
has_and_belongs_to_many :orders, through: :item_order
end
[orders_items] table has the following:
integer :order_id
integer :item_id
Do I have to create a models/order_item.rb file to add:
belongs_to :order
belongs_to :item
If so what is the correct naming format that it should be?
Would the name for the model file [order_item.rb] correct to distinguish which table it refers to?
models/order_item.rb ??
class OrdersItem ??? < ActiveRecord::Base
belongs_to :order
belongs_to :item
end
From the API
The join table should not have a primary key or a model associated
with it. You must manually generate the join table with a migration
such as this
class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration
def change
create_table :developers_projects, id: false do |t|
t.integer :developer_id
t.integer :project_id
end
end
end
Specifies a many-to-many relationship with another class. This
associates two classes via an intermediate join table. Unless the join
table is explicitly specified as an option, it is guessed using the
lexical order of the class names. So a join between Developer and
Project will give the default join table name of “developers_projects”
because “D” precedes “P” alphabetically
In your case the join table name should be items_orders.
Your model must be named OrderItem. And you don't need belongs_to in this class. The file name (order_item.rb) is correct.
I think you need this relationship to fulfill your needs, except if orders is an item too
class Order < ActiveRecord::Base
has_many :items
end
and
class Item < ActiveRecord::Base
belongs_to :order
end

ActiveRecord association seems to read differently depending on reading forward or backward

I've read a ton of ActiveRecord SO Questions and haven't come across this yet. I know that the following code snippet is a little long winded but I'm not using Rails so I wanted to be clear about how my database was created/structured.
A household has the head (or heads) of household (like mom and dad), and it has children. Mom, Dad and the kids are all members of the household. I tried to implement that as follows. This is the full code snippet so you can just copy and paste and run it if you have active_record and sqlite3.
I wrote the question in-line in the code, but here it is in case you don't want to skim the code: when I do household.heads I get the members which I assigned as heads of household. But when I run member.household.heads (on the same household!) I don't get the heads, I get the kids! My only thought is that I shouldn't be using two 'has_many's with the same foreign_key, but everything else I've tried doesn't work.
require 'active_record'
ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'w'))
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => 'test.db'
)
ActiveRecord::Schema.define do
unless ActiveRecord::Base.connection.tables.include? 'members'
create_table :members do |table|
table.integer :household_id
table.integer :head_id
table.integer :child_id
table.string :name
end
end
unless ActiveRecord::Base.connection.tables.include? 'households'
create_table :households do |table|
table.string :address
end
end
end
class Member < ActiveRecord::Base
belongs_to :household
end
class Household < ActiveRecord::Base
has_many :heads, class_name: "Member", foreign_key: :household_id
has_many :children, class_name: "Member", foreign_key: :household_id
end
#Create some members
m1 = Member.create(name: "Foo")
m2 = Member.create(name: "Bar") #Foo's wife
m3 = Member.create(name: "foo-foo") #Foo and Bar's little girl
#Create a household
h1 = Household.create(address: "123 Fake St.")
#Assign members to households
h1.heads = [m1, m2]
h1.children = [m3]
#first let's check, h1 is m1's household. The two are the same.
p h1.id == m1.household.id
#So why doesn't this work?
h1.heads.each{|head| p head.name} #returns Foo and Bar
m1.household.heads.each{|head| p head.name} #<= Doesn't return Foo and Bar ?!?
h1.children.each{|child| p child.name} #returns foo-foo
m1.household.children.each{|child| p child.name} #Also returns foo-foo as expected
As it turns out, ActiveRecord is not OK with two Models of the same class being referred to by the same foreign key (household_id). You're just overwriting the assignment of h1's members when you assign children (or something like that, this still isn't clear to me).
You either need different classes: Head and Child or you need to be ok with the child calling the household differently.
I ended up changing the foreign keys to: household_id and house_id since I need heads.household more than I need child.household
(I still think it is annoying to use children.house but oh well!)
To be clear:
class Household < ActiveRecord::Base
has_many :heads, class_name: "Member", foreign_key: :household_id
has_many :children, class_name: "Member", foreign_key: :house_id
end

ActiveRecord :through to set default values on through table

I would like to set a default value in a has_many through association.
Lets say I have three models:
People
Friends
Dogs
A person can request that a dog becomes their friend.
So a person would create an association where friends has an active column = false.
User
has_many :friends
has_many :dogs, :through => :friends
Now when I assign a dog to a user
User.find(1).dogs << dog
The friends table has null in the active column.
My friends model is defined as
Friend
def initialize(args = {})
super(args)
active = false
end
yet this does not work because the friend object is never created. Do I have to manually create one?
To set default values of a model; In the model I do this
before_save :default_values
private
def default_values
self.status = :active unless self.status
end
Not sure if this is the correct approach though.
With the following code you'll create a new friend with active = false
class User < ActiveRecord::Base
has_many :friends, :conditions => "active = false"
has_many :dogs, :through => :friends
end
#user = User.new
#user.friends.create #or #user.friends.build

Resources