I think 'Context’s are a good idea, just the name is weird (I’ll watch the video when I can ^.^), but the way I do it is shaped by decades of Erlang, C++, Python, and Java, with lots of other languages sprinkled in, so my Elixir projects may be a little different in style. Basically here is what I do:
-
Web: Is strictly for front-end things like controllers, channels, commanders, routing, and views/templates. Everything is kept as minimal as possible just calling out to somewhere else to get data (growingly becoming Absinthe.run
in a lot of places…) and send it back out.
-
Models: Yes I still have plain models
though the directory is called “db” (as is the namespace), it is literally the schemas that map directly to the database tables, a lot of the tables I did not create and have no control over (other systems) but that I have to deal with (you can definitely feel a lot of Ecto’s limitations when the system was not built ‘by’ ecto…), they hold no data, no processing, not even changesets, they are just the schema.
-
Modules: Not modules like elixir modules but modules of functionality, like one is called ‘Accounts’ (within MyServer.Accounts
) but there are many. The Accounts (elixir) module itself just has a set of calls on it, things like confirm_by_login/1
that can be called like Accounts.confirm_by_login(login: username, password: the_password)
or Accounts.confirm_by_login(pidm: pidm, password: the_password)
or Accounts.confirm_by_login(banner_username: username, password: the_password)
or Accounts.confirm_by_login(google: google_oauth_struct)
or various others, all of which is called via my Ueberauth callbacks from a variety of systems, and it returns either a valid account id (a UUID built from a variety of internal data so it is easily reversible to access various systems) or it returns an exception struct, no it does not raise it, it returns it, that allows me to build up a list of possible errors (using some of expede’s libraries, which are quite nice I must add) and return many error messages instead of just the first that happened to happen.
The Accounts module has a lot more calls on it to get various information about an account, do things to accounts, etc… etc…
Most of the modules however (very few calls on Accounts) have a lot of query_
and multi_
calls, depending on if getting or setting data calls (many ‘getting’ use multi_ as well because access needs to be logged in many cases), such as the Banner module, it has things like (this is the only short one in that whole module, most are utterly ginormous because of the horror’s of accessing this ancient system and needing to join across 10 tables in most cases and a lot more in a few, this one will grow that big over time as well…):
def query_classes(selected \\ :processed, refine) do
squery =
from course in DB.Banner.SCBCRSE,
join: section in DB.Banner.SSBSECT, on: section.ssbsect_subj_code == course.scbcrse_subj_code and section.ssbsect_crse_numb == course.scbcrse_crse_numb and section.ssbsect_ssts_code == "A",
join: dept in DB.Banner.STVDEPT, on: dept.stvdept_code == course.scbcrse_dept_code
squery =
Enum.reduce(refine, squery, fn
({:pidm, true}, squery) ->
join(squery, :inner, [course, section, dept],
student_course in DB.Banner.SFRSTCR,
student_course.sfrstcr_term_code == section.ssbsect_term_code and
student_course.sfrstcr_crn == section.ssbsect_crn
)
({:pidm, pidm}, squery) when is_integer(pidm) ->
join(squery, :inner, [course, section, dept],
student_course in DB.Banner.SFRSTCR,
student_course.sfrstcr_pidm == ^pidm and
student_course.sfrstcr_term_code == section.ssbsect_term_code and
student_course.sfrstcr_crn == section.ssbsect_crn
)
({:pidm, pidms}, squery) when is_list(pidms) ->
join(squery, :inner, [course, section, dept],
student_course in DB.Banner.SFRSTCR,
student_course.sfrstcr_pidm in ^pidms and
student_course.sfrstcr_term_code == section.ssbsect_term_code and
student_course.sfrstcr_crn == section.ssbsect_crn
)
({:registered, true}, squery) ->
where(squery, [course, section, dept, student_course], student_course.sfrstcr_rsts_code in ["RA", "RE", "RW"])
({:withdrawn, true}, squery) ->
where(squery, [course, section, dept, student_course], student_course.sfrstcr_rsts_code == "WD")
({:department, dept_code}, squery) when is_binary(dept_code) ->
where(squery, [course, section, dept], course.scbcrse_dept_code == ^dept_code)
({:department, dept_code}, squery) when is_list(dept_code) ->
where(squery, [course, section, dept], course.scbcrse_dept_code in ^dept_code)
({:subject, subject_code}, squery) when is_binary(subject_code) ->
where(squery, [course, section, dept], section.ssbsect_subj_code == ^subject_code)
({:subject, subject_code}, squery) when is_list(subject_code) ->
where(squery, [course, section, dept], section.ssbsect_subj_code in ^subject_code)
({:course, course_number}, squery) when is_binary(course_number) ->
where(squery, [course, section, dept], section.ssbsect_crse_numb == ^course_number)
({:course, course_number}, squery) when is_list(course_number) ->
where(squery, [course, section, dept], section.ssbsect_crse_numb in ^course_number)
({semester, year}, squery) when semester in [:spring, :summer, :fall] and is_integer(year) and year>=1900 and year<=9999 -> squery # Handled below in `dyn`
({:course_group, %DB.Course.Group{}=course_group}, squery) ->
squery = if(course_group.dept_codes, do: where(squery, [course, section, dept], course.scbcrse_dept_code in ^course_group.dept_codes), else: squery)
squery = if(course_group.subject_codes, do: where(squery, [course, section, dept], section.ssbsect_subj_code in ^course_group.subject_codes), else: squery)
squery = if(course_group.course_numbers, do: where(squery, [course, section, dept], section.ssbsect_crse_numb in ^course_group.course_numbers), else: squery)
# TODO: Test the term codes and such too as they will be wanted eventually...
squery
end)
dyn =
Enum.reduce(refine, false, fn
({semester, year}, dyn) when semester in [:spring, :summer, :fall] and is_integer(year) and year>=1900 and year<=9999 ->
term_code =
case semester do
:spring -> "#{year}10"
:summer -> "#{year}20"
:fall -> "#{year}30"
end
case dyn do
false -> dynamic([course, section, dept], section.ssbsect_term_code == ^term_code)
dyn -> dynamic([course, section, dept], ^dyn or section.ssbsect_term_code == ^term_code)
end
(_, dyn) -> dyn
end)
squery =
case dyn do
false -> squery
dyn -> where(squery, ^dyn)
end
squery =
case selected do
:all -> squery
:processed ->
case Keyword.get(refine, :pidm) do
v when v == true or is_list(v) ->
select(squery, [course, section, dept, student_course], %{
pidm: student_course.sfrstcr_pidm,
department_code: course.scbcrse_dept_code,
department_description: dept.stvdept_desc,
crn: section.ssbsect_crn,
subject: section.ssbsect_subj_code,
course_number: section.ssbsect_crse_numb,
section_number: section.ssbsect_seq_numb,
title: fragment("coalesce(?, ?)", section.ssbsect_crse_title, course.scbcrse_title),
section_begins: section.ssbsect_ptrm_start_date,
section_ends: section.ssbsect_ptrm_end_date,
registration_code: student_course.sfrstcr_rsts_code,
effective_term: course.scbcrse_eff_term,
_effective_term_rank: fragment("rank() OVER (PARTITION BY ?, ? ORDER BY ? DESC)", section.ssbsect_subj_code, section.ssbsect_crse_numb, course.scbcrse_eff_term),
})
_ ->
select(squery, [course, section, dept], %{
department_code: course.scbcrse_dept_code,
department_description: dept.stvdept_desc,
crn: section.ssbsect_crn,
subject: section.ssbsect_subj_code,
course_number: section.ssbsect_crse_numb,
section_number: section.ssbsect_seq_numb,
title: fragment("coalesce(?, ?)", section.ssbsect_crse_title, course.scbcrse_title),
section_begins: section.ssbsect_ptrm_start_date,
section_ends: section.ssbsect_ptrm_end_date,
effective_term: course.scbcrse_eff_term,
_effective_term_rank: fragment("rank() OVER (PARTITION BY ?, ? ORDER BY ? DESC)", section.ssbsect_subj_code, section.ssbsect_crse_numb, course.scbcrse_eff_term),
})
end
end
query =
from s in subquery(squery),
where: s._effective_term_rank == 1
query
end
(Wtf, without an extra newline here the forum hides this entire next paragraph?! Bug with code fences inside bullets??)
Now in this one, like most queries in the system, follow this pattern, as a lot of the queries are user-driven and built from custom things to create reports that they pull then a lot of the things that are queried are actually built up from a whole set of refinements (that grows over time as their needs grow over time for more and more reports). But as you can see the ‘functionality’ is split into specific areas. Inside of, say, Accounts
are more modules like Accounts.PIDM
and Accounts.Google
and so forth but nothing outside accesses those directly, everything goes through the main interface.
But this is just normal separation of concerns that is pretty universal among ‘many’ programming languages, but I’ve never heard of a Separation of Concerns as Contexts though, nor does it really seem like it should have a special name anyway as it just seems like normal Good Programming?
So yeah, I do not get why ‘Contexts’ are a barrier for newbies to Elixir, this is stuff that any programmer should be doing in any language already. If anything, giving it such a ‘special name’ is the most confusing part about it, but they themselves are simple. I really do think calling them what they are, Separation of Concerns
would be far more descriptive and less harrowing to newbies as Contexts
has no real context.
/me has never heard of DDD before Phoenix added the ‘Context’ special stuff, and is wondering if that DDD book is just rehashing 50-year-old separation of concern ideas for a money-grab or something…