Eager loading database table using ActiveRecord - mysql

I have a Ruby 2.2.2 app that uses ActiveRecord as the ORM (similar to Rails). I have two tables, "users" and "accounts", where "accounts" have belongs_to :user. For a given user, I simply want to eager load the contents of the "accounts" table into a Ruby object (named user_accounts) so I can perform operations like:
user_accounts.find_by_product_name("nameofproduct")
...without the find_by_product_name method performing a SQL query. I simply want to preload all entries from the "accounts" table (that belong to a given user) into a Ruby object so I can minimize the number of SQL queries performed.
No matter how much documentation I read, I cannot figure out how to do this properly. Is this possible? If so, how?

If you don't want the ORM to re-query the database, then I think you are better of using the select method added by the Enumerable mixin. Because if you try to use the find_by_* methods, I think it will always send another query.
Here is an example of how it could be achieved.
# The accounts association will be loaded and cached the first time
#user.accounts.select { |account| account.name == "nameofproduct" }
# The next time, it is already loaded and will select from the previously loaded records
#user.accounts.select { |account| account.name == "nameofanotherproduct" }

I would store that accounts in a hash instead of an array, because lookups in a hash are much faster (in O(1)) than in an array which only allows O(n).
group_by helps you building that hash.
# assuming that #user.accounts returns all accounts
user_accounts = #user.accounts.group_by(&:name)
# querying the hash. use `first` to receive the first matching
# account (even if there is only one)
user_accounts['nameofproduct'].first

Related

Database Access Objects batchInsert() yii2 run another function after each record is inserted

I am using batchInsert() function of Yii2 Database Access Objects. I need to run another function like sending email from PHP code after each record is inserted.What is the workaround to achieve this? Or is it possible to get all AUTO_INCREMENT ids of rows inserted?
The code is
Yii::$app->db->createCommand()->batchInsert(Address::tableName(),
['form_id','address'], $rows)->execute();
I am using batchInsert() documented in https://www.yiiframework.com/doc/api/2.0/yii-db-command#batchInsert()-detail
First, you're not using ActiveRecord. More about ActiveRecord you can find in the documentation: API and guide. Yii::$app->db->createCommand() is a DAO which is much simpler than ActiveRecord and does not support events in the same way.
Second, there is no batchInsert() for ActiveRecord and there is a good reason for that - it is hard to detect IDs of inserted records when you're inserting them in batch (at least in a reliable and DB-independent way). More about this you can read at GitHub.
However if you know IDs of records or some unique fields before insert (for example user in addition to the numeric ID, it also has a unique login and/or email), you may just fetch them after insert and run event manually:
$models = Address::findAll(['email' => $emailsArray]);
foreach ($models as $model) {
$model->trigger('myEvent');
}
But unless you're inserting millions of records, you should probably stick to simple foreach and $model->save(), for the sake of simplicity and reliability.
There is also a yii2-collection official extension. This is still more like a draft and POC, but it may be interesting in the future.

Eager loading individual entry from User table and associated belongs_to records using ActiveRecord

Is it possible to eager load a specific entry from the user table, as well as all associated tables attached via belong_to.
For example, I have users table with accounts and patients tables, that both belong_to :user. If I wanted to grab a specific User table entry and eager_load the associated tables, akin to something like this:
user = User.find_by_email("testemail#here.com").eager_load(:accounts, :patients)
How can I do it ?
You were close. Try putting the associations in an array within the eager_load call as shown below:
user = User.includes([:accounts, :patients]).find_by(email: "testemail#here.com")
Now that you have included the associated tables and then found your user you can call any attributes from those other tables on the user without firing another query from the database. For example once that is run you can do:
all_user_accounts = user.accounts
This will not fire a query in the database but instead be loaded from memory.
If you use #eager_load you are doing one query whereas includes will do it in one or two depending on what it deems necessary. So what is #includes for? It decides for you which way it is going to be. You let Rails handle that decision.
Check this guide for great information on includes/eager_load/preload in rails:
http://blog.arkency.com/2013/12/rails4-preloading/

How to remove DB record from eager-loaded table array using ActiveRecord and Ruby

We have a users table with accounts and patients tables, that both belong_to :user. We eager load the accounts and patients table for a given user record like this:
user = User.includes([:accounts, :patients]).find_by(email: "testemail#here.com")
...and this works fine. If we want to add a record to a user's accounts table in a manner that both A) commits the new record to the database and B) automatically adds the new record to the eager-loaded array, we execute this:
user.accounts.create(:name => "test name", :customer => "test customer")
...and this also works fine. However, how do we destroy a specific record in such a way that both A) removes the record from the database and B) automatically removes the record from the eager-loaded array? We have tried simply using something akin to:
account1 = user.accounts.first
account1.destroy
...and this does delete the record from the database, but does not automatically remove it from the eager-loaded array. How do we destroy a record in such a manner that it both removes it from the database and removes it from the eager-loaded array?
You want:
account1 = user.accounts.first
user.accounts.destroy(account1)
It actually doesn't matter for your question, or the answer, that you eager-loaded the association. What matters is that the user.accounts association has already been loaded (whether by eager-loading or ordinary on-demand loading), and you want to destroy one of the objects such that, like you say, it both removes it from the database and removes it from the already-loaded in-memory association.
You do that by using the destroy method that any has-many association has.
This destroy method on any has-many association, and others available on any has-many association, are briefly summarized in the Rails Guide, and in a little bit more details in the API docs for the has_many association.
user.accounts.destroy is sort of the inverse of user.accounts.create you already knew about.
Another useful one is << method on a has-many association, which is like create but with an already existing Account object instead of a hash of attributes like create.
account = Account.new(:name => "test name", :customer => "test customer")
user.accounts << account
...which both creates the new account object and adds it to the in-memory user.accounts association.
There are a handful of other useful methods that are available on any has-many association, as well as slightly different sets of useful methods that go along with belongs_to or has_one, etc. Worth reviewing the docs, linked above, to see what's available. (I forget about some of them sometimes, or remember they exist but forget the method signature and have to look it up).

Rails. Get vertically associated data without new queries

I have Order model, that has_many :order_operations. OrderOperation create all the time, when order state is changed. I want to show all OrderOperations.created_at for my orders without creating new queries. I used MySQL.
class Order < ActiveRecord::Base
has_many :order_operations
def change_state new_state
order_operations.create to_state: new_state
end
def date_for_state state_name
order_operations.where(state: state_name).pluck(:created_at).last
end
end
I know about includes and joins methods, but on calling date_for_state always run new query. Even I remove where and pluck query will perform too.
I have only one idea to create service object for this.
When you do a join/include, it caches the results of doing a particular query: specifically, a query to get all of the order_operations associated with the order.
If you had loaded #order, eager-loading the associated order_operations, and did #order.order_operations, then Rails has cached the associated order_operations as part of the include, and doesn't need to load them again.
However, if you do #order.order_operations.where(state: state_name).pluck(:created_at).last, this is a different query than the one used in the include, so rails says "he's asking for some different stuff to the stuff i cached, so i can't use the cached stuff, i need to make another query". You might say "aha, but this will always just be a subset of the stuff you cached, so can't you just work out which of the cached records this applies to?", but Rails isn't that smart.
If you were to do
#order.order_operations.select{|oo| oo.state == state_name}.map(:created_at).last
then you're just doing some array operations and .order_operations will use the cached records, as it's the same query as the one you cached with includes, ie a straight join to all associated records. But, if you call it on an instance var #order that doesn't happen to have already eager-loaded the associated records then it will be much less efficient because it will do a much bigger query than the one you had originally.
In other words: if you want to get an efficiency gain by using includes then the parameters to your includes call needs to exactly match the association calls you will make subsequently on the object.

How do I prevent duplicate values in Google Cloud Datastore?

Is there any mechanism available to prevent duplicate data in certain fields of my entities? Something similar to an SQL unique.
Failing that, what techniques do people normally use to prevent duplicate values?
The only way to do the equivalent on a UNIQUE constraint in SQL will not scale very well in a NoSQL storage system like Cloud Datastore. This is mainly because it would require a read before every write, and a transaction surrounding the two operations.
If that's not an issue (ie, you don't write values very often), the process might look something like:
Begin a serializable transaction
Query across all Kinds for a match of property = value
If the query has matches, abort the transaction
If there are no matches, insert new entity with property = value
Commit the transaction
Using gcloud-python, this might look like...
from gcloud import datastore
client = datastore.Client()
with client.transaction(serializable=True) as t:
q = client.query(kind='MyKind')
q.add_filter('property', '=', 'value')
if q.fetch(limit=1):
t.rollback()
else:
entity = datastore.Entity(datastore.Key('MyKind'))
entity.property = 'value'
t.put(entity)
t.commit()
Note: The serializable flag on Transactions is relatively new in gcloud-python. See https://github.com/GoogleCloudPlatform/gcloud-python/pull/1205/files for details.
The "right way" to do this is to design your data such that the key is your measure of "uniqueness", but without knowing more about what you're trying to do, I can't say much else.
The approach given above will not work in the datastore because you cannot to a query across arbitrary entities inside a transaction. If you try, an exception will be thrown.
However you can do it by using a new kind for each unique field and doing a "get" (lookup by key) within the transaction.
For example, say you have a Person kind and you want to ensure that Person.email is unique you also need a kind for e.g. UniquePersonEmail. That does not need to be referenced by anything but it is just there to ensure uniqueness.
start transaction
get UniquePersonEmail with id = theNewAccountEmail
if exists abort
put UniquePersonEmail with id = theNewAccountEmail
put Person with all the other details including the email
commit the transaction
So you end up doing one read and two writes to create your account.