beam-community/ex_machina

Slow insert when using build/1

speeddragon opened this issue · 2 comments

I've been using ex_machina for a year, and some time back I notice that my inserts were slow (~0.5s) [Solve, it as a issue in my side]. Today I decided to try to find the issue, and found that even when I'm giving an attribute, the function build/1 is always run.

Example:

def order_factory do
    %Order{
       name: "xpto",
       user: build(:user)
    }
end

Using insert(:order) will create the :user fine.
Using insert(:order, user: user) will use the user attribute, but it also will run the build(:user). Shouldn't this detect when the parameter already exists ?

This is a debug text that I've put on 5 inserts inside a loop:

with build/1, but given an user previously as you can see by user_id being the same

Finish setup
Insert Order 1
User ID: 2447
Insert Order 2
User ID: 2447
Insert Order 3
User ID: 2447
Insert Order 4
User ID: 2447
Insert Order 5
User ID: 2447
"Insert total time: 18 ms"

without build/1, but given an user previously as you can see by user_id being the same

Finish setup
Insert Order 1
User ID: 2448
Insert Order 2
User ID: 2448
Insert Order 3
User ID: 2448
Insert Order 4
User ID: 2448
Insert Order 5
User ID: 2448
"Insert total time: 9 ms"

I've found this example from 2015, but I think it doesn't work anymore, https://thoughtbot.com/blog/announcing-ex-machina.

Hi @speeddragon you're absolutely right that running, insert(:order, user: user) will still build the user that was declared in the factory definition.

The reason for that is that ExMachina's insert builds your factory as defined, then merges any additional attributes you've passed into it, and then inserts it into the database. So any build declaration in the definition will get run before we merge in the attributes passed in.

In other words, it's a series of transformations that goes roughly like this:

build(:order, user: build(:user, name: "user-A"))

# step one, build the factory with the default user which results in
 %Order{
       name: "xpto",
       user: %User{ name: "defaul name" }
    }

# step two, merge attributes passed (in this example the `build(:user)` that was passed in  
%Order{
  name: "xpto",
  user: %User{name: "user-A"}
}

# step three, insert into the database
Repo.insert!(%Order{
  name: "xpto",
  user: %User{name: "user-A"}
})

There's not a whole lot of magic other than that.

So I'm not sure what we could do about the speed of building attributes, unless we changed the way we define factories substantially. Does that make sense?

Thanks for the explanation. I could have take a better look into the code, but didn't have time to do it.
Initially my error was with bcrypt round definition in tests not being setup correctly, so after that fix the difference isn't huge, still I think it would be a good improvement.

I will close the issue.