📜 ⬆️ ⬇️

ActiveRecord-based Ruby REST API for accessing tables in a database

On Ruby and many other languages, there are convenient ORM solutions for programmatic access to the DBMS. There are also frameworks like RubyOnRails for easy and convenient creation of web applications that work with the database. Simple conventions allow you to write a little code and at the same time create powerful interfaces.

However, let's imagine the following situation: we have a large developing corporate system, in which there are a lot of entities. Very often, a large number of entities that contain complex business logic has dependencies on many small ones. For example, dictionaries.

Standard actions for getting the ability to work with an entity in Rails - create a model, create a controller, create a view (and if we have a full set of REST actions, then two - to get the list and to get one record if you work through the API). The actions are simple, and the files are very simple. And when there are many dictionaries, we also get a large number of files. A large number of simple and single-type files. And somewhere among them, large, atypical files will be lost. In addition to a large number of files, we get problems with the need to prescribe routes. Even those who support such systems for more than one year will find it difficult to navigate the project, not like a new developer.
')
This is where Rails magic comes to the rescue.

For simplicity, suppose we build a JSON API. Although nothing prevents us from adding support for XML.

So, one file and one class that can replace a very large number of small single-type files and classes.

It is likely that our small and simple dictionary or simple table may grow a little beyond the standard REST actions, and we would not like to lose the possibility of using these actions. We do not really want to write the source code again. Therefore, we will create a base class UniversalApiController. If necessary, you can inherit from it, getting all the existing functionality.

class UniversalApiController < ApplicationController before_action :prepare_model, only: [:index, :show, :create, :update, :destroy] before_action :find_record, only: [:show, :update, :destroy] def index @res = @model_class @res = @res.limit(params[:limit].to_i) if params[:limit] select_list = permitted_select_values @res = @res.select(select_list) if select_list @res = @res.ransack(params[:q]).result render json: @res end def show render json: @res end def create if @res = @model_class.create(permitted_params) render json: @res else invalid_resource!(@res) end end def update if @res.update_attributes(permitted_params) render json: @res else invalid_resource!(@res) end end def destroy @res.destroy raise @res.errors[:base].to_s unless @res.errors[:base].empty? render json: { success: true }, status: 204 end protected def permitted_select_values if params[:select] case params[:select] when String permitted_select_value params[:select] when Array params[:select].map { |field| permitted_select_value field }.compact end end end def permitted_select_value field @select_fields ||= @model_class.column_names + extra_select_values (@select_fields.include? field) ? field : nil end def extra_select_values [] end def permitted_params params.permit![_wrapper_options.name] params[_wrapper_options.name].extract! @model_class.primary_key params[_wrapper_options.name] end def get_model_name params[:model_name] || controller_name.classify end def prepare_model model_name = get_model_name raise "Model class not present" if model_name.nil? || model_name.strip == "" @model_class = model_name.constantize raise "Model class is not ActiveRecord" unless @model_class < ActiveRecord::Base end def find_record @res = @model_class.find(params[@model_class.primary_key.to_sym]) end end 

And a few entries in config / routes.rb:

 Rails.application.routes.draw do ... scope 'universal_api/:model_name', controller: 'universal_api' do get '/', action: 'index' get '/:id', action: 'show' post '/', action: 'create' put '/:id', action: 'update' delete '/:id', action: 'destroy' end ... end 

Now let's go in order:

 scope 'universal_api/:model_name', controller: 'universal_api' 

This line creates a separate scope for our universal API, and the model name will be available in the controller. This is supposed to be the name of the model class. Below is the routing for basic actions.

Controller:

  before_action :prepare_model, only: [:index, :show, :create, :update, :destroy] before_action :find_record, only: [:show, :update, :destroy] 

Before all actions, we need to prepare a model, and before showing, updating and deleting we still need to find a record.

The model name in the simplest case will be available in params [: model_name]. However, in the child class, the model name can usually be obtained from the controller name. In some cases, when Rails cannot adequately convert the controller name to the model name we need, you need to be able to specify it explicitly. Therefore, we will create a separate method that returns the name of the model and a separate method that converts it into a real class.

  def get_model_name params[:model_name] || controller_name.classify end def prepare_model model_name = get_model_name raise "Model class not present" if model_name.nil? || model_name.strip == "" @model_class = model_name.constantize raise "Model class is not ActiveRecord" unless @model_class < ActiveRecord::Base end 

Still, Ruby is a great language. And Rails is a great framework.

Usually a record for viewing, changing or deleting can be obtained by the primary key. However, sometimes (in large and with a large history of corporate systems, anything can happen) there is a need to get a record in a different way. For example, by a combination of several parameters. In this case, in the successor, we simply override this method.

  def find_record @res = @model_class.find(params[@model_class.primary_key.to_sym]) end 

Preparation is finished, now we will pass to specific actions.

Delete and view one record


The simplest actions, even nothing to add.

  def show render json: @res end ... def destroy @res.destroy raise @res.errors[:base].to_s unless @res.errors[:base].empty? render json: { success: true }, status: 204 end 

Getting all the records


  def index @res = @model_class @res = @res.limit(params[:limit].to_i) if params[:limit] select_list = permitted_select_values @res = @res.select(select_list) if select_list @res = @res.ransack(params[:q]).result render json: @res end 

Often we want to limit the number of records issued to the client, we do this with the help of params [: limit]. Also, the client does not always need all the fields of the model. We will limit the available values ​​in the columns in the table and allow descendants to add the necessary values ​​if necessary. The restriction is necessary because we use the select method, which allows you to use any string. That is, and subqueries, and in general any requests.

  def permitted_select_values if params[:select] case params[:select] when String permitted_select_value params[:select] when Array params[:select].map { |field| permitted_select_value field }.compact end end end def permitted_select_value field @select_fields ||= @model_class.column_names + extra_select_values (@select_fields.include? field) ? field : nil end def extra_select_values [] end 

In addition to the simple limit on the number of entries, very often you want to be able to use more intelligent restrictions. As well as sorting, grouping, etc. This is easily implemented using Ransack .

 @res = @res.ransack(params[:q]).result 
- this line gives us the opportunity to carry out a very complex search by model parameters, associations, and also provides the ability to sort and use the scope and methods of the model class .

Creating and editing a record:

  def create if @res = @model_class.create(permitted_params) render json: @res else invalid_resource!(@res) end end def update if @res.update_attributes(permitted_params) render json: @res else invalid_resource!(@res) end end 

In Rails, by default, mass filling of model attributes from parameters is prohibited. For our simple dictionary, this is most likely irrelevant. And for a more complex case, you can explicitly determine the list of attributes available for bulk filling.

  def permitted_params params.permit![_wrapper_options.name] params[_wrapper_options.name].extract! @model_class.primary_key params[_wrapper_options.name] end 

Creation and editing is done using the POST and PUT methods, respectively; data is transmitted in the request body. Rails kindly parses and wraps this data in a separate parameter.

Results


The above method allows to solve a rather narrow range of tasks - not so often in projects there is a need for such access to data. However, for a certain circle of developers, this will be very close and useful - the number of dummy files of the same type will be reduced to 4 (!!!) times, the source code will be freed from typical methods. And in general, it will not be necessary to once again think and reinvent the wheel to organize simple work with simple models.

If necessary, you can continue the thought a bit and realize the possibility of rendering with the help of separate views.

This article did not address access control issues. Usually, access control is the responsibility of a separate subsystem. Also I would like to separately note that not many technologies are used here from the entire Rails stack - ActiveRecord, ActiveSupport and a stand-alone Ransack. You can do this with little effort in Sinatra or another Rails framework.

Source: https://habr.com/ru/post/271243/


All Articles