How to copy a (Active)record between tables, partially? - ruby-on-rails

In two tables mapped to ActiveRecord with unknown number of identical columns, e.g.:
Table A Table B
--------- ---------
id id
name name
age email
email is_member
How can I (elegantly) copy all identical attributes from a record of Table A to a record of Table B, except the id attribute?
For the example tables above, name and email fields should be copied.

Try this:
Get intersection of the columns between TableA and TableB
columns = (TableA.column_names & TableB.column_names) - ["id"]
Now iterate through TableA rows and create the TableB rows.
TableB.create( TableA.all(:select => columns.join(",") ).map(&:attributes) )
Edit: Copying one record:
table_a_record = TableA.first(:select => columns.join(","), :conditions => [...])
TableB.create( table_a_record.attributes)

Migt consider using a union function on the acitverecord attributes hash between the 2 tables. It's not a complete answer but may help

Related

How to write sub query in active record?

I have two tables users and posts and they have association of has_many. I want to fetch details of both users and posts in a single query. I'm able to manage the sql query but I don't want to use the raw query in the code (using execute method) as i think it is kind of simple thing and can be written using active record.
Here is the sql query
SELECT a.id, a.name, a.timestamp, b.id, b.user_id, b.title
FROM users a
INNER JOIN (SELECT id, user_id, title, from, to FROM posts) b on b.user_id = a.id
where id IN ( 1, 2, 3);
I think includes does not help here because i'm dealing with large data.
Can any one help me ?
If you just want those specific columns and nothing else then this will work
User.joins(:post)
.where(id: [1,2,3])
.select("users.id, users.name, users.timestamp,
posts.id as post_id, posts.user_id as post_user_id,
posts.title as post_title")
This will return an ActiveRecord::Relation of User objects with virtual attributes for post_id, post_user_id (Not sure why you need this one since you already selected users.id), and post_title.
The query produced will be
SELECT users.id,
users.name,
users.timestamp,
posts.id as post_id,
posts.user_id as post_user_id,
posts.title as post_title
FROM users
INNER JOIN posts on posts.user_id = users.id
where users.id IN ( 1, 2, 3);
Please note you may have multiple User objects, one for each Post, just as the SQL query does.
You can execute your exact query using the string version of joins e.g.
User.joins("INNER JOIN (SELECT id, user_id, title, from, to FROM posts) b on b.user_id = users.id")
.where(id: [1,2,3])
.select("users.id, users.name, users.timestamp,
b.id as post_id, b.user_id as post_user_id,
b.title as post_title")
Additionally to avoid some of the overhead you can use arel instead e.g.
users_table = User.arel_table
posts_table = Post.arel_table
query = users_table.project(Arel.star)
.join(posts_table)
.on(posts_table[:user_id].eq(users_table[:id]))
.where(users_table[:id].in([1,2,3]))
ActiveRecord::Base.connection.exec_query(query.to_sql)
This will return an ActiveRecord::Result with 2 useful methods columns (the columns selected) and rows. You can convert this to a Hash(#to_hash) but note that any columns with duplicate names (id for instance) will overwrite one another.
You could fix this by specifying the colums you want selected in the project portion. e.g. your current query would be:
query = users_table.project(
users_table[:id],
users_table[:name],
users_table[:timestamp],
posts_table[:id].as('post_id'),
posts_table[:user_id].as('post_user_id'),
posts_table[:title].as('post_title')
).join(posts_table)
.on(posts_table[:user_id].eq(users_table[:id]))
.where(users_table[:id].in([1,2,3]))
ActiveRecord::Base.connection.exec_query(query.to_sql).to_hash
Since none of the names collide now it can be structured into a nice Hash where the keys are the column names and the values or the row value for that record.
users = User.joins(:posts).includes(:posts).where(id: [1, 2, 3])
Will give you all the users with theirs posts.
then you can do whatever you want with them, but to access posts data for first retrieved user
first_user_posts = users.first.posts # this will not make additional DB queries as you used includes and data is already added
We use joins to have INNER JOIN statement in the SQL
We use includes to load all posts in the memory
I have two tables users and posts and they have association of
has_many. I want to fetch details of both users and posts in a single
query.
can be done with includes like
users = User.includes(:posts).where({posts: {user_id: [1,2,3]}})
other is eager_load and preload you can use as per your requirements, for more https://blog.arkency.com/2013/12/rails4-preloading/

How can I match the same column value in different tables?

I have two tables, Table A is the parent, Table B is the child. Both tables have the column "order_number".
I want to find the value in the "order_number" column in Table A, that matches the value of the "order_number" column in Table B, then I'll save the "id" of the Table A record in another column of the Table B record.
I'm having trouble coming up with an activerecord query to find the matching column value. How should I do that?
Activerecord query to find the matching column value:
I think this should do it:
TableA.where(order_number: TableB.pluck(:order_number))
Then you can loop over it to update the tableB column for TableA id like below(assuming its called table_a_id)
tablea_records = TableA.where(order_number: TableB.pluck(:order_number))
tablea_records.each do |a|
b.where(order_number:a.order_number).update_all(table_a_id: a.id)
end

Rails Postgres query to exclude any results that contain one of three records on join

This is a hard problem to describe but I have Rails query where I join another table and I want to exclude any results where the join table contain one of three conditions.
I have a Device model that relates to a CarUserRole model/record. In that CarUserRole record it will contain one of three :role - "owner", "monitor", "driver". I want to return any results where there is no related CarUserRole record where role: "owner". How would I do that?
This was my first attempt -
Device.joins(:car_user_roles).where('car_user_roles.role = ? OR car_user_roles.role = ? AND car_user_roles.role != ?', 'monitor', 'driver', 'owner')
Here is the sql -
"SELECT \"cars\".* FROM \"cars\" INNER JOIN \"car_user_roles\" ON \"car_user_roles\".\"car_id\" = \"cars\".\"id\" WHERE (car_user_roles.role = 'monitor' OR car_user_roles.role = 'driver' AND car_user_roles.role != 'owner')"
Update
I should mention that a device sometimes has multiple CarUserRole records. A device can have an "owner" and a "driver" CarUserRole. I should also note that they can only have one owner.
Anwser
I ended up going with #Reub's solution via our chat -
where(CarUserRole.where("car_user_roles.car_id = cars.id").where(role: 'owner').exists.not)
Since the car_user_roles table can have multiple records with the same car_id, an inner join can result in the join table having multiple rows for each row in the cars table. So, for a car that has 3 records in the car_user_roles table (monitor, owner and driver), there will be 3 records in the join table (each record having a different role). Your query will filter out the row where the role is owner, but it will match the other two, resulting in that car being returned as a result of your query even though it has a record with role as 'owner'.
Lets first try to form an sql query for the result that you want. We can then convert this into a Rails query.
SELECT * FROM cars WHERE NOT EXISTS (SELECT id FROM car_user_roles WHERE role='owner' AND car_id = cars.id);
The above is sufficient if you want devices which do not have any car_user_role with role as 'owner'. But this can also give you devices which have no corresponding record in car_user_roles. If you want to ensure that the device has at least one record in car_user_roles, you can add the following to the above query.
AND EXISTS (SELECT id FROM car_user_roles WHERE role IN ('monitor', 'driver') AND car_id = cars.id);
Now, we need to convert this into a Rails query.
Device.where(
CarUserRole.where("car_user_roles.car_id = cars.id").where(role: 'owner').exists.not
).where(
CarUserRole.where("car_user_roles.car_id = cars.id").where(role: ['monitor', 'driver']).exists
).all
You could also try the following if your Rails version supports exists?:
Device.joins(:car_user_roles).exists?(role: ['monitor', 'driver']).exists?(role: 'owner').not.select('cars.*').distinct
Select the distinct cars
SELECT DISTINCT (cars.*) FROM cars
Use a LEFT JOIN to pull in the car_user_roles
LEFT JOIN car_user_roles ON cars.id = car_user_roles.car_id
Select only the cars that DO NOT contain an 'owner' car_user_role
WHERE NOT EXISTS(SELECT NULL FROM car_user_roles WHERE cars.id = car_user_roles.car_id AND car_user_roles.role = 'owner')
Select only the cars that DO contain either a 'driver' or 'monitor' car_user_role
AND (car_user_roles.role IN ('driver','monitor'))
Put it all together:
SELECT DISTINCT (cars.*) FROM cars LEFT JOIN car_user_roles ON cars.id = car_user_roles.car_id WHERE NOT EXISTS(SELECT NULL FROM car_user_roles WHERE cars.id = car_user_roles.car_id AND car_user_roles.role = 'owner') AND (car_user_roles.role IN ('driver','monitor'));
Edit:
Execute the query directly from Rails and return only the found object IDs
ActiveRecord::Base.connection.execute(sql).collect { |x| x['id'] }

postgresql get list of unique list with order by another table column

Tables:
#leads
id | user_id |created_at | updated_at
#users
id | first_name
#todos
id | deadline_at | target_id
I want to get unique list of leads between two dates(deadline_at) with ordering by todos.deadline_at desc
I do:
SELECT distinct(leads.*), todos.deadline_at
FROM leads
INNER JOIN users ON users.id = leads.user_id
LEFT JOIN todos ON todos.target_id = leads.user_id
WHERE (todos.deadline_at between '2015-11-26T00:00:00+00:00' and '2015-11-26T23:59:59+00:00')
ORDER BY todos.deadline_at DESC;
This query returns right ordered list but with duplicates. If I use distinct or distinct on with leads.id, then postgresql requires me use it in order by - In that case I got wrong ordered list.
How do I can achive expected result?
Since you don't really need the users table.
Maybe try this?
Lead.joins("INNER JOIN todos ON leads.user_id = todos.target_id")
.where("leads.deadline_at" => (date_a..date_b))
.select("leads.*, todos.deadline_at")
.order("todos.deadline_at desc")
It seams that you're confusing the result of a sql table with joins and the same result after ActiveRecord treatment on an association.
I presume Lead has_many :todos, through: :user so you can do this :
Lead.eager_load(:todos).
where("leads.deadline_at" => (date_a..date_b)).
order("todos.deadline_at")
No need to apply distinct or whatever, ActiveRecord will sort out the leads from the todosand you'll have them in the right order with no duplicates. The raw sql result however will have plenty of duplicates.
If you want to achieve something similar in sql alone, you can use distinct or group by on leads.id, but then you'll lose all the todos it "contains". However you can use aggregate function to calculate/extract things on the "lost todo data".
For example :
Lead.joins(:todos).
group("leads.id").
select("leads.*, min(todos.deadline_at) as first_todo_deadline")
order("first_todo_deadline")
Notice that todos data is only available in the aggregate functions (min, count, avg, etc) since the todos are "compressed" if you wish in each lead!
Hope it makes sense.

Select all columns by a unique column value in Rails 3

In Rails 3, how do i select rows based on unique column values, i need to get all the columns for eg:
SELECT COUNT(DISTINCT date) FROM records
This only returns date column, but i want all the columns (name, date , age , created_at) columns not just the date.
Thanks for your help
The issue here is that, by definition, there may be multiple records with the same date. It requires logic in the user space to determine which of the multiple records with the unique date to use. Here's some code to get those rows:
Record.select("distinct date").each do |record|
records = Record.find_by_date record.date
puts records.count # do something with the records
end
If what you're really after is uniqueness among multiple columns, list all the relevant columns in the distinct query:
Record.select("distinct date, name, age, created_at").each do |record|
puts record.date
puts record.name
puts record.age
puts record.created_at
# ``record'' still represents multiple possible records
end
The fact that you are using distinct means that each "row" returned actually represents n rows, so the DB doesn't know which of the n rows to pull the remaining columns from. That's why it only returns the columns used in distinct. It can do no other...
I think this will help you
Model.find(:all, :select => 'DISTINCT name, date, age, created_at')
Please use it and let me know.
Model.group(:column)
For your case:
Record.group(:date)
This will return all your columns with no "date" repetitions.
For rails 3.2 and higher, Model.select('DISTINCT name, date, age, created_at')

Resources