rogerkk
rogerkk

Reputation: 5695

Wrapping #find_each and exposing similar functionality myself

Background

I've separated some piece of functionality into a service/library of sorts, to try to cleanly separate this functionality from the rest of my application. I'll call it the service from here on.

Currently this small service leaks ActiveRecord objects into the calling code, but I'd really prefer the separation to be cleaner.

Code example

Service's API:

module MyService
  # This method should really be read-only, but we're exposing 
  # ActiveRecord objects, so the caller can mess around with them. :(
  def self.people
    Person.all
  end
end

Calling code example

MyService.people.find_each(batch_size: 500) do |person|
  # Do something useful with person
end

I'm thinking converting the ActiveRecord objects into Structs before returning them to the calling code could be an idea to achieve this.

The problem

There's just one problem with this: The Person model has thousands of entries. Thus my naive approach would instantiate thousands of ActiveRecord, copy their contents to thousands of Structs and then return. This would obviously be a huge memory hog.

What I'd like is to be able for the calling code to fetch the Person objects in batches, and avoid having MyService.people instantiating all records at once.

I really can't completely wrap my head around how this can be implemented. Can anyone point me in the right direction here?

Upvotes: 1

Views: 33

Answers (1)

Kristján
Kristján

Reputation: 18813

You can write your own method on MyService that expects a block and converts the Person to a read-only version before it yields.

module MyService
  ReadOnlyPerson = Struct.new(:first_name, :last_name)

  def self.each_person
    Person.find_each do |person|
      read_only_person = ReadOnlyPerson.new(person.first_name, person.last_name)
      yield read_only_person
    end
  end
end
MyWrapper.each_person do |person|
  puts person.first_name
end

If you want to keep full ActiveRecord functionality except for writing, you can also directly mark your instances with readonly!:

def self.each_person
  Person.find_each do |person|
    person.readonly!
    yield person
  end
end

You can still assign things like person.first_name = 'Phil', but trying to save will raise an exception:

ActiveRecord::ReadOnlyRecord: Person is marked as readonly

Upvotes: 2

Related Questions