Thin controller / Thick model in Phoenix/Ecto - phoenix-framework

I'm trying to figure out where to place common functions that I would normally (in Rails/ActiveRecord) put in a model class. Specifically, I have User and Company with a many-to-many relationship between them, but a user has a default_company, which just has a boolean flag on the user_companies join table.
ActiveRecord
class User < ActiveRecord::Base
belongs_to :user_companies
has_many :companies, through: :user_companies
def default_company
# Filter through companies to find the one that I want
end
end
(Note, there's probably an even easier way to do it, but this is the basic idea.)
Ecto
I could do something similar in Ecto, like so:
defmodule MyApp.User do
use MyApp.Web, :model
alias MyApp.{Company, CompaniesUser}
schema "users" do
has_many :companies_users, CompaniesUser, on_delete: :delete_all
many_to_many :companies, Company, join_through: "companies_users"
end
def default_company(%User{} = user) do
from(company in Company,
join: cu in CompaniesUser,
where: cu.company_id == company.id
and cu.user_id == ^user.id
and cu.default_company == true
) |> first() |> Repo.one()
end
end
However, based on my limited experience, this seems incorrect. All the examples I have seen keep the Ecto model very limited, just a bunch of changeset methods and some validation code, but strictly nothing business related. There is talk of keeping your business logic separate from your database logic. I get that and respect it, but most of the examples show putting raw Ecto queries inside a controller or otherwise sprinkling Ecto queries all over your app, and that seems wrong too.

Phoenix 1.3
From what I've read about the upcoming 1.3, it looks like the expectation is that this will be handled with Contexts, or specifically, modules that will allow you to logically group your Ecto schema models along with associated modules that define (manually: you define it) an API to access your persistence layer. So, using my above example, it would be something like:
defmodule MyApp.Account do
alias MyApp.Account.User
alias MyApp.Corporate.{Company, CompaniesUser}
def default_company(%User{} = user) do
from(company in Company,
join: cu in CompaniesUser,
where: cu.company_id == company.id
and cu.user_id == ^user.id
and cu.default_company == true
) |> first() |> Repo.one()
end
end
defmodule MyApp.Account.User do
use MyApp.Web, :model
alias MyApp.Corporate.{Company, CompaniesUser}
schema "users" do
has_many :companies_users, CompaniesUser, on_delete: :delete_all
many_to_many :companies, Company, join_through: "companies_users"
end
end
It has 2 modules, one (MyApp.Account.User) is my raw Ecto schema. The other (MyApp.Account) is the API/entry point for all the other logic in my app, like the controllers.
I guess I like the theory, but I'm worried about trying to figure out what models should go where, like in this example: Does Company belong in the Account context, or do I make a new Corporate context?
(Sorry for asking/answering my own question, but in researching the question I found the info for Phoenix 1.3 and thought I might as well just post for anyone who is interested.)

Related

return results from active record relation many_to_many assocation

I currently have a many_to_many association between templates and types.
I have an active record relation of templates.
I want to return all of the types that are linked to those templates.
For example, in an ideal world i would be able to do templates.types.
I have tried templates.joins(:types), however this returns templates rather then types.
So i'm not sure on another way to do this.
In pure ruby, without a DB you would want to flat_map
types = templates.flat_map do |template|
template.types
end
types.uniq # probably you only want unique types
This works but is not efficient as soon as you got many Templates/Types since it triggers more queries and loads more objects than needed.
When you have ActiveRecord you can add a scope or self method (either or, not borth as in my example) to Type
class Type < ApplicationRecord
has_many :template_types
has_many :templates, through: :template_types
scope :for_templates, -> (templates) { joins(:template_types).where(template_types: {template: templates}).distinct } # either this or the method below
def self.for_templates(templates)
Type.joins(:template_types).where(template_types: {template: templates}).distinct
end
end
(i assumed that the join model is TemplateType)
and then do
templates = Template.some_complicated_query
Type.for_templates(template)
I'd recommend you rename Type since type already has a special meaning with ActiveRecord (Single Table Inheritance).

How to JOIN tables of two different databases in Ruby on Rails [duplicate]

My environment: Ruby 1.9.2p290, Rails 3.0.9 and RubyGem 1.8.8
unfortunately I have an issue when come across multiple database.
The situation is this: I have two model connect with two different database and also establishing association between each other.
database connection specifying in each model, look likes
class Visit < ActiveRecord::Base
self.establish_connection "lab"
belongs_to :patient
end
class Patient < ActiveRecord::Base
self.establish_connection "main"
has_many :visits
end
I got an error when meet following scenario
#visits = Visit.joins(:patient)
Errors: Mysql2::Error: Table 'lab.patients' doesn't exist: SELECT visits.* FROM visits INNER JOIN patients ON patients.id IS NULL
Here 'patients' table is in 'main' database and 'visits' table in 'lab' database
I doubt when executing the code, that Rails is considering 'patients' table is part of 'lab' database [which holds 'visits' table].
Well, I don't know if this is the most elegant solution, but I did get this to work by defining self.table_name_prefix to explicitly return the database name.
class Visit < ActiveRecord::Base
def self.table_name_prefix
renv = ENV['RAILS_ENV'] || ENV['RACK_ENV']
(renv.empty? ? "lab." : "lab_#{renv}.")
end
self.establish_connection "lab"
belongs_to :patient
end
class Patient < ActiveRecord::Base
def self.table_name_prefix
renv = ENV['RAILS_ENV'] || ENV['RACK_ENV']
(renv.empty? ? "main." : "main_#{renv}.")
end
self.establish_connection "main"
has_many :visits
end
I'm still working through all the details when it comes to specifying the join conditions, but I hope this helps.
Might be cleaner to do something like this:
def self.table_name_prefix
"#{Rails.configuration.database_configuration["#{Rails.env}"]['database']}."
end
That will pull the appropriate database name from your database.yml file
Or even
def self.table_name_prefix
self.connection.current_database+'.'
end
Is your 2nd database on another machine? You can always do as suggested in this other question:
MySQL -- Joins Between Databases On Different Servers Using Python?
I'd use the self.table_name_prefix as proposed by others, but you can define it a little more cleanly like this:
self.table_name_prefix "#{Rails.configuration.database_configuration["#{Rails.env}"]['database']}."
alternatively you could also use this:
self.table_name_prefix "#{connection.current_database}."
You have to keep in mind that the latter will execute a query SELECT DATABASE() as db the first time that class is loaded.

Rails metaprogramming: singleton_class and associations

I'm trying to understand metaprogramming in rails, creating validations and associations dynamically on a class.
Let's say I have the following models:
class House < ActiveRecord::Base
belongs_to :owner
end
class Owner < ActiveRecord::Base
end
Now let's say my House model has a boolean attribute is_ownable, and I only want the house to have the owner association if is_ownable==true.
I thought this would work:
class House < ActiveRecord::Base
after_initialize :create_associations
after_find :create_associations
def create_associations
if self.is_ownable
self.singleton_class.belongs_to :owner
end
end
end
Now when I build or find a record of House, the create_associations function gets called with no errors, but then when I try to access the House.first.owner it throws ActiveRecord::AssociationNotFoundError.
Am I misunderstanding something about how AR associations work?
I hate to say it but this is probably a bad idea. Models should have consistent relationships even if they're not utilized on every model. This is not only against the spirit of ActiveRecord or Ruby, but object oriented programming in general. In most cases objects of a particular class are expected to have an identical interface for the sake of consistency and clarity. Adding methods to individual objects is permitted, but there should be exceptional circumstances to justify such a thing.
That's not to say you can't get the effect you want in a more idiomatic way:
class House < ActiveRecord::Base
belongs_to :owner
validates :validate_owner_assignment
protected
def validate_owner_assignment
if (self.ownable? and !self.owner)
self.errors.add(:owner, "is required if ownable")
elsif (!self.ownable? and self.owner)
self.errors.add(:owner, "cannot be assigned if not ownable")
end
end
end
Now assigning owner will trigger a save failure of type ActiveRecord::RecordInvalid if the expectations aren't met.
I'd advocate calling your booleans x and not is_x to reduce verbosity. The vast majority of the time the is_ part is redundant.

Join type in ActiveRecord has_one Relationship

Just getting started with ActiveRecord (in a sinatra app). Trying to port existing queries to AR but getting a little stuck.
if i have a has_one relation for users and profiles (using legacy tables unfortunately)
class User < ActiveRecord::Base
self.table_name = "systemUsers"
self.primary_key = "user_id"
has_one :profile, class_name: 'Profile', foreign_key: 'profile_user_id'
end
class Profile < ActiveRecord::Base
self.table_name = "systemUserProfiles"
self.primary_key = "profile_id"
belongs_to :user, class_name: "User", foreign_key: 'user_id'
end
if i want to query all users with profile using an inner join then get the user_age field from the profiles using one query can i do it?
for example
(just added .first to reduce code but would be looping through all users with profiles)
user = User.all(:joins => :profile).first
user.profile.user_age
gives me the correct data and uses a INNER join for the first query but then issues a second query to get the profile data
it also gives a depreciated warning and suggests i use load, which i tried but won't use an inner join.
similar case with
user = User.joins(:profile).first
user.profile.user_age
i get an INNER join but a query for each user row.
I have tried includes
user = User.includes(:profile).first
user.profile.user_age
This lazy loads the profile and would reduce the number of queries in the loop, however i think it would pull users without a profile too
I also tried with a reference
user = User.includes(:profile).references(:profile).first
user.profile.user_age
This gives me the correct data and reduces the queries to 1 but uses a LEFT JOIN
I probably have not quite grasped it and am trying to achieve something thats not do-able, i figured i might either need to use includes and check for nil profiles inside the loop or use joins and accept the additional query for each row.
Thought i'd check incase i was missing something obvious.
Cheers
Pat.
Profile should always have one user. So, I would do Profile.first.user_age for the first user profile. But going by the user approach like you did,
User.find { |u| u.profile }.profile.user_age
User.find { |u| u.profile } returns the first user with true value.
To query all the user profiles and get their user_ages. Assuming all profiles has user_id and that should be the case.
Profile.pluck(:user_age)
This checks the presence of user_id if you save profiles without user id. This where.not is a new feature in Activerecord, check your version.
Profile.where.not(user_id: nil).pluck(:user_age)

Is there a way in MongoMapper to achieve similar behavior as AR's includes method?

Is there a feature equivalent in MongoMapper to this:
class Model < ActiveRecord::Base
belongs_to :x
scope :with_x, includes(:x)
end
When running Model.with_x, this avoids N queries to X.
Is there a similar feature in MongoMapper?
When it's a belongs_to relationship, you can turn on the identity map and run two queries, once for your main documents and then one for all the associated documents. That's the best you can do since Mongo doesn't support joins.
class Comment
include MongoMapper::Document
belongs_to :user
end
class User
include MongoMapper::Document
plugin MongoMapper::Plugins::IdentityMap
end
#comments = my_post.comments # query 1
users = User.find(#comments.map(&:user_id)) # query 2
#comments.each do |comment|
comment.user.name # user pulled from identity map, no query fired
end
(Mongoid has a syntax for eager loading, but it works basically the same way.)

Resources