I have a four-level deep model structure: Domain > Subject > Device > Property
class Domain < ApplicationRecord
has_many :subjects
end
class Subject < ApplicationRecord
has_many :devices
belongs_to: :domain
end
class Device < ApplicationRecord
has_many :properties
belongs_to: :subject
end
class Property < ApplicationRecord
belongs_to :device
end
controller code
def update
result = #subject.update(parameters)
if result
render json: #subject
else
render_errors(#subject.errors)
end
end
#subject is retrieved as a before action, by querying the model tree from domain onwards, using domain_id and id parameters for domain and subject respectively. parameters is simply a hash of parameters, e.g. {name: :new_name}
When updating a Subject, the relation to domain is lost, i.e. domain_id is set to NUL by rails. The entire model tree below subject will also be disconnected from the parent domain as a result.
When removing has_many: :devices from the Subject model, everything works as expected. I just want to update a subject and preserve the relation to the parent domain. How would I achieve this with the model described above?
EDIT 1 - Added log of both situations.
Log with full relational model (that results in the bug)
Domain Load (0.5ms) SELECT "domains".* FROM "domains" WHERE "domains"."name" = ? LIMIT ? [["name", "Manatree"], ["LIMIT", 1]]
CACHE (0.0ms) SELECT "domains".* FROM "domains" WHERE "domains"."name" = ? LIMIT ? [["name", "Manatree"], ["LIMIT", 1]]
Subject Load (0.0ms) SELECT "subjects".* FROM "subjects" WHERE "subjects"."domain_id" = ? AND "subjects"."name" = ? LIMIT ? [["domain_id", 3], ["name", "s1"], ["LIMIT", 1]]
Subject Load (0.5ms) SELECT "subjects".* FROM "subjects" WHERE "subjects"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]]
(0.0ms) begin transaction
Domain Load (0.0ms) SELECT "domains".* FROM "domains" WHERE "domains"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
SQL (7.1ms) UPDATE "subjects" SET "domain_id" = NULL WHERE "subjects"."domain_id" = ? AND "subjects"."id" = 5 [["domain_id", 3]]
Subject Exists (0.0ms) SELECT 1 AS one FROM "subjects" WHERE "subjects"."domain_id" = ? AND "subjects"."name" = ? LIMIT ? [["domain_id", 3], ["name", "s2"], ["LIMIT", 1]]
SQL (1.0ms) UPDATE "subjects" SET "name" = ?, "updated_at" = ? WHERE "subjects"."id" = ? [["name", "s2"], ["updated_at", "2017-07-31 08:46:38.171240"], ["id", 5]]
(7.5ms) commit transaction
Subject Load (0.0ms) SELECT "subjects".* FROM "subjects"
Completed 200 OK in 30ms (Views: 0.5ms | ActiveRecord: 16.6ms)
Log when removing belongs_to: :devices from Subject model
Domain Load (0.0ms) SELECT "domains".* FROM "domains" WHERE "domains"."name" = ? LIMIT ? [["name", "Manatree"], ["LIMIT", 1]]
CACHE (0.0ms) SELECT "domains".* FROM "domains" WHERE "domains"."name" = ? LIMIT ? [["name", "Manatree"], ["LIMIT", 1]]
Subject Load (0.5ms) SELECT "subjects".* FROM "subjects" WHERE "subjects"."domain_id" = ? AND "subjects"."name" = ? LIMIT ? [["domain_id", 3], ["name", "s3"], ["LIMIT", 1]]
Subject Load (0.5ms) SELECT "subjects".* FROM "subjects" WHERE "subjects"."id" = ? LIMIT ? [["id", 6], ["LIMIT", 1]]
(0.0ms) begin transaction
Domain Load (0.5ms) SELECT "domains".* FROM "domains" WHERE "domains"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
SQL (6.5ms) UPDATE "subjects" SET "name" = ?, "updated_at" = ? WHERE "subjects"."id" = ? [["name", "s4"], ["updated_at", "2017-07-31 08:48:57.962218"], ["id", 6]]
(7.0ms) commit transaction
Subject Load (0.0ms) SELECT "subjects".* FROM "subjects"
Completed 200 OK in 25ms (Views: 0.4ms | ActiveRecord: 15.1ms)
Edit 2 - There might be something wrong with the seed data...
domainA = Domain.create(name: :company)
s1 = domainA.subjects.create(name: :subject)
# domainA.save
d1 = s1.devices.create(name: :device)
# s1.save
p1 = d1.properties.create(name: :prop1, property_type: :double, value: 10.0)
p2 = d1.properties.create(name: :prop2, property_type: :string, value: :on)
p3 = d1.properties.create(name: :prop3, property_type: :string, value: :Lamp)
# d1.save
domainA.save
When removing has_many: :devices from the Subject model, everything
works as expected.
Going on with your models, Your Device model is flawed. You have belongs_to :device inside Device. Perhaps you should have belongs_to :subject as per the associations. This could have lead to your current problem. Try changing the Device model like so
class Device < ApplicationRecord
has_many :properties
belongs_to: :subject
end
Related
So I'm learning more about belongs_to and has_many Associations in Rails and am combining it with ActiveAdmin.
I have created a Model "Semester" and a Model "Field". A Semester has many Fields and a Field belongs to Semester.
My field Class looks like this:
class Field < ApplicationRecord
belongs_to :semester
accepts_nested_attributes_for :semester, allow_destroy: true
end
and my Semester class looks like this:
class Semester < ApplicationRecord
has_many :fields
accepts_nested_attributes_for :fields, allow_destroy: true
end
Now I registered the Models with active admin with the following two files:
ActiveAdmin.register Field do
permit_params :name, semesters_attributes: [:name]
end
and
ActiveAdmin.register Semester do
permit_params :name, :fields, fields_attributes: [ :field_id, :name]
end
And now there are two issues that come up upon proceeding that I absolutely can not ged rid off:
1) If I do not add optional: true after belongs_to :semester I will get an error message "must exist" upon trying to create a new Field with a respective Semester.
2) If I do add optional: true after belongs_to :semester I will be able to create a new Field but the "Semester" will just be "EMPTY" in the new field.
The console output of case 2) will look like this:
Started POST "/admin/fields" for 127.0.0.1 at 2018-08-17 15:23:54 +0200
Processing by Admin::FieldsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"+GPjjNPOv9GsjXnEtEjBcC0xUMHKKC+YpFLfiUFUOgsgBJ+pLCucscrN0YaTk551GFp4K5lBEI2RW1clw2vCWw==", "field"=>{"semester_id"=>"2", "name"=>"MAVT"}, "commit"=>"Create Field"}
AdminUser Load (0.1ms) SELECT "admin_users".* FROM "admin_users" WHERE "admin_users"."id" = ? ORDER BY "admin_users"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ /home/divepit/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record/log_subscriber.rb:98
Unpermitted parameter: :semester_id
(0.0ms) begin transaction
↳ /home/divepit/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record/log_subscriber.rb:98
Field Create (0.6ms) INSERT INTO "fields" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "MAVT"], ["created_at", "2018-08-17 13:23:54.026418"], ["updated_at", "2018-08-17 13:23:54.026418"]]
↳ /home/divepit/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record/log_subscriber.rb:98
(12.2ms) commit transaction
↳ /home/divepit/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record/log_subscriber.rb:98
Redirected to http://0.0.0.0:3000/admin/fields/22
Completed 302 Found in 22ms (ActiveRecord: 13.0ms)
Started GET "/admin/fields/22" for 127.0.0.1 at 2018-08-17 15:23:54 +0200
Processing by Admin::FieldsController#show as HTML
Parameters: {"id"=>"22"}
AdminUser Load (0.2ms) SELECT "admin_users".* FROM "admin_users" WHERE "admin_users"."id" = ? ORDER BY "admin_users"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ /home/divepit/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record/log_subscriber.rb:98
Field Load (0.1ms) SELECT "fields".* FROM "fields" WHERE "fields"."id" = ? LIMIT ? [["id", 22], ["LIMIT", 1]]
↳ /home/divepit/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record/log_subscriber.rb:98
Rendering /home/divepit/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activeadmin-1.3.1/app/views/active_admin/resource/show.html.arb
(0.1ms) SELECT COUNT(*) FROM "active_admin_comments" WHERE "active_admin_comments"."resource_type" = ? AND "active_admin_comments"."resource_id" = ? AND "active_admin_comments"."namespace" = ? [["resource_type", "Field"], ["resource_id", 22], ["namespace", "admin"]]
↳ /home/divepit/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record/log_subscriber.rb:98
ActiveAdmin::Comment Exists (0.1ms) SELECT 1 AS one FROM "active_admin_comments" WHERE "active_admin_comments"."resource_type" = ? AND "active_admin_comments"."resource_id" = ? AND "active_admin_comments"."namespace" = ? LIMIT ? OFFSET ? [["resource_type", "Field"], ["resource_id", 22], ["namespace", "admin"], ["LIMIT", 1], ["OFFSET", 0]]
↳ /home/divepit/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.1/lib/active_record/log_subscriber.rb:98
Rendered /home/divepit/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activeadmin-1.3.1/app/views/active_admin/resource/show.html.arb (73.4ms)
Completed 200 OK in 77ms (Views: 74.9ms | ActiveRecord: 0.5ms)
Thanks in advance for any tips on how to solve this! :)
First things first. Your associations are correct, but as per your associations you should not have field_id in semester table. Instead you should have semester_id in fields table. Also you should change semesters_attributes to semester_attributes
Unpermitted parameter: :semester_id
You should permit semester_id in the fields_attributes
fields_attributes: [ :semester_id, :name]
And finally in Rails 5, whenever a belongs_to association is defined, it is required to have the associated record present by default. To avoid this default behavior, you need to add optional: true
I have 2 simple models in a has_many relationship. A Template has_many TemplateItems. A Template has a template_type which can be one of two values ('template' or 'checklist').
For brevity I have removed non-relevant code.
template.rb
class Template < ApplicationRecord
# Relationships
belongs_to :account
has_many :template_items, -> { order('sort ASC') }, dependent: :destroy
accepts_nested_attributes_for :template_items, allow_destroy: true
# Enums
enum template_type: {template: 0, checklist: 1}
enum status: {not_started: 0, started: 1, completed: 2}
# Callbacks
before_save :set_status, unless: :is_template? # only care about status for checklists
def is_template?
return self.template_type == 'template'
end
def set_status
completed = 0
self.template_items.each do |item|
completed += 1 if item.is_completed
end
case completed
when 0
self.status = Template.statuses[:not_started]
when 1..(self.template_items.length - 1)
self.status = Template.statuses[:started]
when self.template_items.length
self.status = Template.statuses[:completed]
end
end
end
template_item.rb
class TemplateItem < ApplicationRecord
# Relationships
belongs_to :template
# Validations
validates_presence_of :template
end
When a client sends an update to Template Controller, it includes the template_items nested:
templates_controller.rb
def template_params
params.require(:template).
permit(:id, :account_id, :list_type, :name, :title, :info, :status,
template_items_attributes:
[:id, :template_id, :is_completed, :content, :item_type, :sort, :_destroy])
end
Notice that one of the attributes of an item is called sort. Notice also that the sort order is used in the Template model to sort the template_items (see the has_many line).
If a client resorts the template_items, the following update action is called:
templates_controller.rb
def update
if #template.update(template_params)
render json: #template, serializer: TemplateSerializer, status: :ok
else
render json: ErrorSerializer.serialize(#template.errors), status: :unprocessable_entity
end
end
The strange behaviour is that the database is always updated (verified in the logs and in the db) but sometimes the render does not render the new sort order but instead renders the previous sort order.
Here is the log when the action incorrectly returns the previous data:
I, [2018-02-20T20:22:55.997835 #1852] INFO -- : Processing by Api::TemplatesController#update as JSON
...parameters here...
D, [2018-02-20T20:22:56.002965 #1852] DEBUG -- : User Load (1.7ms) SELECT "users".* FROM "users" WHERE "users"."uid" = $1 LIMIT $2 [["uid", "rmcsharry+owner#gmail.com"], ["LIMIT", 1]]
D, [2018-02-20T20:22:56.115190 #1852] DEBUG -- : Template Load (2.6ms) SELECT "templates".* FROM "templates" WHERE "templates"."id" = $1 ORDER BY LOWER(templates.name) ASC LIMIT $2 [["id", "f9f6bca2-cb84-4349-8546-ca38026db407"], ["LIMIT", 1]]
D, [2018-02-20T20:22:56.121995 #1852] DEBUG -- : (0.4ms) BEGIN
D, [2018-02-20T20:22:56.129177 #1852] DEBUG -- : TemplateItem Load (2.5ms) SELECT "template_items".* FROM "template_items" WHERE "template_items"."template_id" = $1 AND "template_items"."id" IN ('419cb7ec-ca3f-4911-8a00-bec20f5ca89c', 'a7ac1687-8cb5-4199-a03b-d7cc975a0387', 'd7d885b6-2a75-487a-918c-6f3abaae7df1', 'b1b0277c-632f-4fe1-82e5-d020ee313d5b') ORDER BY sort ASC [["template_id", "f9f6bca2-cb84-4349-8546-ca38026db407"]]
D, [2018-02-20T20:22:56.137975 #1852] DEBUG -- : Account Load (1.4ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2 [["id", "c379e356-4cce-4de2-b1b4-984b773dd43e"], ["LIMIT", 1]]
D, [2018-02-20T20:22:56.144421 #1852] DEBUG -- : CACHE Template Load (0.0ms) SELECT "templates".* FROM "templates" WHERE "templates"."id" = $1 ORDER BY LOWER(templates.name) ASC LIMIT $2 [["id", "f9f6bca2-cb84-4349-8546-ca38026db407"], ["LIMIT", 1]]
D, [2018-02-20T20:22:56.148992 #1852] DEBUG -- : CACHE Template Load (0.0ms) SELECT "templates".* FROM "templates" WHERE "templates"."id" = $1 ORDER BY LOWER(templates.name) ASC LIMIT $2 [["id", "f9f6bca2-cb84-4349-8546-ca38026db407"], ["LIMIT", 1]]
D, [2018-02-20T20:22:56.156300 #1852] DEBUG -- : TemplateItem Load (2.4ms) SELECT "template_items".* FROM "template_items" WHERE "template_items"."template_id" = $1 ORDER BY sort ASC [["template_id", "f9f6bca2-cb84-4349-8546-ca38026db407"]]
D, [2018-02-20T20:22:56.171567 #1852] DEBUG -- : SQL (1.9ms) UPDATE "template_items" SET "sort" = $1, "updated_at" = $2 WHERE "template_items"."id" = $3 [["sort", 2], ["updated_at", "2018-02-20 19:22:56.167142"], ["id", "d7d885b6-2a75-487a-918c-6f3abaae7df1"]]
D, [2018-02-20T20:22:56.175072 #1852] DEBUG -- : SQL (0.7ms) UPDATE "template_items" SET "sort" = $1, "updated_at" = $2 WHERE "template_items"."id" = $3 [["sort", 1], ["updated_at", "2018-02-20 19:22:56.172797"], ["id", "a7ac1687-8cb5-4199-a03b-d7cc975a0387"]]
D, [2018-02-20T20:22:56.176305 #1852] DEBUG -- : (0.6ms) COMMIT
I, [2018-02-20T20:22:56.183481 #1852] INFO -- : Rendered TemplateSerializer with ActiveModelSerializers::Adapter::Attributes (2.97ms)
Here is the log when the action correctly returns the new data - I have marked the differences (1) and (2):
I, [2018-02-20T20:52:47.490513 #3087] INFO -- : Processing by Api::TemplatesController#update as JSON
...parameters...
D, [2018-02-20T20:52:47.499201 #3087] DEBUG -- : User Load (2.0ms) SELECT "users".* FROM "users" WHERE "users"."uid" = $1 LIMIT $2 [["uid", "rmcsharry+owner#gmail.com"], ["LIMIT", 1]]
D, [2018-02-20T20:52:47.706520 #3087] DEBUG -- : Template Load (2.3ms) SELECT "templates".* FROM "templates" WHERE "templates"."id" = $1 ORDER BY LOWER(templates.name) ASC LIMIT $2 [["id", "c965c3ed-ace2-43af-9abd-f85392bdb948"], ["LIMIT", 1]]
D, [2018-02-20T20:52:47.727668 #3087] DEBUG -- : (0.3ms) BEGIN
D, [2018-02-20T20:52:47.777126 #3087] DEBUG -- : TemplateItem Load (2.2ms) SELECT "template_items".* FROM "template_items" WHERE "template_items"."template_id" = $1 AND "template_items"."id" IN ('ff034c14-252f-4366-9b31-526b5211e92b', '4e6ec7ef-ba53-4ec2-ab2e-97dd3b2c41bc', '3628b6ca-cddb-4d65-a6c3-86dfdcaa92f4', '35e61d68-143c-4bac-ab15-fbbb2b3f13d1') ORDER BY sort ASC [["template_id", "c965c3ed-ace2-43af-9abd-f85392bdb948"]]
D, [2018-02-20T20:52:47.820226 #3087] DEBUG -- : Account Load (1.4ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2 [["id", "c379e356-4cce-4de2-b1b4-984b773dd43e"], ["LIMIT", 1]]
D, [2018-02-20T20:52:47.847928 #3087] DEBUG -- : CACHE Template Load (0.0ms) SELECT "templates".* FROM "templates" WHERE "templates"."id" = $1 ORDER BY LOWER(templates.name) ASC LIMIT $2 [["id", "c965c3ed-ace2-43af-9abd-f85392bdb948"], ["LIMIT", 1]]
D, [2018-02-20T20:52:47.850995 #3087] DEBUG -- : CACHE Template Load (0.0ms) SELECT "templates".* FROM "templates" WHERE "templates"."id" = $1 ORDER BY LOWER(templates.name) ASC LIMIT $2 [["id", "c965c3ed-ace2-43af-9abd-f85392bdb948"], ["LIMIT", 1]]
(1) D, [2018-02-20T20:52:47.856858 #3087] DEBUG -- : Template Exists (0.9ms) SELECT 1 AS one FROM "templates" WHERE "templates"."name" = $1 AND ("templates"."id" != $2) AND "templates"."account_id" = 'c379e356-4cce-4de2-b1b4-984b773dd43e' AND "templates"."template_type" = $3 LIMIT $4 [["name", "Daffy"], ["id", "c965c3ed-ace2-43af-9abd-f85392bdb948"], ["template_type", 0], ["LIMIT", 1]]
D, [2018-02-20T20:52:47.863415 #3087] DEBUG -- : SQL (1.1ms) UPDATE "template_items" SET "sort" = $1, "updated_at" = $2 WHERE "template_items"."id" = $3 [["sort", 2], ["updated_at", "2018-02-20 19:52:47.859495"], ["id", "3628b6ca-cddb-4d65-a6c3-86dfdcaa92f4"]]
D, [2018-02-20T20:52:47.865969 #3087] DEBUG -- : SQL (0.6ms) UPDATE "template_items" SET "sort" = $1, "updated_at" = $2 WHERE "template_items"."id" = $3 [["sort", 3], ["updated_at", "2018-02-20 19:52:47.864044"], ["id", "35e61d68-143c-4bac-ab15-fbbb2b3f13d1"]]
D, [2018-02-20T20:52:47.868568 #3087] DEBUG -- : (2.0ms) COMMIT
(2) D, [2018-02-20T20:52:47.918381 #3087] DEBUG -- : TemplateItem Load (1.5ms) SELECT "template_items".* FROM "template_items" WHERE "template_items"."template_id" = $1 ORDER BY sort ASC [["template_id", "c965c3ed-ace2-43af-9abd-f85392bdb948"]]
I, [2018-02-20T20:52:47.930257 #3087] INFO -- : Rendered TemplateSerializer with ActiveModelSerializers::Adapter::Attributes (17.22ms)
Notice the differences:
(1) the log shows a 'Template Exists' message
(2) after the commit Rails reloads the template_items to get the updated data from the database.
I know that I can fix this and force the update action to always do (2) and reload the template_items child objects:
templates_controller.rb
def update
if #template.update(template_params)
#template.template_items.reload
render json: #template, serializer: TemplateSerializer, status: :ok
else
render json: ErrorSerializer.serialize(#template.errors), status: :unprocessable_entity
end
end
But why do I need to do that if Rails has the ability (sometimes) to figure that out on its own? Although the cache is used in both calls, in the correct second example Rails has figured out it needs to reload the child objects after the database was updated, but not in the first case.
So what I am trying to understand is what controls this behaviour. It seems to me that it must be related to the before_save action in the Template model, since that action only fires for the 2nd case (template_type is 'template') and not the 1st (template_type is 'checklist'). In other words it seems when that action fires it 'changes' the behaviour of the update action.
So my questions are:
Why the different behaviour for the same action? If it is the
before_save, then why?
Why in the correct case does the log show Template Exists (since it
does exist in both cases)?
How does Rails know to reload the updated children in the correct case
but not in the incorrect case?
** UPDATE **
Here is the template_serializer.rb
class TemplateSerializer < ActiveModel::Serializer
attributes :id,
:account_id,
:name,
:info,
:title,
:template_type,
:status
has_many :template_items, include_nested_associations: true
end
The issue here is that you are requesting the items prior changing the sort. This means that the array of items that you have will no longer be sorted since you changed the property they are sorted on. Put another way, after you modify them, there isn't another query which returns the correct order.
So, I'll say the possible solutions are:
Reload the items after you mutate the sort.
Don't pull the items until after you mutate the sort.
Mutate the order of template_items based on sort values that changed.
The tradeoffs:
You have 2 select queries as well as the updates.
You have to update the records using TemplateItem.update(id, sort: sort) with all those updates within a transaction prior to selecting the records.
If you aren't rendering all the results, or decide not to in the future, it is possible that you will be modifying an item which will no longer be on the page. And, possibly other issues.
Why the different behaviour for the same action? If it is the before_save, then why?
The before_save is requesting template_items prior to them being saved. Otherwise, template_items doesn't get called until the serializer renders them. Note, that this means your before_save callback isn't performing the way you want it to since it is modifying the status based on the previous values.
Why in the correct case does the log show Template Exists (since it does exist in both cases)?
SELECT 1 AS one FROM "templates" WHERE
"templates"."name" = 'Daffy' AND
("templates"."id" != 'c965c3ed-ace2-43af-9abd-f85392bdb948') AND
"templates"."account_id" = 'c379e356-4cce-4de2-b1b4-984b773dd43e' AND
"templates"."template_type" = 0
LIMIT 1
Looking at the SQL, this looks like a validation to ensure name is unique across templates and type.
How does Rails know to reload the updated children in the correct case but not in the incorrect case?
Rails does not know. It is only loading them once in both cases. Just, with the before_save it is running before the records are updated.
Summary:
The easiest way to fix this timing issue would using a different callback which fires after updating the children such as after_update.
I'm allowing a user to enter IP addresses in an input field which may be of different types delimited by a comma, such as (in no particular order):
192.168.1.1,192.168.2.1-25,10.10.10.0/24,192.168.1.2
This 'string' would get saved in my DB under device.ips_to_scan.
I want to validates_format_of on these, but am finding it a little difficult to write a regex that seems to work in rails, while it does work on regex101 (https://regex101.com/r/nf2bnM/1):
validates_format_of :ips_scan, with: /\A([0-9]{1,3}\.){3}[0-9]{1,3}(\/([1-2][0-9]|[0-9]|3[0-2]))?(-([0-9]{1,3}))?,?\Z/i, on: :update
This one is expected to fail:
Started PUT "/devices/2" for 127.0.0.1 at 2018-02-19 22:03:15 -0500
Processing by DevicesController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"EQCFG6/xoJHtP6Nd3oqaYRW6mypfEoCMrnio1yj6loP+KtvjgLZ9Gmhb0oTwCjD0RGH+qQuctZFVIvF5HBJcGw==", "device"=>{"ips_scan"=>"192.168.1.1,192.168.2.1-25,a.b.c.d", "ips_exclude"=>"10.10.10.1"}, "commit"=>"Save", "id"=>"2"}
User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 2], ["LIMIT", 1]]
Device Load (1.6ms) SELECT "devices".* FROM "devices" WHERE "devices"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
(0.5ms) BEGIN
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
(0.5ms) ROLLBACK
Redirected to http://localhost:3000/devices/2/edit
Completed 302 Found in 47ms (ActiveRecord: 12.1ms)
...But this one should have worked:
Processing by DevicesController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"JJfmT/0l5MEDc+gUH/WHHp3bbgyzjGa0xTzaXM3E/WHLvbi30mI5SoYXmc0xdS2LzAALj+cCU6k+ZoPy+Sw3+Q==", "device"=>{"ips_scan"=>"192.168.1.1,192.168.2.1-25,192.168.1.2", "ips_exclude"=>"10.10.10.1"}, "commit"=>"Save", "id"=>"2"}
User Load (0.9ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 2], ["LIMIT", 1]]
Device Load (0.7ms) SELECT "devices".* FROM "devices" WHERE "devices"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
(0.6ms) BEGIN
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
(0.6ms) ROLLBACK
Redirected to http://localhost:3000/devices/2/edit
Completed 302 Found in 17ms (ActiveRecord: 3.5ms)
Last thing I can think of, is that I do have strong parameters, but I'm permitting ips_scan, so that this shouldn't be an issue:
def update
if #device.update(device_params)
flash[:notice] = 'Successful update'
respond_with :edit, :device
else
flash[:warning] = 'Unable to update'
respond_with :edit, :device
end
end
private def device_params
params.require(:device).permit(:token, :ips_scan, :ips_exclude)
end
I'm hoping you rubyist's out there have a eloquent solution. The first thought that comes to mind is that I have to split the string, and check each element sequentially to ensure it matches instead.
While I'm still open to a nice eloquent one-liner within the Model itself, I was able to get this working through creating a concern:
models/concerns/ip_validator.rb
class IpValidator < ActiveModel::Validator
def validate(record)
ips = record.ips_scan.split(',')
ips.each do |ip|
/([0-9]{1,3}\.){3}[0-9]{1,3}(\/([1-2][0-9]|[0-9]|3[0-2]))?(-([0-9]{1,3}))?/ =~ ip
record.errors.add(:ips_scan, ' is not valid') unless $LAST_MATCH_INFO
end
end
end
The call in my model now looks like:
validates :ips_scan, :ips_exclude, ip: true, on: :update
You can use this method in your custom validator to check an IP address
require 'ipaddr'
def valid_ip_addr?(ip_addr)
IPAddr.new(ip_addr)
true
rescue IPAddr::InvalidAddressError => _error
false
end
I have 3 models, Portfolio, Stock and Transaction. When I'm in a portfolio show page I iterate through the portfolio's transactions and plot the stocks to show what I've bought.
When I load a portfolio show page the logs look something like this:
Started GET "/portfolios/1" for ::1 at 2016-09-24 02:15:32 -0400
Processing by PortfoliosController#show as HTML
Parameters: {"id"=>"1"}
Portfolio Load (0.6ms) SELECT "portfolios".* FROM "portfolios"
WHERE "portfolios"."id" = $1 LIMIT 1 [["id", 1]]
Transaction Load (2.0ms) SELECT "transactions".* FROM "transactions"
WHERE "transactions"."portfolio_id" = $1 [["portfolio_id", 1]]
Stock Load (1.7ms) SELECT "stocks".* FROM "stocks" WHERE "stocks"."id" = $1 LIMIT 1 [["id", 1]]
Stock Load (0.2ms) SELECT "stocks".* FROM "stocks" WHERE "stocks"."id" = $1 LIMIT 1 [["id", 6]]
Stock Load (0.2ms) SELECT "stocks".* FROM "stocks" WHERE "stocks"."id" = $1 LIMIT 1 [["id", 4]]
CACHE (0.0ms) SELECT "stocks".* FROM "stocks" WHERE "stocks"."id" = $1 LIMIT 1 [["id", 6]]
Stock Load (0.2ms) SELECT "stocks".* FROM "stocks" WHERE "stocks"."id" = $1 LIMIT 1 [["id", 2]]
Stock Load (0.2ms) SELECT "stocks".* FROM "stocks" WHERE "stocks"."id" = $1 LIMIT 1 [["id", 13]]
Stock Load (0.2ms) SELECT "stocks".* FROM "stocks" WHERE "stocks"."id" = $1 LIMIT 1 [["id", 9]]
Stock Load (0.2ms) SELECT "stocks".* FROM "stocks" WHERE "stocks"."id" = $1 LIMIT 1 [["id", 16]]
Stock Load (0.3ms) SELECT "stocks".* FROM "stocks"
Rendered transactions/_form.html.erb (19.1ms)
Rendered portfolios/show.html.erb within layouts/application (69.6ms)
Rendered application/_nav.html.erb (0.4ms)
Completed 200 OK in 128ms (Views: 90.5ms | ActiveRecord: 9.2ms)
My transaction, stock and portfolio models have these associations:
class Transaction < ActiveRecord::Base
belongs_to :portfolio
belongs_to :stock
end
class Stock < ActiveRecord::Base
has_many :transactions
end
class Portfolio < ActiveRecord::Base
belongs_to :user
has_many :transactions
has_many :stocks, through: :transactions
end
Within the show page, I loop through an instance portfolio and it's transactions using .each to grab the associated stock.
<% #portfolio.transactions.each do |trade| %>
In the portfolio controller the #portfolio is defined as #portfolio = Portfolio.find(params[:id]).
Is there a more efficient way to query the database (postgres) to retrieve the associated stock objects. Right now it's not a problem but I'm thinking of when people have a portfolio for a number of years and have hundreds of trades within the portfolio. Or should I be thinking of caching the results of a particular portfolio page using memcache or redis?
To avoid these queries you need to preload associations.
Something like this
#portfolio = Portfolio.preload(:stocks).find(params[:id])
This looks like an N + 1 queries problem, you can use eager_load or preload or includes to get rid of it:
#portfolio = Portfolio.eager_load(:stocks).find(params[:id])
I re-tooled my #portfolio object to #portfolio = Portfolio.includes(:transactions, :stocks).find(params[:id]).
Now I get all the stocks in one batch.
Parameters: {"id"=>"1"}
Portfolio Load (0.3ms) SELECT "portfolios".* FROM "portfolios"
WHERE "portfolios"."id" = $1 LIMIT 1 [["id", 1]]
Transaction Load (0.5ms) SELECT "transactions".* FROM "transactions"
WHERE "transactions"."portfolio_id" IN (1)
Stock Load (0.4ms) SELECT "stocks".* FROM "stocks" WHERE
"stocks"."id" IN (1, 6, 4, 2, 13, 9, 16)
Stock Load (0.3ms) SELECT "stocks".* FROM "stocks"
This is what I was trying to get. Since I'm using Rails 5 I'm not sure if eager_load or preload method are degraded. Includes seems like a more stable choice.
From last two days i try expect whats going on in my app. This is my problem:
My model:
class User
has_many :orders
---
class Order
belongs_to :user
has_and_belongs_to_many :contributors, class_name: 'User'
Situation:
User can create order, next for this order can add contributors(other users) with
<%= collection_check_boxes(:order, :contributor_ids, #prac, :id, :to_s) %>
After that in my custom action in OrdersController i have something like this:
#order.contributors.each do |u|
u.orders << #order
end
Here i want add this order to list of contributors(users) orders. Unfortunately this work only for last contributor(user). Below log from my console:
Started PATCH "/orders/282/zatwierdz" for 127.0.0.1 at 2014-07-01 08:48:59 +0200
Processing by OrdersController#zatwierdz as HTML
Parameters: {"authenticity_token"=>"hxWUnazKLoS9t8bVmOAMeIxdo/KYuO33bUeuQZYgeV
k=", "id"=>"282"}
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 32 OR
DER BY "users"."id" ASC LIMIT 1
Role Load (0.0ms) SELECT "roles".* FROM "roles" WHERE "roles"."id" = ? LIMI
T 1 [["id", 4]]
Order Load (0.0ms) SELECT "orders".* FROM "orders" WHERE "orders"."id" = ?
ORDER BY created_at DESC LIMIT 1 [["id", 282]]
OrderItem Exists (0.0ms) SELECT 1 AS one FROM "order_items" WHERE "order_it
ems"."order_id" = ? LIMIT 1 [["order_id", 282]]
(0.0ms) begin transaction
SQL (0.0ms) UPDATE "orders" SET "status" = ?, "updated_at" = ? WHERE "orders"
."id" = 282 [["status", 1], ["updated_at", "2014-07-01 06:48:59.486472"]]
(62.5ms) commit transaction
User Load (0.0ms) SELECT "users".* FROM "users" INNER JOIN "orders_users" ON
"users"."id" = "orders_users"."user_id" WHERE "orders_users"."order_id" = ? [["
order_id", 282]]
(0.0ms) begin transaction
(0.0ms) commit transaction
(0.0ms) begin transaction
SQL (0.0ms) UPDATE "orders" SET "updated_at" = ?, "user_id" = ? WHERE "orders
"."id" = 282 [["updated_at", "2014-07-01 06:48:59.564595"], ["user_id", 33]]
(78.1ms) commit transaction
(0.0ms) begin transaction
SQL (0.0ms) UPDATE "orders" SET "updated_at" = ?, "user_id" = ? WHERE "orders
"."id" = 282 [["updated_at", "2014-07-01 06:48:59.642719"], ["user_id", 35]]
(78.1ms) commit transaction
(0.0ms) begin transaction
SQL (0.0ms) UPDATE "orders" SET "updated_at" = ?, "user_id" = ? WHERE "orders
"."id" = 282 [["updated_at", "2014-07-01 06:48:59.720842"], ["user_id", 36]]
(109.4ms) commit transaction
(0.0ms) begin transaction
SQL (0.0ms) UPDATE "orders" SET "updated_at" = ?, "user_id" = ? WHERE "orders
"."id" = 282 [["updated_at", "2014-07-01 06:48:59.830215"], ["user_id", 37]]
(78.1ms) commit transaction
Redirected to http://localhost:3000/orders/282
Completed 302 Found in 437ms (ActiveRecord: 406.2ms)
I try use this action with callbacks and from model methods but nothing happened. Maybe better for this aprouch is to use other associations? Anny sugestions welcome.
Thx for help.
By doing:
#order.contributors.each do |u|
u.orders << #order
end
You are adding your order to the user.orders relationship, which you define as a many (Order) to one (User). The "has_many" orders on the User class is just the other side of the "belongs_to" user on the Order class. So yes, as per your model, an Order has one and only one user.
Now, if you want to be able to navigate your has_and_belongs_to_many relation from the User side, you need to define it there too.