The simplified example is that I have several classes inheriting from an Asset class, which is an ActiveRecord model. When I use create or create! on one of the subclasses both the db layer and ActiveRecord layer validations for one field are ignored.
The Asset table has a type field with null: false, the Asset model has validates_presence_of :type and also has attribute :type, default: self.name.
If I use new on a subclass, such as Item, it behaves as expected, I get an Item with the type field set to "Item". If I use create or create! with valid attributes, the defaults are not applied, validation is ignored and I get a persisted record with a type field of nil.
What's odd is that other validations are respected. If I try creating a new Item without a name attribute, validates_presence_of :name properly raises a validation error.
Here's some pared down code snippets for reference:
class CreateAssets < ActiveRecord::Migration[6.0]
def change
create_table :assets do |t|
t.string :type, null: false
t.string :name
# ...
end
end
end
class Asset < ApplicationRecord
enum type: {
Item: :Item,
Accessory: :Accessory,
Component: :Component,
Consumable: :Consumable,
}
attribute :type, default: self.name
validates_presence_of :name
validates_presence_of :type
end
class Item < Asset
# ...
end
i = Item.create({ ... })
i.type
#nil
i.persisted?
#true
i.valid?
#false
i = Item.new({ ... })
i.type
#"Item"
i.valid?
#true
'type' is a reserved column name in ActiveRecord because it is used to indicate Single Table Inheritance (STI) . If you are not intending to use STI you should pick a different column name otherwise the STI behaviors will interfere with what you're trying to do here.
Related
I have this model Person
class Person
generate_public_uid generator: PublicUid::Generators::HexStringSecureRandom.new(32)
has_many :addresses, as: :resource, dependent: :destroy
accepts_nested_attributes_for :addresses, allow_destroy: true, update_only: true,
reject_if: proc { |attrs| attrs[:content].blank? }
end
in my person table, I have this public_id that is automatic generated when a person is created.
now the nested attribute in adding addresses is working fine. but the update is not the same as what nested attribute default does.
my goal is to update the addresses using public_id
class Address
generate_public_uid generator: PublicUid::Generators::HexStringSecureRandom.new(32)
belongs_to :resource, polymorphic: true
end
this is my address model
{ person: { name: 'Jack', addresses_attributes: { id: 1, content: 'new#gmail.com' } } }
this is the rails on how to update the record in the nested attribute
{ person: { name: 'Jack', addresses_attributes: { public_id: XXXXXXXX, content: 'new#gmail.com' } } }
I want to use the public_id to update records of addresses, but sadly this is not working any idea how to implement this?
Rails generally assumes that you have a single column named id that is the primary key. While it is possible to work around this, lots of tools in and around Rails assume this default – so you'll be giving yourself major headaches if you stray from this default assumption.
However, you're not forced to use integer ids. As someone else has already pointed out, you can change the type of the ID. In fact, you can supply any supported type by doing id: type, where type can e.g. be :string in your case. This should then work with most if not all of Rails' default features (including nested attributes) and also with most commonly used gems.
Since you say you are using your public_id as primary key I assume you don't mind dropping the current numbered id. The main advantage of not using an auto increment numbered key is that you don't publicly show record creation growth and order of records. Since you are using PostgreSQL, you could use a UUID is id which achieves the same goal as your current PublicUid::Generators::HexStringSecureRandom.new(32) (but does have a different format).
accepts_nested_attributes_for uses the primary key (which is normally id). By using UUIDs as data type for your id columns, Rails will automatically use those.
I've never used this functionality myself, so I'll be using this article as reference. This solution does not use the public_uid gem, so you can remove that from your Gemfile.
Assuming you start with a fresh application, your first migration should be:
bundle exec rails generate migration EnableExtensionPGCrypto
Which should contain:
def change
enable_extension 'pgcrypto'
end
To enable UUIDs for all future tables create the following initializer:
# config/initializers/generators.rb
Rails.application.config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
end
With the above settings changes all created tables should use an UUID as id. Note that references to other tables should also use the UUID type, since that is the type of the primary key.
You might only want to use UUIDs for some tables. In this case you don't need the initializer and explicitly pass the primary key type on table creation.
def change
create_table :people, id: :uuid, do |t|
# explicitly set type uuid ^ if you don't use the initializer
t.string :name, null: false
t.timestamps
end
end
If you are not starting with a fresh application things are more complex. Make sure you have a database backup when experimenting with this migration. Here is an example (untested):
def up
# update the primary key of a table
rename_column :people, :id, :integer_id
add_column :people, :id, :uuid, default: "gen_random_uuid()", null: false
execute 'ALTER TABLE people DROP CONSTRAINT people_pkey'
execute 'ALTER TABLE people ADD PRIMARY KEY (id)'
# update all columns referencing the old id
rename_column :addresses, :person_id, :person_integer_id
add_reference :addresses, :people, type: :uuid, foreign_key: true, null: true # or false depending on requirements
execute <<~SQL.squish
UPDATE addresses
SET person_id = (
SELECT people.id
FROM people
WHERE people.integer_id = addresses.person_integer_id
)
SQL
# Now remove the old columns. You might want to do this in a separate
# migration to validate that all data is migrating correctly.
remove_column :addresses, :person_integer_id
remove_column :people, :integer_id
end
The above provides an example scenario, but should most likely be extended/altered to fit your scenario.
I suggest to read the full article which explains some additional info.
Because you still need an :id field in your params, unless you want to change your to_param directly in model. Try something like this:
person = Person.first
address = person.address
person.update({ name: 'Jack', adddresses_attributes: { id: address.id, public_id: XXX, _destroy: true } } )
This is the way I have my nested-attributes
#app/models/person.rb
class Person < ApplicationRecord
...
has_many :addresses, dependent: :destroy
accepts_nested_attributes_for :addresses, reject_if: :all_blank, allow_destroy: true
...
end
my controller
#app/controllers/people_controller.rb
class PeopleController < ApplicationController
...
def update
#person = Person.find_by(id: params[:id])
if #person.update(person_params)
redirect_to person_path, notice: 'Person was successfully added'
else
render :edit, notice: 'There was an error'
end
end
...
private
def person_params
params.require(:person).permit(
... # list of person fields
addresses_attributes: [
:id,
:_destroy,
... # list of address fields
]
)
end
...
end
I hope that this is able to help you.
Let me know if you need more help
I have a few enums in my project that will be reused across multiple models, and a few of which will have their own internal logic, and so I've implemented them as value objects (as described here # section 5) but I can't seem to get ActiveRecord validations to work with them. The simplest example is the Person model with a Gender value object.
Migration:
# db/migrate/###_create_people.rb
class CreatePeople < ActiveRecord::Migration[5.2]
def change
create_table :people do |t|
t.string :name
t.integer :age
t.integer :gender
end
end
end
Model:
# app/models/person.rb
class Person < ApplicationRecord
validates :gender, presence: true
enum gender: Enums::Gender::GENDERS
def gender
#gender ||= Enums::Gender.new(read_attribute(:gender))
end
end
Value Object:
# app/models/enums/gender.rb
module Enums
class Gender
GENDERS = %w(female male other).freeze
def initialize(gender)
#gender = gender
end
def eql?(other)
to_s.eql?(other.to_s)
end
def to_s
#gender.to_s
end
end
end
The only problem is that despite the model being set to validate the presence of the gender attribute, it allows a Person to be saved with a gender of nil. I'm not sure why that is, so I'm not sure where to start trying to fix the problem.
So I figured it out myself. Big thanks to benjessop whose suggestion didn't work, but did set me on the right train of thought.
validates :gender, numericality: { integer_only: true, greater_than_or_equal_to: 0, less_than: Enums::Gender::GENDERS.count }
I'll probably write a custom validation to implement that logic into several different value object enums. Thanks again for those that tried to help.
In your model file person.rb:
enum gender: Enums::Gender::GENDERS
But, In your model file gender.rb:
the constant is GENDER
Change the line in person.rb to:
enum gender: Enums::Gender::GENDER
instead of
enum gender: Enums::Gender::GENDERS
I have the following code in my Rails 'seeds.rb' file:
acondigitaladdresslineone = Addressline.create!(address_id: acondigitaladdress.id, address: "Sørkedalsveien 273", address_line_position: 0)
The 'Address' model has a one-to-many relation with the 'Addressline' model, and the respective 'belongs_to' and 'has_many' declarations are properly in place. Here is the content of the migration files:
class CreateAddresses < ActiveRecord::Migration[5.0]
def change
create_table :addresses do |t|
t.references :towncity, foreign_key: true
t.references :stateregion, foreign_key: true, null: true
t.references :postalcode, foreign_key: true
t.timestamps
end
end
end
class CreateAddresslines < ActiveRecord::Migration[5.0]
def change
create_table :addresslines do |t|
t.references :address, foreign_key: true
t.string :address
t.integer :address_line_position
t.timestamps
end
end
end
When I run the Rails Console, I receive the following error, which refers to the "acondigitaladdresslineone = ..." line of code:
ActiveRecord::AssociationTypeMismatch: Address(#8269560) expected, got String(#6922340)
I corrected the error by making the following changes:
# From the 'CreateAddresslines' migration file
# Original code
t.string :address
# Revised code
t.string :address_name
# From the 'acondigitaladdresslineone' variable declaration line
# Original code
address: "Sørkedalsveien 273"
# Revised code
address_name: "Sørkedalsveien 273"
I suspect the cause of the error possibly is due the underlying database's (in this case SQLite) inability to disambiguate the same field name based upon datatype. For example, in the original version of the 'CreateAddresslines' migration file, SQLlite raises an error as a result of these two declarations:
t.references :address, foreign_key: true
t.string :address
It is easy to overlook the obvious when entangled in coding. Clearly, a relational database table cannot have two or more columns with the same name. I am unaware of any relational database management system having a capability to disambiguate same-named table columns based upon differences in their datatypes.
I need a sanity check. Is this a reasonable assumption why the 'AssociationTypeMismatch' error was raised?
Thank you.
t.references :address will create the column address_id so there should be no conflict with t.string :address on the DB level.
The problem occurs in ActiveRecord when accessors and associations are declared.
ActiveRecord reads the schema from the database and uses it to create attributes and accessor methods in your models for each column. This happens before any of the code in the class declaration block is evaluated.
When you then create an association you are overwriting the #address and #address= methods created for the string attribute.
class Addressline
belongs_to :address
end
Which is why when you do:
Addressline.new(address: 'Foo')
You get a type error since the #address= setter method is setting address_id and expects an Address instance.
The solution is as you might have surmised is to not name other columns the same thing as your associations. Just rename the column:
class FixColumnName < ActiveRecord::Migration
def self.change
# call it whatever you want, but not address!
rename_column :addresslines, :address, :value
end
end
When I run
irb(main):003:0> House.new(name: "A house")
I get the error
ActiveRecord::StatementInvalid: Could not find table 'houses'
from /home/overflow012/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.0.rc1/lib/active_record/connection_adapters/sqlite3_adapter.rb:429:in `table_structure'
...
You can see my code below
property.rb
class Property < ApplicationRecord
self.abstract_class = true
end
apartment.rb
class Apartment < Property
end
house.rb
class House < Property
end
db/migrate/20160616022800_create_properties.rb
class CreateProperties < ActiveRecord::Migration[5.0]
def change
create_table :properties do |t|
t.string :name
t.string :detail
t.float :price
t.string :type
t.timestamps
end
end
end
And the properties table was created via rake db:migrate
Note: I'm using rails 5.0.0.rc1
What am I doing wrong?
I believe you need to remove the self.abstract_class line from your Property model.
Adding abstract_class to the model will force child classes to bypass the implied STI table naming of the parent class Property. Essentially, we're saying Property can no longer be instantiated, and is not backed by a database table.
Therefore, child classes of Property are not going to look to the parent class for the table name, they will look for a table based on their own class name.
Alternatively, you could set self.table_name = 'properties' in your Property model and that should work. However, this defeats the purpose of defining an abstract_class.
I am trying to make a conditional validation of a field. Such that it only validates if another field is a specific value. The problem here is, that this other field is a one to many relation, and I can't seem to get it working.
Here is the relevant code:
class CreateInvolvedPartyTypes < ActiveRecord::Migration
def change
create_table :involved_party_types do |t|
t.string :code
t.string :name
t.timestamps null: false
end
end
end
class CreateInvolvedParties < ActiveRecord::Migration
def change
create_table :involved_parties do |t|
t.string :first_name
t.string :last_name
t.references :involved_party_type
t.timestamps null: false
end
end
end
class InvolvedParty < ActiveRecord::Base
def ipt_cd?
self.involved_party_type.code == 'I'
end
validates :first_name, presence: { message: "Please insert first name" }
validates :last_name, presence: { message: "Please insert last name" }, :if => :ipt_cd?
validates :involved_party_type, presence: { message: "Please select involved party type" }
belongs_to :involved_party_type
end
The above code fails with:
undefined method `code' for nil:NilClass
Thanks for your help
The error means that self.involved_party_type in InvolvedParty#ipt_cd? is nil. You should test the presence of involved_party_type before calling #code on it, or use #try.
def ipt_cd?
return false if involved_party.nil?
involved_party_type.code == 'I'
end
def ipt_cd?
self.involved_party_type.try(:code) == 'I'
end
Or you can avoid the problem by only invoking the validation if involved_party_type exists.
validates :last_name, presence: { message: "Please insert last name" }, if: -> { involved_party_type && ipt_cd? }
I think the issue is that you're getting confused with calling instance and class level data.
"Instance" data is populated each time a class is invoked
"Class" data is static, always appending to the class
The cosmetic difference between the two is that class data is typically called through self (EG def self.method & self.attribute), whilst instance data is called with "naked" attributes (IE without self).
You're calling the following:
def ipt_cd?
self.involved_party_type.code == 'I'
end
The problem is that you're referencing self as if it's a piece of class data. What you want is the instance equivalent:
def ipt_cd?
involved_party_type.code == 'I'
end
As the other answer states, your error is caused by a piece of data having no method for code, meaning it's nil.
The casue of this is here (solution is above -- IE remove self):
involved_party_type.code == 'I'
Thus, if you want to make sure you don't receive this error, you'll have to ensure that involved_party_type is present. This can be done by first ensuring you're referencing the instance variant of the data, followed by ensuring it's there anyway. The other answer provided the best way to achieve that.
Finally, I think your structure could be improved.
Referencing the actual data representation of an associated field is bad practice in my opinion. You're trying to create a new piece of data, and yet you're referencing an associated attribute?
Why not do the following:
#app/models/party_type.rb
class PartyType < ActiveRecord::Base
has_many :involved_parties
end
class InvolvedParty < ActiveRecord::Base
belongs_to :party_type
validates :first_name, :party_type, presence: true
validates :last_name, presence: { message: "Please insert last name" }, if: :cd?
private
def cd?
party_type == PartyType.find_by(code: "I").pluck(:id)
end
end
This will send another DB query but it removes the dependency on specific data. Your current setup is not relying on foreign keys, but on a value which may change.
Whilst this recommendation also relies on data (IE code == I), it uses it as a quantifier within ActiveRecord. That is, you're not comparing the data, but the relationship.