Related
Based on other answers here I thought I could do this:
rails generate model Availability
class CreateAvailabilities < ActiveRecord::Migration[6.0]
def change
create_table :availabilities do |t|
t.references :user, null: false, foreign_key: true
t.datetime :starts_at, null: false
t.interval :duration, null: false
t.timestamps null: false
end
end
end
Availability.rb
class Availability < ApplicationRecord
attribute :duration, :duration
belongs_to :user
end
config/initializers/duration_type.rb
class DurationType < ActiveRecord::Type::String
def cast(value)
return value if value.blank? || value.is_a?(ActiveSupport::Duration)
ActiveSupport::Duration.parse(value)
end
def serialize(duration)
duration ? duration.iso8601 : nil
end
end
ActiveRecord::Type.register(:duration, DurationType)
rails console
Loading development environment (Rails 6.0.3.2)
> a = Availability.create(user: User.first, starts_at: Time.now, duration: 10.minutes)
User Load (0.7ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
(0.2ms) BEGIN
Availability Create (1.7ms) INSERT INTO "availabilities" ("user_id", "starts_at", "duration", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["user_id", 1], ["starts_at", "2020-08-09 18:50:37.732198"], ["duration", "PT10M"], ["created_at", "2020-08-09 18:50:37.747158"], ["updated_at", "2020-08-09 18:50:37.747158"]]
(1.0ms) COMMIT
> a.duration
=> 10 minutes
I thought I was home free but I get this error when I try this
> Availability.first.duration
Availability Load (0.6ms) SELECT "availabilities".* FROM "availabilities" ORDER BY "availabilities"."id" ASC LIMIT $1 [["LIMIT", 1]]
Traceback (most recent call last):
3: from (irb):5
2: from (irb):6:in `rescue in irb_binding'
1: from config/initializers/duration_type.rb:5:in `cast'
ActiveSupport::Duration::ISO8601Parser::ParsingError (Invalid ISO 8601 duration: "00:10:00")
It seems to be creating the Postgres database entry correctly but I'm confused why it's not able to read it back
Might be that you need to change how the duration is persisted in the database. It looks like it’s stored like 00:10:00. But the parser is expecting the string to be in ISO8601 format (PT10M).
class ChangeIntervalStyleInDatabase < ActiveRecord::Migration[6.0]
def self.up
execute "ALTER DATABASE \"#{connection.current_database}\" SET intervalstyle = 'iso_8601'"
end
def self.down
execute "ALTER DATABASE \"#{connection.current_database}\" SET intervalstyle = 'postgres'"
end
end
Rails 6.1+
Since my original answers, Rails 6.1 has been released adding support for PostgreSQL interval datatype. This eliminates the need for a custom type that casts and serializes values.
Rails 6.1 Changelog
Commit
I'm trying to migrate my project from Rails 5.0 to 5.2
Project extensively uses creation of related models through .build_%related_model%, it was working on rails 5.0 and now it's broke.
Does this functionality removed, or should i use another syntax?
class User < ActiveRecord::Base
belongs_to :profile, inverse_of: :user
end
class Profile < ActiveRecord::Base
has_one :user, inverse_of: :profile
end
new_user = User.new
new_user.build_profile
new_user.save
Previously this code created both User and his Profile. Now this will create only User, without Profile.
Any ideas how to fix this?
irb(main):001:0> new_user = User.new
=> #<User id: nil, profile_id: nil, created_at: nil, updated_at: nil>
irb(main):002:0> new_user.build_profile
=> #<Profile id: nil, created_at: nil, updated_at: nil>
irb(main):003:0> new_user.save
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO "profiles" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2018-04-21 13:16:23.669286"], ["updated_at", "2018-04-21 13:16:23.669286"]]
Profile Load (0.2ms) SELECT "profiles".* FROM "profiles" WHERE "profiles"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
SQL (0.2ms) INSERT INTO "users" ("profile_id", "created_at", "updated_at") VALUES (?, ?, ?) [["profile_id", 1], ["created_at", "2018-04-21 13:16:23.691292"], ["updated_at", "2018-04-21 13:16:23.691292"]]
(181.1ms) commit transaction
=> true
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.integer :profile_id
t.timestamps
end
end
end
class CreateProfiles < ActiveRecord::Migration[5.1]
def change
create_table :profiles do |t|
t.timestamps
end
end
end
class User < ApplicationRecord
belongs_to :profile, inverse_of: :user
end
class Profile < ApplicationRecord
has_one :user, inverse_of: :profile
end
tested it out all worked. Copied code to answer since its not readable in comment. Your problem must be somewhere else do u get any errors or paste migration files to under question
accepts_nested_attributes_for :profile fixed this exact issue, but many other have popped up.
I had to stop this update and rollback everything.
I'm creating an app to represent the pedigree of livestock. Each child has one dam (e.g. ewe) and one sire (e.g. ram). A dam/sire pairing can have multiple children (e.g. lambs) and a dam and sire may have many more children independent of the other. I am trying to represent this relationship so that I could do something like ewe.children and get a listing of her offspring. Similarly, I'd like to be able to do something like lamb.ewe to get her mother or lamb.ewe.ewe to get her maternal grandmother.
from schema.rb...
create_table "parent_child_relationships", force: :cascade do |t|
t.integer "parent_id"
t.integer "child_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["child_id"], name: "index_parent_child_relationships_on_child_id"
t.index ["parent_id", "child_id"], name: "index_parent_child_relationships_on_parent_id_and_child_id", unique: true
t.index ["parent_id"], name: "index_parent_child_relationships_on_parent_id"
end
from parent_child_relationship.rb...
class ParentChildRelationship < ApplicationRecord
belongs_to :sire, class_name: "Animal"
belongs_to :dam, class_name: "Animal"
belongs_to :children, class_name: "Animal"
end
from animal.rb...
has_one :sire_relationship, class_name: "ParentChildRelationship",
foreign_key: "child_id",
dependent: :destroy
has_one :dam_relationship, class_name: "ParentChildRelationship",
foreign_key: "child_id",
dependent: :destroy
has_many :child_relationships, class_name: "ParentChildRelationship",
foreign_key: "parent_id",
dependent: :destroy
has_one :sire, through: :sire_relationship, source: :child
has_one :dam, through: :dam_relationship, source: :child
has_many :children, through: :child_relationships, source: :parent
In the console, I run the following commands to grab the animals I want to relate to each other...
s = Shepherd.first
ewe = s.animals.find_by(id: 37)
ram = s.animals.find_by(id: 133)
lamb = s.animals.find_by(id: 61)
Now, when I try to create the sire_relationship and dam_relationship I get an error since it doesn't seem to see the relationship as being unique. The sire_relationship is replaced by the dam_relationship...
>> lamb.create_sire_relationship(parent_id: ram.id)
(0.1ms) begin transaction
SQL (0.7ms) INSERT INTO "parent_child_relationships" ("parent_id", "child_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["parent_id", 133], ["child_id", 61], ["created_at", "2018-01-15 15:33:06.649936"], ["updated_at", "2018-01-15 15:33:06.649936"]]
(2.5ms) commit transaction
ParentChildRelationship Load (0.2ms) SELECT "parent_child_relationships".* FROM "parent_child_relationships" WHERE "parent_child_relationships"."child_id" = ? LIMIT ? [["child_id", 61], ["LIMIT", 1]]
=> #<ParentChildRelationship id: 1, parent_id: 133, child_id: 61, created_at: "2018-01-15 15:33:06", updated_at: "2018-01-15 15:33:06">
>> lamb.create_dam_relationship(parent_id: ewe.id)
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "parent_child_relationships" ("parent_id", "child_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["parent_id", 37], ["child_id", 61], ["created_at", "2018-01-15 15:33:35.045703"], ["updated_at", "2018-01-15 15:33:35.045703"]]
(1.0ms) commit transaction
ParentChildRelationship Load (0.1ms) SELECT "parent_child_relationships".* FROM "parent_child_relationships" WHERE "parent_child_relationships"."child_id" = ? LIMIT ? [["child_id", 61], ["LIMIT", 1]]
(0.0ms) begin transaction
SQL (0.5ms) DELETE FROM "parent_child_relationships" WHERE "parent_child_relationships"."id" = ? [["id", 1]]
(1.2ms) commit transaction
=> #<ParentChildRelationship id: 2, parent_id: 37, child_id: 61, created_at: "2018-01-15 15:33:35", updated_at: "2018-01-15 15:33:35">
Creating the children_relationships, I get these errors...
>> ewe.child_relationships.create(child_id: lamb.id)
(0.1ms) begin transaction
SQL (0.9ms) INSERT INTO "parent_child_relationships" ("parent_id", "child_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["parent_id", 37], ["child_id", 61], ["created_at", "2018-01-15 15:37:11.436086"], ["updated_at", "2018-01-15 15:37:11.436086"]]
(0.1ms) rollback transaction
ActiveRecord::RecordNotUnique: SQLite3::ConstraintException: UNIQUE constraint failed: parent_child_relationships.parent_id, parent_child_relationships.child_id: INSERT INTO "parent_child_relationships" ("parent_id", "child_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)
from (irb):13
>> ram.child_relationships.create(child_id: lamb.id)
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "parent_child_relationships" ("parent_id", "child_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["parent_id", 133], ["child_id", 61], ["created_at", "2018-01-15 15:37:25.264947"], ["updated_at", "2018-01-15 15:37:25.264947"]]
(2.5ms) commit transaction
Finally, if I check to see whether I can access the sire of lamb, I get another error...
>> lamb.dam
ActiveRecord::HasManyThroughSourceAssociationNotFoundError: Could not find the source association(s) :child in model ParentChildRelationship. Try 'has_many :dam, :through => :dam_relationship, :source => <name>'. Is it one of sire, dam, or children?
from (irb):21
>> lamb.sire
ActiveRecord::HasManyThroughSourceAssociationNotFoundError: Could not find the source association(s) :child in model ParentChildRelationship. Try 'has_many :sire, :through => :sire_relationship, :source => <name>'. Is it one of sire, dam, or children?
from (irb):22
I get similar errors if I do ewe.children or ram.children.
I'm looking for an extra pair of eyes to tell me what I'm doing wrong or whether there's an easier way to achieve what I'm after.
The problem is that you only have one parent_id in your animals table which can only store the ID of a single parent. This works for bacteria but not for animals which have two parents. Your parent_id is getting written to when you set the dam and the sire.
There are multiple ways to do this but I think the simplest is to have a dam_id and sire_id in your animals table.
This is the migration to create the table:
class CreateAnimals < ActiveRecord::Migration[5.1]
def change
create_table :animals do |t|
t.integer :dam_id, index: true
t.integer :sire_id, index: true
t.timestamps
end
end
end
This is what your model will look like this. Notice that you need two belongs_to/has_many relationships:
class Animal < ApplicationRecord
belongs_to :dam, class_name: 'Animal'
belongs_to :sire, class_name: 'Animal'
has_many :children_as_sire, class_name: 'Animal', foreign_key: :sire_id
has_many :children_as_dam, class_name: 'Animal', foreign_key: :dam_id
def children
children_as_dam + children_as_sire
end
end
Notice the getter method children that grabs both children_as_dam and children_as_sire. This will result in two SQL queries which is not ideal. If you're tracking the sec of the Animal, you could do something like:
def children?
case sex
when 'male'
children_as_sire
when 'female'
children_as_dam
end
end
I wrote some specs to demonstrate:
require 'rails_helper'
RSpec.describe Animal, type: :model, focus: true do
it 'can be created' do
expect { Animal.create }.to_not raise_error
end
it 'can have a dam' do
animal = Animal.new
animal.update! dam: Animal.create
expect(animal.dam).to be_a(Animal)
expect(animal.sire).to be_nil
end
it 'can have a sire' do
animal = Animal.new
animal.update! sire: Animal.create
expect(animal.sire).to be_a(Animal)
expect(animal.dam).to be_nil
end
it 'can have both a dam and a sire and tell the difference' do
dam = Animal.create
sire = Animal.create
child = Animal.create dam: dam, sire: sire
expect(child.reload.dam).to eq(dam)
expect(child.reload.sire).to eq(sire)
end
it 'grandma' do
grandma = Animal.create
dam = Animal.create dam: grandma
child = Animal.create dam: dam
expect(child.reload.dam.dam).to eq(grandma)
end
it 'has children' do
sire = Animal.create
animal = Animal.create sire: sire
expect(sire.reload.children).to include(animal)
end
end
Notice that you can't add children to a model:
animal = Animal.create
animal.children << Animal.create # will raise an error
Instead, you have to manually set the sire and dam (which is probably what you want to do since you're keeping track).
I've been working on implementing a new model that belongs to one of our web apps existing models. Eventually, I want to build out a nested form. I understand that the form, and strong params can have it's own suite of issues, but I am currently struggling to get the models to behave as I would expect in the rails console.
Rails 4.2.7, Postgres DB
UPDATE - 10/3/16 - Still trying to find the right solution, but have made some changes
Our work is with Schools and Districts, and this particular case deals with surveys and how a survey is assigned to a school and district. Until now, a survey has been assigned to a district with a SurveyAssignment model, and some down the line logic assumed that all schools in a district were also "assigned" to the survey. Now, we want to be able to add more granularity to the SurveyAssignment and allow some specificity at the school level.
So I created a SchoolSurveyAssignment model and started to get the bits in place.
Here is the relevant model info:
class District < ActiveRecord::Base
...
has_many :schools, dependent: :destroy
has_many :survey_assignments, dependent: :destroy
...
end
class School
...
belongs_to :district
has_many :school_survey_assignments
has_many :survey_assignments, :through => :school_survey_assignments
...
end
class SurveyAssignment
belongs_to :district
belongs_to :survey
has_one :survey_version, through: :survey
has_many :school_survey_assignments, inverse_of: survey_assignment
has_many :schools, :through => :school_survey_assignments
accepts_nested_attributes_for :school_survey_assignments
attr_accessor :survey_group, :survey_version_type, :survey_version_id, :school_survey_assignments_attributes
validates :survey_id, presence: true
end
class SchoolSurveyAssignment
belongs_to :survey_assignment, inverse_of: :school_survey_assignments
belongs_to :school
attr_accessor :school_id, :survey_assignment_id, :grades_affected, :ulc_affected
validates_presence_of :survey_assignment
validates :school_id, presence: true, uniqueness: {scope: :survey_assignment_id}
end
Relevant Controller code:
class SurveyAssignmentsController < ApplicationController
before_action :set_district
before_action :set_survey_assignment, only: [:show, :edit, :update, :destroy]
respond_to :html, :json, :js
def new
#new_survey_assignment = SurveyAssignment.new()
#district.schools.each do |school|
#new_survey_assignment.school_survey_assignments.build(school_id: school.id)
end
end
def create
#survey_assignment = SurveyAssignment.new(survey_assignment_params)
if #survey_assignment.save
flash[:notice] = "Survey successfully assigned to #{#district.name}"
else
flash[:alert] = "There was a problem assigning this survey to #{#district.name}"
end
redirect_to district_survey_assignments_path(#district)
end
def survey_assignment_params
params.require(:survey_assignment).permit(:survey_id, :status, :survey_version_id, school_survey_assignments_attributes: [:id, :survey_assignment_id, :school_id, grades_affected: [], ulc_affected: []]).tap do |p|
p[:district_id] = #district.id
p[:school_year] = session[:selected_year]
end
end
def set_district
#district = District.find(params[:district_id])
end
Here is the relevant schema info:
create_table "school_survey_assignments", force: :cascade do |t|
t.integer "survey_assignment_id"
t.integer "school_id"
t.integer "grades_affected", default: [], array: true
t.string "ulc_affected", default: [], array: true
end
add_index "school_survey_assignments", ["school_id"], name: "index_school_survey_assignments_on_school_id", using: :btree
add_index "school_survey_assignments", ["survey_assignment_id"], name: "index_school_survey_assignments_on_survey_assignment_id", using: :btree
create_table "survey_assignments", force: :cascade do |t|
t.integer "district_id"
t.integer "survey_id"
t.integer "status"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "school_year"
t.integer "last_response_status_id"
end
add_index "survey_assignments", ["district_id"], name: "index_survey_assignments_on_district_id", using: :btree
Once these were in place, I stepped into my rails console and attempted the following:
2.3.1 :002 > sa1 = SurveyAssignment.create(district_id: 3, survey_id: 508, school_year: 2017)
(0.2ms) BEGIN
SQL (0.7ms) INSERT INTO "survey_assignments" ("district_id", "survey_id", "school_year", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["district_id", 3], ["survey_id", 508], ["school_year", 2017], ["created_at", "2016-09-30 21:30:20.205144"], ["updated_at", "2016-09-30 21:30:20.205144"]]
(7.2ms) COMMIT
=> #<SurveyAssignment id: 369, district_id: 3, survey_id: 508, status: nil, created_at: "2016-09-30 21:30:20", updated_at: "2016-09-30 21:30:20", school_year: 2017, last_response_status_id: nil>
2.3.1 :003 > sa2 = SurveyAssignment.create(district_id: 3, survey_id: 508, school_year: 2017)
(0.3ms) BEGIN
SQL (0.4ms) INSERT INTO "survey_assignments" ("district_id", "survey_id", "school_year", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["district_id", 3], ["survey_id", 508], ["school_year", 2017], ["created_at", "2016-09-30 21:30:30.701197"], ["updated_at", "2016-09-30 21:30:30.701197"]]
(0.5ms) COMMIT
=> #<SurveyAssignment id: 370, district_id: 3, survey_id: 508, status: nil, created_at: "2016-09-30 21:30:30", updated_at: "2016-09-30 21:30:30", school_year: 2017, last_response_status_id: nil>
So now, I've successfully created two Survey Assignments. I'm now going to create two School Survey Assignments off of sa1:
2.3.1 :004 > [{school_id: 5}, {school_id: 6}].each do |ssa|
2.3.1 :005 > sa1.school_survey_assignments.create(ssa)
2.3.1 :006?> end
(0.2ms) BEGIN
SchoolSurveyAssignment Exists (2.4ms) SELECT 1 AS one FROM "school_survey_assignments" WHERE ("school_survey_assignments"."school_id" = 5 AND "school_survey_assignments"."survey_assignment_id" = 369) LIMIT 1
SQL (0.4ms) INSERT INTO "school_survey_assignments" ("survey_assignment_id") VALUES ($1) RETURNING "id" [["survey_assignment_id", 369]]
(6.4ms) COMMIT
(0.6ms) BEGIN
SchoolSurveyAssignment Exists (0.4ms) SELECT 1 AS one FROM "school_survey_assignments" WHERE ("school_survey_assignments"."school_id" = 6 AND "school_survey_assignments"."survey_assignment_id" = 369) LIMIT 1
SQL (0.3ms) INSERT INTO "school_survey_assignments" ("survey_assignment_id") VALUES ($1) RETURNING "id" [["survey_assignment_id", 369]]
(0.4ms) COMMIT
=> [{:school_id=>5}, {:school_id=>6}]
2.3.1 :007 > sa1.save
(0.3ms) BEGIN
(0.4ms) COMMIT
=> true
Now, it looks like I've successfully created two SchoolSurveyAssignments with survey_assignment_id = 369 and school_ids = 5 and 6
2.3.1 :008 > sa1.school_survey_assignments
SchoolSurveyAssignment Load (0.3ms) SELECT "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."survey_assignment_id" = $1 [["survey_assignment_id", 369]]
=> #<ActiveRecord::Associations::CollectionProxy [#<SchoolSurveyAssignment id: 5, survey_assignment_id: 369, school_id: nil, grades_affected: [], ulc_affected: []>, #<SchoolSurveyAssignment id: 6, survey_assignment_id: 369, school_id: nil, grades_affected: [], ulc_affected: []>]>
As you can see from the ActivRecord::Associations::CollectionProxy, both of the SchoolSurveyAssignments were created, with survey_assignment_id: 369, but with a nil school_id. This is troubling as it seems to be
Ignoring the parameters being passed into the create function, and
ignoring the validation of school_id
Another item that I don't understand is the following:
2.3.1 :009 > SchoolSurveyAssignment.find(5).survey_assignment_id
SchoolSurveyAssignment Load (0.6ms) SELECT "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."id" = $1 LIMIT 1 [["id", 5]]
=> nil
2.3.1 :011 > SchoolSurveyAssignment.find(5).survey_assignment.id
SchoolSurveyAssignment Load (0.3ms) SELECT "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."id" = $1 LIMIT 1 [["id", 5]]
SurveyAssignment Load (0.4ms) SELECT "survey_assignments".* FROM "survey_assignments" WHERE "survey_assignments"."id" = $1 LIMIT 1 [["id", 369]]
=> 369
Calling .survey_assignment_id should return the attribute on the SchoolSurveyAssignment and give 369. .survey_assignment.id is simply just grabbing the parent object's ID. I would expect both to return the same value, but one returns nil.
The end use case is making a SurveyAssignment form that lets the user set the attributes for a new SurveyAssignment and also set the attributes for X number of SchoolSurveyAssignments (based on # of schools in a district; varies from 2 to 15). Once I get a better grasp on how these models are interacting, I feel confident in executing this goal, but the behavior I'm seeing doesn't make sense to me, and I was hoping to find some clarity on implementing these related models. I feel like I'm bouncing around the answer, but am missing a key detail.
Thanks,
Alex
Try removing your attr_accessor lines of code. attr_accessor shouldn't be used for attributes that are persisted in the database and it's probably messing up the methods that ActiveRecord already provides by default, causing those attributes to not be saved properly
class SurveyAssignment
belongs_to :district
belongs_to :survey
has_one :survey_version, through: :survey
has_many :school_survey_assignments, inverse_of: survey_assignment
has_many :schools, :through => :school_survey_assignments
accepts_nested_attributes_for :school_survey_assignments
validates :survey_id, presence: true
end
class SchoolSurveyAssignment
belongs_to :survey_assignment, inverse_of: :school_survey_assignments
belongs_to :school
validates_presence_of :survey_assignment
validates :school_id, presence: true, uniqueness: {scope: :survey_assignment_id}
end
For the first question, School and SurveyAssignment don't know each other, school_id becomes nil. In your app, these models have m-to-n association indirectly, so how about using has_many through association between models?
In School model, add below:
has_many :survey_assignments, :through => :school_survey_assignments
In SurveyAssignments model, add below:
has_many :schools, :through => :school_survey_assignments
For the last question, both codes seem to be same..
I'm trying to model a system where each user may have many emails (at least one).
Following good normalization rules I created two migrations (some fields removed for brevity):
create_table :users do |t|
end
create_table :user_emails do |t|
t.integer :user_id, null: false
t.string :email, null: false
end
add_index :user_emails, :email, :unique => true
add_foreign_key :user_emails, :users, dependent: :delete
and the following rails models:
class User < ActiveRecord::Base
has_many :emails, class_name: 'UserEmail', dependent: :destroy
def self.find_by_email(email)
UserEmail.find_by(email: email).try(:user)
end
validate do
if emails.count < 1
errors.add(:emails, "is empty")
end
end
end
class UserEmail < ActiveRecord::Base
belongs_to :user
validates_presence_of :user_id, :email
validates_uniqueness_of :email
end
Now I am not able to create any of those. User cannot be created since it requires at least an UserEmail. At the same time, UserEmail cannot be created beforehand since it requires an user_id.
I believe I have tried any combination of #user.emails.build and #user.emails << e that I can think of.
How can I solve this really simple problem without renouncing to data consistency (one is saved and the other is not)?
P.s.: I tought that maybe relaxing the validations and using transactions may solve the problem consistently. However I've never used transactions in rails, so any help is really appreciated.
Thanks
validates_presence_of :user_id only check for any valid integer which might not be a valid user.
But if you use validates_presence_of :user and validates_presence_of :emails, they will check the user and emails association are valid and not blank.
Also, when you are trying to create the user and the associated email, you are able to do the following code on application level.
user = User.new(name: 'user name')
user.emails.build(email_address: 'email address') #build(email: '') for your case.
Then, you can save to database with
user.save!
Below is the code I tested with.
class User < ActiveRecord::Base
has_many :emails, class_name: 'UserEmail', dependent: :destroy
validates_presence_of :emails, :message => 'User should have at least one email address.'
end
class UserEmail < ActiveRecord::Base
belongs_to :user
validates_presence_of :user
validates_presence_of :email_address
end
[40] pry(main)> u = User.create!(name: 'doh')
(0.1ms) begin transaction
(0.0ms) rollback transaction
ActiveRecord::RecordInvalid: Validation failed: Emails User should have at least one email address.
[1] pry(main)> u = User.new(name: 'nice')
=> #<User id: nil, name: "nice", created_at: nil, updated_at: nil>
[2] pry(main)> u.emails.build(email_address: 'hello#world.blah')
=> #<UserEmail id: nil, user_id: nil, email_address: "hello#world.blah", created_at: nil, updated_at: nil>
[3] pry(main)> u.save!
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO "users" ("created_at", "name", "updated_at") VALUES (?, ?, ?) [["created_at", "2014-07-26 10:14:52.184245"], ["name", "nice"], ["updated_at", "2014-07-26 10:14:52.184245"]]
SQL (0.2ms) INSERT INTO "user_emails" ("created_at", "email_address", "updated_at", "user_id") VALUES (?, ?, ?, ?) [["created_at", "2014-07-26 10:14:52.189757"], ["email_address", "hello#world.blah"], ["updated_at", "2014-07-26 10:14:52.189757"], ["user_id", 5]]
(0.7ms) commit transaction
=> true
[4] pry(main)> u.emails.count
(0.2ms) SELECT COUNT(*) FROM "user_emails" WHERE "user_emails"."user_id" = ? [["user_id", 5]]
=> 1
[5] pry(main)> u.id
=> 5
[6] pry(main)> u.name
=> "nice"
[7] pry(main)> u.emails
=> [#<UserEmail id: 4, user_id: 5, email_address: "hello#world.blah", created_at: "2014-07-26 10:14:52", updated_at: "2014-07-26 10:14:52">]
[8] pry(main)> u
=> #<User id: 5, name: "nice", created_at: "2014-07-26 10:14:52", updated_at: "2014-07-26 10:14:52">
[19] pry(main)> UserEmail.create!(email_address: 'test#test.org')
(0.1ms) begin transaction
(0.1ms) rollback transaction
ActiveRecord::RecordInvalid: Validation failed: User can't be blank
Hope this helps.
I just realized that I adding a NOT NULL constraint on the storage layer is sufficient. I can remove the :user_id presence validation and have the same consistency behavior, with the exception that it now works.
However this looks quite hacky from the rails POW. Any better solution is still really appreciated...