To be more specific, "How do I validate that a model requires at least x valid associated models to be created?". I've been trying to validate nested models that get created in the same form as the parent (and ultimately show immediate validations a la jQuery). As a popular example, lets assume the following models and schema.
class Project
include DataMapper::Resource
property :id, Serial
property :title, String, :nullable => false
has 2..n, :tasks
end
class Task
include DataMapper::Resource
property :id, Serial
property :project_id, Integer, :key => true
property :title, String, :nullable => false
belongs_to :project
end
All the validations are done in the schema definitions, as you can see. The important one here is "has 2..n, :tasks". This validation actually works normally, given that the nested task attributes in the params hash will produce valid tasks. If they produce an invalid task, however, then the task won't get created and you'll end up with a Project that has less than 2 tasks, and thus an invalid project object.
As I understand it, this is because it can't figure out if the task attributes are valid or not until it attempts to save the tasks, and since - as far as I know - the tasks can't get saved before the project, the project is unaware if the tasks will be valid or not. Am I correct in assuming this?
Anyway, I was hoping there would be a quick answer, but it seems a lot less trivial then I'd hoped. If you've got any suggestions at all, that would be greatly appreciated.
I actually found a nice solution here using transactions in DataMapper. Basically this transaction tries to save the parent object as well as all the child objects. As soon as one fails to save, then the transaction stops and nothing gets created. If all goes well, then the objects will save successfully.
class Project
def make
transaction do |trans|
trans.rollback unless save
tasks.each do |task|
unless task.save
trans.rollback
break
end
end
end
end
end
This assures that everything is valid before it anything gets saved. I just needed to change my #save and #update methods to #make in my controller code.
SET CONSTRAINTS DEFERRED might be useful if your database engine supports that.
Otherwise, maybe write a stored procedure to do the inserts, and then say that its the resonsibility of the stored procedure to ensure that only correct, validated data is inserted.
There is a model method valid? that runs the validations on a model object before it is saved. So, the simple way to validate the associations would be to use validates_with_block' or 'validates_with_method to check the validations on the associations.
It would look something like this
validates_with_block do
if #tasks.all?{|t|t.valid?}
true
else
[false, "you have an invalid task"]
end
end
Or you could look at dm-association-validator or dm-accepts-nested-attributes
Edit: for extra crazy. run validations on the tasks, then check to see if the only errors are ones related to the association.
validates_with_block do
if #tasks.all?{|t|t.valid?;!t.errors.any?{|e|e[0]==:project}}
true
else
[false, "you have an invalid task"]
end
end
Related
I'm using Rails 5. I have this model
class MyObject < ActiveRecord::Base
...
belongs_to :distance_unit
and I notice when I have a line like below
distance = Distance.new({:distance => my_obj.distance, :distance_unit => my_obj.distance_unit})
it causes the following to be executed
SELECT "distance_units".* FROM "distance_units" WHERE "distance_units"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Nothing unusual, but I have a cached method created in my DistanceUnit model
class DistanceUnit < ActiveRecord::Base
def self.cached_find_by_id(id)
Rails.cache.fetch("distanceunit-#{id}") do
puts "looking for id: #{id}"
find_by_id(id)
end
end
and I would like the "distance = Distance.new({:distance => my_obj.distance, :distance_unit => my_obj.distance_unit})" line to invoke my cached functionality instead of running off to the database. How can I achieve this?
Regarding to my comment, note that the memory_store is only used within a process, so each process will have its own store (consider MemCacheStore if that is an issue). Also the cache is gone when the process ends.
As a general note: The belongs_to association inherently triggers a lookup if the object hasn't been fetched yet. There are plenty of caches built in the Rails framework and in general I wouldn't worry too much about premature optimization in the beginning (and only optimize when you find queries are running too slow).
Also: Accessing the cache is also a call to a type of database, and an lookup based on an probably indexed id typically isn't really a heavy call. Also if there's just a few distance units, you maybe simply using a constant could work?
But to answer your question. There doesn't seem to be anything in your code that says cached_find_by_id (and while rails does a lot of things automagically, this isn't one of them).
You could create the following method in MyObject that overrides the 'getter' that is created by the belongs_to association:
def distance_unit
DistanceUnit.cached_find_by_id(distance_unit_id)
end
However, if you're simply initializing an ActiveRecord object and you don't need the DistanceUnit in this call you could also pass the id directly, since that is what is stored in the database.
Distance.new({:distance => my_obj.distance, :distance_unit_id => my_obj.distance_unit_id})
I am using Sequel and I have model defined like this:
class A < Sequel::Model
one_to_one :lang, class: ALang, key: :a_id,
graph_join_type: :inner do |ds|
ds.where(ALang__lang: I18n.locale.to_s)
end
delegate :title, :titleSanitized, :description, to: :lang
# ...
end
I18n.lang = :de
A.eager(:lang).all
# block is called ("ds.where(ALang__lang: I18n.locale.to_s)" code)
# database was queried (I can see the query in logs)
I18n.lang = :en
A.eager(:lang).all
# block is not called
# database was queried (I can see the query in logs)
Is it bug or feature? Or am I doing something wrong?
Thank you
In this case, the block is eagerly evaluated and the resulting dataset is cached. To delay the evaluation of the current locale, you need to use a delayed evaluation:
one_to_one :lang, class: ALang, key: :a_id,
graph_join_type: :inner do |ds|
ds.where(ALang__lang: Sequel.delay{I18n.locale.to_s})
end
I've updated Sequel's documentation to reflect this.
Assuming when you're saying "block" you're meaning the body of the A class or something within said body, that makes perfect sense. Classes are only loaded once (typically, unless monkey patching, but even then "loading" is a debatable term).
The body of A, in this case, sets up the declarative logic of of the queries you are performing. If you're talking abut the block passed to one_to_one it's likely Sequel::Model is calculating its result and caching it when the class is being loaded.
Am I missing the question here?
That is a feature:
The block is only evaluated one, when the class is loaded. This is the reason why you use lambdas in ActiveRecord to define variable parts in scopes or associations. I don't know if Sequel also supports lambdas in query or association definitions.
The database is not called twice, because associations are cached after being retrieved. See Caching in the docs for Sequel::Model
Let's say there is a post model like this:
class Post < ActiveRecord::Base
.......
.......
end
It has two attributes :title and :body.
Now a Post object can go through multiple stages: 'draft' -> 'published'.
Now while saving a post in drafts mode, the :title isn't required. But while saving it in published mode, it needs to have a presence validation on the title:
validates_presence_of :title
Now, what is the best way to do this in Rails? I think some implementation of a decorator pattern would be great, wherein in a controller, I would dynamically add validations to an active record object.
This is a simplified version of a bigger problem I have. In the actual case, there are a lot more validations including those done on associated objects.
If you are using state_machine for your state transitions, it supports just what you are looking for, with examples in the readme file.
Otherwise, all rails validations have optional if paramter. If, for example, your post has a published? method that returns whether it's in a published state, you could write validates_presence_of :title, if: :published? and have it do exactly what you need.
I have two models that are associated via a has_many relationship. E.g.
class Newspaper < ActiveRecord::Base
has_many :articles
end
class Article < ActiveRecord::Base
belongs_to :newspaper
validates :uid, presence: true,
uniqueness: { case_sensitive: true }
end
A newspaper is updated several times a day but we only want to construct and add articles to the association that do not already exist. The following code was my first cut of achieving this.
new_articles.each do |article|
unless newspaper.articles.exists? uid: article.uid
newspaper.articles.build(uid: article.uid)
end
end
The newspaper object is either new and unsaved, or retrieved with existing relationships at this point.
My tests indicate that I am able to add two articles to the newspaper that have the same UID using the code above and this is obviously not want I want.
I appears to me that my current code will result in a validation failure upon being saved as the validation looks at uniqueness across the entire articles table and not the association.
What I'm struggling to understand is how the exists? method behaves in this scenario (and why it's not saving my bacon as I planned). I'm using FactoryGirl to build a newspaper, add an article and then simulate an update containing an article with the same uid as the article I've already added. If the code works I should get only one associated article but instead I get two. Using either build or create makes no difference, thus whether the article record is already present in the database does not appear to change the outcome.
Can anyone shed some light on how I can achieve the desired result or why the exists? method is not doing what I expect?
Thanks
The association exists? actually creates a scoped query, as per the association. This is why your existing articles filter doesn't work.
unless newspaper.articles.exists? uid: article.uid
# `articles.exists?` here will produce this if the newspaper is new
# ... WHERE "articles"."newspaper_id" IS NULL AND "articles.uid" = '<your uid>'
# and this, if the newspaper is persisted (with an id of 1)
# ... WHERE "articles"."newspaper_id" = 1 AND "articles.uid" = '<your uid>'
The case of the new newspaper is clearly wrong, as it would only return articles with a nil newspaper ID. But the persisted case is probably undesirable as well, as it still unnecessarily filters against newspaper ID, when you real concern here is that the UID is unique.
Rather, you probably want simply against Article, rather than scoping the exists? through the association, like:
unless Article.exists? uid: article.uid
Concerning your other problem:
this appears to be a FactoryGirl problem where the create method isn't creating db entries in the same way I can in the irb.
FactoryGirl.create should still abide by validations. It might help to see your test.
so I'm setting up some models and they are based off of 2 abstract base classes (or rather they used to be classes). After running into a lot of trouble with Datamapper's handling of STI for my use case, which appears to be an open bug on their lighthouse page, I decided instead to just do modules to define all the properties to keep my models DRY. Unfortunately, I'm having a scoping issue, and what complicates matters worse is that I have to use 2 levels of inheritance. Here's my code:
module Part
def self.included(receiver)
receiver.class_eval do
include DataMapper::Resource
property :id, Serial
#other junk
end
end
end
module HardDrive
def self.included(receiver)
receiver.class_eval do
include Part
property :kind, Enum[:magnetic, :flash]
#buncha crap here
end
end
end
class Fujitsu
include HardDrive
property :rev, String
end
The error I get is:
uninitialized constant HardDrive::Enum (NameError)
from /usr/lib/ruby/gems/1.8/gems/activesupport-2.3.4/lib/active_support/dependencies.rb:80:in `const_missing'
from ./app/models/hard_drive.rb:6:in `included'
from ./app/models/hard_drive.rb:4:in `class_eval'
from ./app/models/hard_drive.rb:4:in `included'
from ./app/models/hard_drives/fujitsu.rb:2:in `include'
from ./app/models/hard_drives/fujitsu.rb:2
I'm at a loss here. Anyone know of how I could solve this or better yet, a smarter way I could do this?
It seems to me that Enum is defined under the DataMapper modules and the HardDrive scope does not resolve it. (Want to know why ?)
Just put DataMapper::Enum instead of Enum and it should work.
In a more general discussion, are you sure you really need these abstractions ? One drawback I see in your code is that you won't be able to query your database for parts and harddrives because the logic is stored in ruby modules instead of in the database.
Update (after comment from author)
The general answer is: forget about STI. While ORM are nice to have, the best part of them is SQL backend abstraction. While they give you the impression that you can have a persisten object model, the abstractions often leak and STI is a good example. I won't go in large details here but you can find resources online. Best is that you stay close enough to SQL modelling best practices, like one-one, one-many and many-many relationsships.
Here is an updated version. I didn't test it and the method names are probably wrong, but you will get the idea:
class Part
property :serial_number
has_one Manufacturer
end
class HardDisk
property :technology
property :platters
property :model
#...
is_one Part
end
class Manufacturer
property :name #Fujitsu, ...
property :website
#...
has_many HardDisk, [:trough=>Part]
end