Say you have a Factory in FactoryBot, and say that factory has several traits for associations, and that each trait will need to update the model being created by the factory. So, your main model is MainModel, and you have associations to A, B, C, D, etc...
FactoryBot.define do
factory :main_model
trait :with_a do
after(:create) do |main_model|
# Both sides refer to each other, so this must be done in after(:create).
main_model.update(a_id: create(:a, main_model_id: main_model.id).id)
end
end
trait :with_b do
after(:create) do |main_model|
# Both sides refer to each other, so this must be done in after(:create).
main_model.update(b_id: create(:b, main_model_id: main_model.id).id)
end
end
# More traits omitted...
end
Side note: We're not using ActiveRecord, so we cannot use the associations features built into FactoryBot. Associations must be explicitly registered using a call to update on both sides of the association. Even if I could, I'm not sure it would have any effect?
Now, if client code does the following:
create(:main_model, :with_a, :with_b, :with_c, :with_d)
This is going to result in the following writes to the db:
Create the main record
Create the A record that points to the main.
Update the main record to point to the new A.
Create the B record that points to the main.
Update the main record to point to the new B.
Create the C record that points to the main.
Update the main record to point to the new C.
Create the D record that points to the main.
Update the main record to point to the new D.
Of the above, Numbers 3, 5, 7, and 9 could all be done at the same time, at the very end, if I could simply build up a list of updates that are needed and find a "hook" that would allow me to do one more update after the final trait was applied, and factory bot was ready to return the final result to me. I cannot figure out a way to do that, but the ability to do this would save thousands (probably tens of thousands) of DB writes across my spec suite.
Anyone have any ideas?
You could define a transient variable (I used associations, but call it whatever) - and use this to store all of the associated record IDs for a single record update in an after(:create) block.
Something like this:
FactoryBot.define do
factory :main_model do
transient do
associations { {} }
end
trait :with_a do
associations { super.merge(a_id: create(:a, main_model_id: id).id) }
end
trait :with_a do
associations { super.merge(b_id: create(:b, main_model_id: id).id) }
end
after(:create) do |main_model|
main_model.update(associations)
end
end
end
Usage of the factory remains unchanged - e.g.
create(:main_model, :with_a, :with_b)
...So you shouldn't need to update any of the spec code.
Related
I am working with python and sqlalchemy. I have one table named Team and another named Game. The Game table has columns "away_id" and "home_id" and the Team table has the column "team_id". I just made this hybrid_method for the Team class which will return all game instances where the away_id, or home_id matches the team_id. The argument I pass it, s, is a session instance. How can I write this code as a #hybrid_property where I don't have to pass it a session instance?
#hybrid_method
def games(self, s):
return s.query(Game).filter(or_(Game.away_id==self.team_id, Game.home_id==self.team_id)).all()
First off, from what I can see here this is not a use case for "#hybrid" hybrid is used for specifically the use case where you'd like to say: "MyClass.games == something", at the class level, as well as, "my_object.games == something", at the instance level. That is not the case here, you're trying to run a query in its entirety, passing a specific self.team_id into it - so you need a self, so this is just a regular method or descriptor.
So just use #property with object_session() as the docs say right here:
class MyClass(Base):
# ...
#property
return object_session(self).query(Game).filter(...)
I have a BankAccount class. I was trying to create multiple instances of this class and put them into an array. For example
accounts = [Ba1 = BankAccount.new(100), Ba2 = BankAccount.new(100)]
I want to initialize the array with a large number of instances inside, let's say 20, so from Ba1 to Ba20. Is there an easier way to do it instead of just manually inputting it? I have tried a loop but I just can't figure out how to make it work.
This should do the trick:
accounts = 100.times.collect { BankAccount.new(100) }
If you need to do something different for each account based on which one it is then:
accounts = 100.times.collect { |i| BankAccount.new(i) }
i represents each number in the collection being iterated over.
If you actually need to set the variable names using the data you can call eval().
accounts = 100.times.collect { |i| eval("B#{i} = BankAccount.new(100)") }
And now B1 through B100 should be set to the appropriate BankAccount instances.
Disclaimer:
I should say that this approach will be generally frowned upon. In this case you already have an array called accounts. All you need to do is index on it to get the corresponding bank account. accounts[50] for example instead of Ba50. In my years of ruby development I've found few places to use eval that made sense.
I'd like to know if I can add something similar to this in my class and have it build a class attribute that I can reference in other classes. I don't want to have to remember id's and I don't want to keep having to update id's as the id's in the weight tables change. Nor do I want to lock the weight table into a set of specific id's.
So I'd love to do something like the following:
class Weight < ActiveRecord::Base
attr_accessible :name
##kind = {
Weight.all.each do |weight|
weight.name.delete(' ').underscore.to_sym: weight.id,
end
}
cattr_reader :kind, :instance_accessor => false
end
Then in other areas of the code I can do things like..
scope :light, where(weight_id, Weight::kind(:light))
I'm sure there's some magic ruby way, I'm just not sure of the right syntax.
This is the closest I've come so far.
def self.kinds
kinds = Weight.all.map { |weight| { weight.name.delete(' ').underscore.to_sym => weight.id } }
kinds.reduce({}, :update)
end
and then...
scope :light, where(weight_id, Weight.kinds[:light])
Why not turn kind accessor into a class method which would lazily load the weights from database and lookup the neccessary one?
However, the stuff you are trying to do doesn't seem really good. What is the table behind Weight changes? What your classes will be loaded prior to the database connection gets set up (in some test environment, for instance)? I would suggest rewriting the scope to inner joing weight model with the appropriate name...
I would like to
1. Grab an object 1 and object 2 from the database (both have 2 has_many associations each)
2. Create object 3 which is a clone of object 1 and therefore has no associations yet
2. Duplicate object 2's associations and add them to object 3's associations
3. Use the new object 3 for operations in memory
4. Exit the process with no permanent change to the database - object 1 and object 2 still have their original associations when the process exits
What instead is happening
I grab an object 1 and object 2 from the database (both have 2 has_many associations each)
I create object 3 which is a clone of object 1 and therefore has no associations yet
I duplicate object 2's associations and add them to object 3's associations
I use the new object 3 for operations in memory
I exit the process HOWEVER there is now a permanent change to the database - object 2 no longer has its associations because their keys have been changed to the (Temporary) object 3 id.
Here is my code. Note that all contains an array with an arbitrary # of objects in it
object1 = all.last.clone #we take the most recently created object
all.each do |instance|
instance_association1 = (instance.association1).dup
object1.association1 += instance_association1 #BUG this moves the association
object1.association1.uniq!
instance_association2 = (instance.association2).dup
object1.association2 += instance_association2
object1.association2.uniq!
end
Note that now when I say:
all.last.association1, I get an empty array.
Help!!
MongoMapper's associations may be a little overzealous with saving. I want to overhaul it sometime, but it's not an easy problem.
The code that's going to tell you when saves happen is many_documents_proxy.rb. When you do my_association = [...], replace is the method that's called.
The only method that doesn't do any saving is build, so you may be able to build up your temporary object like so:
tmp = all.last.clone
all.each do |instance|
instance.association1.each { |doc| tmp.association1.build(doc.attributes) }
tmp.association1.uniq!
# ...
end
More generally, you can convert all your associated documents to arrays and not worry about saving...
associations1 = []
associations2 = []
all.each do |doc|
associations1 += doc.associations1.to_a
associations2 += doc.associations2.to_a
end
assocations1.uniq!
assocations2.uniq!
HOWEVER, watch out! Ruby's uniq method looks like it uses Ruby's #hash method to compute equality, which may not give you the results you want in this case. Do a few tests to make sure that my_obj.hash == my_obj2.hash if my_obj == my_obj2. See this discussion for strategies on implementing your own #hash method, if that's the route you need to go.
I've been practicing some Ruby meta-programming recently, and was wondering about assigning anonymous classes to constants.
In Ruby, it is possible to create an anonymous class as follows:
anonymous_class = Class.new # => #<Class:0x007f9c5afb21d0>
New instances of this class can be created:
an_instance = anonymous_class.new # => #<#<Class:0x007f9c5afb21d0>:0x007f9c5afb0330>
Now, when the anonymous class is assigned to a constant, the class now has a proper name:
Foo = anonymous_class # => Foo
And the previously created instance is now also an instance of that class:
an_instance # => #<Foo:0x007f9c5afb0330>
My question: Is there a hook method for the moment when an anonymous class is assigned to a constant?
There are many hooks methods in Ruby, but I couldn't find this one.
Let's take a look at how constant assignment works internally. The code that follows is extracted from a source tarball of ruby-1.9.3-p0. First we look at the definition of the VM instruction setconstant (which is used to assign constants):
# /insns.def, line 239
DEFINE_INSN
setconstant
(ID id)
(VALUE val, VALUE cbase)
()
{
vm_check_if_namespace(cbase);
rb_const_set(cbase, id, val);
INC_VM_STATE_VERSION();
}
No chance to place a hook in vm_check_if_namespace or INC_VM_STATE_VERSION here. So we look at rb_const_set (variable.c:1886), the function that is called everytime a constant is assigned:
# /variable.c, line 1886
void
rb_const_set(VALUE klass, ID id, VALUE val)
{
rb_const_entry_t *ce;
VALUE visibility = CONST_PUBLIC;
# ...
check_before_mod_set(klass, id, val, "constant");
if (!RCLASS_CONST_TBL(klass)) {
RCLASS_CONST_TBL(klass) = st_init_numtable();
}
else {
# [snip], won't be called on first assignment
}
rb_vm_change_state();
ce = ALLOC(rb_const_entry_t);
ce->flag = (rb_const_flag_t)visibility;
ce->value = val;
st_insert(RCLASS_CONST_TBL(klass), (st_data_t)id, (st_data_t)ce);
}
I removed all the code that was not even called the first time a constant was assigned inside a module. I then looked into all the functions called by this one and didn't find a single point where we could place a hook from Ruby code. This means the hard truth is, unless I missed something, that there is no way to hook a constant assignment (at least in MRI).
Update
To clarify: The anonymous class does not magically get a new name as soon as it is assigned (as noted correctly in Andrew's answer). Rather, the constant name along with the object ID of the class is stored in Ruby's internal constant lookup table. If, after that, the name of the class is requested, it can now be resolved to a proper name (and not just Class:0xXXXXXXXX...).
So the best you can do to react to this assignment is to check the name of the class in a loop of a background worker thread until it is non-nil (which is a huge waste of resources, IMHO).
Anonymous classes don't actually get their name when they're assigned to a constant. They actually get it when they're next asked what their name is.
I'll try to find a reference for this. Edit: Can't find one, sorry.