Skip to content

Instantly share code, notes, and snippets.

@ippeiukai
Last active August 29, 2015 14:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ippeiukai/23eee3d5ad8c0ab4515f to your computer and use it in GitHub Desktop.
Save ippeiukai/23eee3d5ad8c0ab4515f to your computer and use it in GitHub Desktop.
bulk update of records in rails with UPDATE SET FROM (VALUES ...) AS WHERE
# NOTE assumes the relation is very very simple
def bulk_update(relation, values, where_columns, set_columns)
return if values.empty?
connection = relation.connection
base_class = relation.base_class
if relation.arel.where_sql.present?
relation_ids = relation.ids
end
quote_column_name_fn = connection.method(:quote_column_name)
quote_column_value_fn = ->(value, col_name) { base_class.quote_value(value, base_class.column_for_attribute(col_name)) }
destination_table_name_quoted = relation.quoted_table_name
temporary_table_name_quoted = quote_column_name_fn.('tmp_data')
temporary_table_columns = [*where_columns, *set_columns]
temporary_table_def = "#{temporary_table_name_quoted}(#{temporary_table_columns.map(&quote_column_name_fn).join(', ')})"
temporary_table_values_list = values.map do |record|
record.to_h.values_at(*temporary_table_columns).map.with_index do |val, i|
# cast is to make sure temporary table is correctly typed (e.g. timestamp without explicit type coercion is text)
"CAST(#{quote_column_value_fn.(val, temporary_table_columns[i])} AS #{base_class.column_for_attribute(temporary_table_columns[i]).sql_type})"
end
end
set_column_names_quoted = set_columns.map(&quote_column_name_fn)
set_fields = set_column_names_quoted.map { |col| "#{col} = #{temporary_table_name_quoted}.#{col}" }.join(', ')
where_column_names_quoted = where_columns.map(&quote_column_name_fn)
where_cond = where_column_names_quoted.map { |col| "#{destination_table_name_quoted}.#{col} = #{temporary_table_name_quoted}.#{col}" }.join(' AND ')
if relation_ids
relation_cond = "#{destination_table_name_quoted}.#{quote_column_name_fn.(relation.primary_key)} IN (#{relation_ids.map {|val| quote_column_value_fn.(val, relation.primary_key) }.join(', ')})"
end
connection.update(<<-SQL)
UPDATE #{destination_table_name_quoted}
SET #{set_fields}
FROM
(VALUES
#{temporary_table_values_list.map { |vals| "(#{vals.join(', ')})" }.join(', ')}
) AS #{temporary_table_def}
WHERE
#{[where_cond, relation_cond].compact.join(' AND ')}
SQL
end
@ippeiukai
Copy link
Author

bulk_update(Users.all, [{id: 1, name: 'User 1'}, {id: 2, name: 'User 2'}], [:id], [:name])

Note:
This is primarily for PostgreSQL. Does not work with MySQL because it does not support (VALUES ...) AS.

I'd love to know how to do this with MySQL. INSERT with ON DUPLICATE KEY UPDATE? Maybe CASE-WHEN is most universal...

@ippeiukai
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment