How do you work with an optional association in ActiveAdmin? I have two models,
class A
self.primary_key = :serialnumber
has_many :bs, foreign_key: :a_serialnumber, inverse_of: :a
end
and
class B
belongs_to :a, foreign_key: :a_serialnumber, inverse_of: :bs, optional: true
end
As well as a standard form generated by ActiveAdmin. The problem is that if you do not fill the "a" attribute in the form, then it is blank instead of nil and the following error appears:
PG::ForeignKeyViolation: ERROR: insert or update on table "bs" violates foreign key constraint "fk_rails_784ba3f655"
DETAIL: Key (a_serialnumber)=() is not present in table "as".
How can you avoid this error? (of course you can write a setter that only saves the presence of the attribute, but is there some nicer way?)
Related
I have 2 Mongoid classes:
class Reservation
include Mongoid::Document
belongs_to :listing, class_name: 'Listing', inverse_of: 'Reservation'
end
class Listing
include Mongoid::Document
has_many :reservations, class_name: 'Reservation', foreign_key: :listing_id
end
If I find the listing by _id and #save! it, there is no error raised
listing = Listing.find(...)
listing.save! #=> true
Then I initialize a new reservation obect:
reservation = Reservation.find_or_initialize_by(params)
reservation.save! #=> error, exptected though, because I didn't set the listing_id yet
Mongoid::Errors::Validations:
message:
Validation of Reservation failed.
summary:
The following errors were found: Listing can't be blank
resolution:
Try persisting the document with valid data or remove the validations.
from /home/ec2-user/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/bundler/gems/mongoid-71c29a805990/lib/mongoid/persistable.rb:78:in `fail_due_to_validation!'
So I assign the earlier listing's id to the reservation:
reservation.listing_id = listing._id
reservation.listing_id #=> nil
I can't even assign the listing_id field ?!
reservation.listing #=> returns the associated document no problem though..
reservation.listing.save! #=> error
Mongoid::Errors::Validations:
message:
Validation of Listing failed.
summary:
The following errors were found: Reservations is invalid
resolution:
Try persisting the document with valid data or remove the validations.
from /home/ec2-user/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/bundler/gems/mongoid-71c29a805990/lib/mongoid/persistable.rb:78:in `fail_due_to_validation!'
Can't save the reservation without a valid listing,
Can't save the listing without a valid reservation
WHAT IS THIS?!?!
Please save my day...
You actually have to specify the inverse field in inverse_of, so try with something like:
class Reservation
include Mongoid::Document
belongs_to :listing, class_name: 'Listing', inverse_of: :reservations
end
class Listing
include Mongoid::Document
has_many :reservations, class_name: 'Reservation', inverse_of :listing
end
I also replaced the foreign_key by inverse_of for the has_many relation, it's easier to let Mongoid guess the foreign key name :)
Then, check the validations you specified and you did not included in your post, but if you first create a listing you should be able to create a reservation for it without any problem.
Also, assigning the directly the object is fine too, and often easier, so you can directly write reservation.listing = my_listing
Given tables with integer and uuid primary keys what is the best way to integrate a polymorphic join (has_many)? For example:
class Interest < ActiveRecord::Base
# id is an integer
has_many :likes, as: :likeable
end
class Post < ActiveRecord::Base
# id is a UUID
has_many :likes, as: :likeable
end
class User < ActiveRecord::Base
has_many :likes
has_many :posts, through: :likes, source: :likeable, source_type: "Post"
has_many :interests, through: :likes, source: :likeable, source_type: "Interest"
end
class Like < ActiveRecord::Base
# likeable_id and likeable_type are strings
belongs_to :likeable, polymorphic: true
belongs_to :user
end
Many queries work:
interest.likes
post.likes
user.likes
However:
user.interests
Gives:
PG::UndefinedFunction: ERROR: operator does not exist: integer = character varying
LINE 1: ...interests" INNER JOIN "likes" ON "interests"."id" = "likes"....
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "interests".* FROM "interests" INNER JOIN "likes" ON "interests"."id" = "likes"."likeable_id" WHERE "likes"."user_id" = $1 AND "likes"."likeable_type" = $2
What's the best way to include ensure the proper casting happens?
This is an old question, but here's my recommendation.
This is more of an architecture problem. Don't combine UUID ids and integer ids, it get's messy real fast. If you can, migrate the integer IDs to UUID or revert the uuids to integer ids.
My experience has been that the best solution is probably to make use of the rather nice Friendly ID gem: https://github.com/norman/friendly_id
In the off case this is broken in the future, it is basically just a slug generation/managemnet tool, the slug would use this kind of route path: posts/this-is-a-potential-slug instead of posts/1, but nothing prevents you from using posts/<UUID here> or posts/<alphanumeric string here>.
Typically if you are using UUIDs it's because you don't want to show the sequential integers. Friendly ID works well to avoid that issue.
There's no means to specify the necessary cast using Rails. Instead, add a generated column with the cast, and declare an extra belongs_to association to use it. For example, with this in a migration:
add_column :interests, :_id_s, 'TEXT GENERATED ALWAYS AS (id::text) STORED'
add_index :interests, :_id_s
and this in your models:
class Like
belongs_to :_likeable_cast, polymorphic: true, primary_key: :_id_s, foreign_key: :likeable_id, foreign_type: :likeable_type
class User
has_many :interests, through: :likes, source: :_likeable_cast, source_type: "Interest"
then user.interests joins through the alternative association, i.e. using the generated column with the cast.
I suggest using a column type of text rather than varchar for the likeable_id column, to avoid unnecessary conversions during the join and ensure the index is used.
Can you describe your likes table? I suppose that it contains
user_id as integer,
likeable_id as integer,
likeable_type as integer
any third-part fields
So, technically you can not create the same polymorphic association with uuid string and id as integer in scope of two fields likeable_id and likeable_type.
As solution - you can simply add id as primary key to posts table instead of uuid. In case if you maybe do not want to show id of post in URL, or for another security reasons - you can still use uuid as before.
You might be able to define your own method to retrieve likes in your Interest model.
def likes
Like.where("likeable_type = ? AND likeable_id = ?::text", self.class.name, id)
end
The problem with this solution is that you're not defining the association, so something like 'has_many through' won't work, you'd have to define those methods/queries yourself as well.
Have you considered something like playing around with typecasting the foreign- or primary-key in the association macro? E.g. has_many :likes, foreign_key: "id::UUID" or something similar.
Tested on Rails 6.1.4
Having a likeable_id as string works well and rails takes care of the casting of IDs.
Here is an example of my code
Migration for adding polymorphic "owner" to timeline_event model
class AddOwnerToTimelineEvent < ActiveRecord::Migration[6.1]
def change
add_column :timeline_events, :owner_type, :string, null: true
add_column :timeline_events, :owner_id, :string, null: true
end
end
Polymorphic model
class TimelineEvent < ApplicationRecord
belongs_to :owner, polymorphic: true
end
Now we have 2 owner, Contact which has id as Bigint and Company which has id as uuid, you could see in the SQL that rails has already casted them to strings
contact.timeline_events
TimelineEvent Load (5.8ms) SELECT "timeline_events"."id", "timeline_events"."at_time",
"timeline_events"."created_at", "timeline_events"."updated_at",
"timeline_events"."owner_type", "timeline_events"."owner_id" FROM
"timeline_events" WHERE "timeline_events"."owner_id" = $1 AND
"timeline_events"."owner_type" = $2 [["owner_id", "1"],
["owner_type", "Contact"]]
company.timeline_events
TimelineEvent Load (1.3ms) SELECT "timeline_events"."id", "timeline_events"."action",
"timeline_events"."at_time", "timeline_events"."created_at",
"timeline_events"."updated_at", "timeline_events"."owner_type",
"timeline_events"."owner_id" FROM "timeline_events" WHERE
"timeline_events"."owner_id" = $1 AND "timeline_events"."owner_type" =
$2 [["owner_id", "0b967b7c-8b15-4560-adac-17a6970a4274"],
["owner_type", "Company"]]
There is a caveat though when you are loading timeline_events for a particular owner type and rails cannot do the type casting for you
have to do the casting yourself. for e.g. loading timelines where owner is a Company
TimelineEvent.where(
"(owner_type = 'Company' AND uuid(owner_id) in (:companies))",
companies: Company.select(:id)
)
I'm not good with ActiveRecord, and this is definitely not the answer you're looking for, but if you need a temporary *ugly workaround till you can find a solution, you could override the getter :
class User
def interests
self.likes.select{|like| like.likeable._type == 'Interest'}.map(&:likeable)
end
end
*Very ugly cause it will load all the user likes and then sort them
EDIT I found this interesting article :
self.likes.inject([]) do |result, like|
result << like.likeable if like.likeable._type = 'Interest'
result
end
I am making an ecommerce site, and I have Purchases which has_one :shipping_address and has_one :billing_address
In the past the way I've implemented this is to structure my models like so:
class Address < ActiveRecord::Base
belongs_to :billed_purchase, class_name: Purchase, foreign_key: "billed_purchase_id"
belongs_to :shipped_purchase, class_name: Purchase, foreign_key: "shipped_purchase_id"
belongs_to :state
end
class Purchase < ActiveRecord::Base
INCOMPLETE = 'Incomplete'
belongs_to :user
has_one :shipping_address, class: Address, foreign_key: "shipped_purchase_id"
has_one :billing_address, class: Address, foreign_key: "billed_purchase_id"
...
end
As you can see, I reuse the Address model and just mask it as something else by using different foreign keys.
This works completely find, but is there a cleaner way to do this? Should I be using concerns? I'm sure the behavior of these two models will always be 100% the same, so I'm not sure if splitting them up into two tables is the way to go. Thanks for your tips.
EDIT The original version of this was wrong. I have corrected it and added a note to the bottom.
You probably shouldn't split it into two models unless you have some other compelling reason to do so. One thing you might consider, though, is making the Address model polymorphic. Like this:
First: Remove the specific foreign keys from addresses and add polymorphic type and id columns in a migration:
remove_column :addresses, :shipping_purchase_id
remove_column :addresses, :billing_purchase_id
add_column :addresses, :addressable_type, :string
add_column :addresses, :addressable_id, :integer
add_column :addresses, :address_type, :string
add_index :addresses, [:addressable_type, :addressable_id]
add_index :addresses, :address_type
Second: Remove the associations from the Address model and add a polymorphic association instead:
class Address < ActiveRecord::Base
belongs_to :addressable, polymorphic: true
...
end
Third: Define associations to it from the Purchase model:
class Purchase < ActiveRecord::Base
has_one :billing_address, -> { where(address_type: "billing") }, as: :addressable, class_name: "Address"
has_one :shipping_address, -> { where(address_type: "shipping") }, as: :addressable, class_name: "Address"
end
Now you can work with them like this:
p = Purchase.new
p.build_billing_address(city: "Phoenix", state: "AZ")
p.build_shipping_address(city: "Indianapolis", state: "IN")
p.save!
...
p = Purchase.where(...)
p.billing_address
p.shipping_address
In your controllers and views this will work just like what you have now except that you access the Purchase for an Address by calling address.addressable instead of address.billed_purchase or address.shipped_purchase.
You can now add additional address joins to Purchase or to any other model just by defining the association with the :as option, so it is very flexible without model changes.
There are some disadvantages to polymorphic associations. Most importantly, you can't eager fetch from the Address side in the above setup:
Address.where(...).includes(:addressable) # <= This will fail with an error
But you can still do it from the Purchase side, which is almost certainly where you'd need it anyway.
You can read up on polymorphic associations here: Active Record Association Guide.
EDIT NOTE: In the original version of this, I neglected to add the address_type discriminator column. This is pernicious because it would seem like it is working, but you'd get the wrong address records back after the fact. When you use polymorphic associations, and you want to associate the model to another model in more than one way, you need a third "discriminator" column to keep track of which one is which. Sorry for the mixup!
In addtion to #gwcoffey 's answer.
Another option would be using Single Table Inhertinace which perhaps suits more for that case, because every address has a mostly similar format.
I have 3 models: Question, Option, Rule
Question has_many options;
Option needs a foreign key for question_id
Rule table consists of 3 foreign_keys:
2 columns/references to question_ids -> foreign keys named as 'assumption_question_id' and 'consequent_question_id'
1 column/reference to option_id -> foreign key named as option_id or condition_id
Associations for Rule:
Question has_many rules; and
Option has_one rule
I want to understand how to write up migrations for this, and how that associates to the 'has_many'/'belongs_to' statements I write up in my model, and the ':foreign_key' option I can include in my model.
I had this for my Option migration, but I'm not sure how the "add_index" statement works in terms of foreign keys, and how I can use it for my Rule migration: (my Question and Options models have appropriate has_many and belongs_to statements - and work fine)
class CreateOptions < ActiveRecord::Migration
def change
create_table :options do |t|
t.integer :question_id
t.string :name
t.integer :order
t.timestamps
end
add_index :options, :question_id
end
end
Thank you for the help!
Note: I have found this way to solve the problem.Kindness from China.
If you have RailsAdmin with you,you may notice that you can see all rules of one question as long as one field of both question fields(assumption_question_id,consequent_question_id) equals to id of the question.
I have done detailed test on this and found out that Rails always generates a condition "question_id = [current_id]" which make to_sql outputs
SELECT `rules`.* FROM `rules` WHERE `rules`.`question_id` = 170
And the reason that the following model
class Question < ActiveRecord::Base
has_many :options
# Notice ↓
has_many :rules, ->(question) { where("assumption_question_id = ? OR consequent_question_id = ?", question.id, question.id) }, class_name: 'Rule'
# Notice ↑
end
makes Question.take.rules.to_sql be like this
SELECT `rules`.* FROM `rules` WHERE `rules`.`question_id` = 170 AND (assumption_question_id = 170 OR consequent_question_id = 170)
Is that we have not yet get ride of the annoy question_id so no matter how we describe or condition properly, our condition follows that "AND".
Then,we need to get ride of it.How?
Click here and you will know how,Find sector 8.1,and you can see
Article.where(id: 10, trashed: false).unscope(where: :id)
# SELECT "articles".* FROM "articles" WHERE trashed = 0
Then lets do it:
class Question < ActiveRecord::Base
has_many :options
# Notice ↓
has_many :rules, ->(question) { unscope(where: :question_id).where("assumption_question_id = ? OR consequent_question_id = ?", question.id, question.id) }, class_name: 'Rule'
# Notice ↑
end
class Rule < ActiveRecord::Base
belongs_to :option
belongs_to :assumption_question, class_name: "Question", foreign_key: :assumption_question_id, inverse_of: :assumption_rules
belongs_to :consequent_question, class_name: "Question", foreign_key: :consequent_question_id, inverse_of: :consequent_rules
end
class Option < ActiveRecord::Base
belongs_to :question
has_one :rule
end
All done.
Finally
This is my first answer here at stackoverflow,and this method is never found anywhere else.
Thanks for reading.
add_index adds an index to column specified, nothing more.
Rails does not provide native support in migrations for managing foreign keys. Such functionality is included in gems like foreigner. Read the documentation that gem to learn how it's used.
As for the associations, just add the columns you mentioned in your Question to each table (the migration you provided looks fine; maybe it's missing a :rule_id?)
Then specify the associations in your models. To get you started
class Question < ActiveRecord::Base
has_many :options
has_many :assumption_rules, class_name: "Rule"
has_many :consequent_rules, class_name: "Rule"
end
class Rule < ActiveRecord::Base
belongs_to :option
belongs_to :assumption_question, class_name: "Question", foreign_key: :assumption_question_id, inverse_of: :assumption_rules
belongs_to :consequent_question, class_name: "Question", foreign_key: :consequent_question_id, inverse_of: :consequent_rules
end
class Option < ActiveRecord::Base
belongs_to :question
has_one :rule
end
Note This is just a (untested) start; options may be missing.
I strongly recommend you read
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
http://guides.rubyonrails.org/association_basics.html
Edit: To answer the question in your comment
class Option < ActiveRecord::Base
belongs_to :question
# ...
The belongs_to tells rails that the question_id column in your options table stores an id value for a record in your questions table. Rails guesses the name of the column is question_id based on the :question symbol. You could instruct rails to look at a different column in the options table by specifying an option like foreign_key: :question_reference_identifier if that was the name of the column. (Note your Rule class in my code above uses the foreign_key option in this way).
Your migrations are nothing more than instructions which Rails will read and perform commands on your database based from. Your models' associations (has_many, belongs_to, etc...) inform Rails as to how you would like Active Record to work with your data, providing you with a clear and simple way to interact with your data. Models and migrations never interact with one another; they both independently interact with your database.
You can set a foreign key in your model like this:
class Leaf < ActiveRecord::Base
belongs_to :tree, :foreign_key => "leaf_code"
end
You do not need to specify this in a migration, rails will pull the foreign key from the model class definition.
I am using Ruby 1.8.7 and Rails 2.3.8 and I have the following root resource:
class PointOfInterest < ActiveRecord::Base
set_primary_key "Id"
set_table_name "POI"
has_many :attributes, :foreign_key => 'POIId'
end
The point of interest can have several attributes:
class Attribute < ActiveRecord::Base
set_primary_key "Id"
set_table_name "Attribute"
belongs_to :point_of_interest, :foreign_key => 'POIId'
has_one :multimedia, :foreign_key => 'Id', :primary_key => 'Value'
end
The attribute class may have media associated with it:
class Multimedia < ActiveRecord::Base
set_primary_key "Id"
set_table_name "Multimedia"
end
I am trying to insert a point of interest in my database like so:
poi = PointOfInterest.new
attr = poi.attributes.new
attr.SomeAttribute = 1
attr.build_multimedia(:content => 'test')
poi.save
This is properly persisting both the root (PointOfInterest) and the Multimedia record. The Attribute, however, is not being properly persisted. While the foreign key to the point of interest is properly set (POIId), the foreign key to the Multimedia record remains null.
Any clue as to why this is very much appreciated!
Thanks!
Your relationship / foreign key are set at cross purposes.
If Attribute has_one Multimedia, then you need the Multimedia model to declare belongs_to :attribute, and the Multimedia table to contain a foreign key for Attribute.
I'm guessing that you can't change your database schema (otherwise, I'd have to ask why you're using non-Rails-standard table and key names); in which case you want to switch the sense of the relation. Make Attribute belongs_to :multimedia, and Multimedia has_one :attribute. Then the Multimedia FK in the Attribute table is pointing in the right direction.