cascade and inverse

本文详细解析了Hibernate框架中cascade与inverse属性的功能与使用场景,包括它们如何影响数据的一致性,以及在一对多和多对多关系中的具体应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、到底在哪用cascade="..."?



cascade属性并不是多对多关系一定要用的,有了它只是让我们在插入或删除对像时更方便一些,只要在cascade的源头上插入或是删除,所有 cascade的关系就会被自己动的插入或是删除。便是为了能正确的cascade,unsaved-value是个很重要的属性。Hibernate通 过这个属性来判断一个对象应该save还是update,如果这个对象的id是unsaved-value的话,那说明这个对象不是 persistence object要save(insert);如果id是非unsaved-value的话,那说明这个对象是persistence object(数据库中已存在),只要update就行了。saveOrUpdate方法用的也是这个机制。



2、到底在哪用inverse="ture"?

“set的inverse属性决定是否把对set的改动反映到数据库中去。inverse=false————反映;inverse=true————不反映”inverse属性默认为false



inverse属性默认是false的,就是说关系的两端都来维护关系。这个意思就是说,如有一个Student, Teacher和TeacherStudent表,Student和Teacher是多对多对多关系,这个关系由TeacherStudent这个表来表 现。那么什么时候插入或删除TeacherStudent表中的记录来维护关系呢?在用hibernate时,我们不会显示的对 TeacherStudent表做操作。对TeacherStudent的操作是hibernate帮我们做的。hibernate就是看hbm文件中指 定的是"谁"维护关系,那个在插入或删除"谁"时,就会处发对关系表的操作。前提是"谁"这个对象已经知道这个关系了,就是说关系另一头的对象已经set 或是add到"谁"这个对象里来了。前面说过inverse默认是false,就是关系的两端都维护关系,对其中任一个操作都会处发对表系表的操作。当在 关系的一头,如Student中的bag或set中用了inverse="true"时,那就代表关系是由另一关维护的(Teacher)。就是说当这插 入Student时,不会操作TeacherStudent表,即使Student已经知道了关系。只有当Teacher插入或删除时才会处发对关系表的 操作。所以,当关系的两头都用inverse="true"是不对的,就会导致任何操作都不处发对关系表的操作。当两端都是inverse= "false"或是default值是,在代码对关系显示的维护也是不对的,会导致在关系表中插入两次关系。



在一对多关系中inverse就更有意义了。在多对多中,在哪端inverse="true"效果差不多(在效率上)。但是在一对多中,如果要一方维护关 系,就会使在插入或是删除"一"方时去update"多"方的每一个与这个"一"的对象有关系的对象。而如果让"多"方面维护关系时就不会有update 操作,因为关系就是在多方的对象中的,直指插入或是删除多方对象就行了。当然这时也要遍历"多"方的每一个对象显示的操作修关系的变化体现到DB中。不管 怎样说,还是让"多"方维护关系更直观一些。

(1)对one-to-many而言,改变set,会让hibernate执行一系列的update语句, 不会delete/insert数据

(2)对many-to-many而言,改变set,只修改关系表的数据,不会影响many-to-many的另一方。

(3)虽然one-to-many和many-to-many的数据库操作不一样,但目的都是一个:维护数据的一致性。



3、cascade和inverse有什么区别?

可以这样理解,cascade定义的是关系两端对象到对象的级联关系;而inverse定义的是关系和对象的级联关系。

inverse只对set+one-to-many(或many-to-many)有效,对many-to-one, one-to-one无效。cascade对关系标记都有效。

inverse对集合对象整体起作用,cascade对集合对象中的一个一个元素起作用,如果集合为空,那么cascade不会引发关联操作。

比如将集合对象置为null, school.setStudentSet(null)

inverse导致hibernate执行:udpate STUDENT set SCHOOL_ID=null where SCHOOL_ID=?

cascade则不会执行对STUDENT表的关联更新, 因为集合中没有元素。

再比新增一个school, session.save(school)

inverse导致hibernate执行:

for( 对(school的每一个student ){

udpate STUDENT set SCHOOL_ID=? where STUDENT_ID=? //将学生的school_id改为新的school的id

}

cascade导致hibernate执行:

for( 对school的每一个student ){

session.save(aStudent); //对学生执行save操作

}

extends:如果改变集合中的部分元素(比如新增一个元素),

inverse: hibernate先判断哪些元素改变了,对改变的元素执行相应的sql

cascade: 它总是对集合中的每个元素执行关联操作。

(在关联操作中,hibernate会判断操作的对象是否改变)

两个起作用的时机不同:

cascade:在对主控方操作时,级联发生。

inverse: 在flush时(commit会自动执行flush),对session中的所有set,hibernate判断每个set是否有变化,

对有变化的set执行相应的sql,执行之前,会有个判断:if( inverse == true ) return;可以看出cascade在先,inverse在后。

inverse 对set + one-to-many 和 set + many-to-many 起的作用不同。hibernate生成的sql不同。

对one-to-many,hibernate对many方的数据库表执行update语句。

对many-to-many, hibernate对关系表执行insert/update/delte语句,注意不是对many方的数据库表而是关系表。

cascase 对set都是一致的,不管one-to-many还是many-to-many。都简单地把操作传递到set中的每个元素。所以它总是更新many方的数据库表。

4、cascade和inverse有什么相同?

这两个属性本身互不影响,但起的作用有些类似,都能引发对关系表的更新。

5、 建议:只对set + many-to-many设置inverse=false,其他的标记不考虑inverse属性,都设为inverse=true。对cascade,一 般对many-to-one,many-to-many,constrained=true的one-to-one 不设置级联删除。
修改以下代码,当存在不属于账号的草稿文档存在时,不阻止账号进入知识库页面,仅仅是将不属于该账号的知识从该账号的tree页面的列表中隐藏: from odoo import _, api, fields, models from odoo.exceptions import ValidationError, UserError, AccessError from odoo.osv import expression class DocumentPage(models.Model): """This class is use to manage Document.""" _name = "document.page" _inherit = ["mail.thread", "mail.activity.mixin"] _description = "Document Page" _order = "name" _HTML_WIDGET_DEFAULT_VALUE = "<p><br></p>" name = fields.Char("Title", required=True) type = fields.Selection( [("content", "Content"), ("category", "Category")], help="Page type", default="content", ) active = fields.Boolean(default=True) parent_id = fields.Many2one( "document.page", "Category", domain=[("type", "=", "category")] ) child_ids = fields.One2many("document.page", "parent_id", "Children") content = fields.Html( compute="_compute_content", inverse="_inverse_content", search="_search_content", sanitize=False, ) draft_name = fields.Char( string="Name", help="Name for the changes made", related="history_head.name", readonly=False, ) draft_summary = fields.Char( string="Summary", help="Describe the changes made", related="history_head.summary", readonly=False, ) template = fields.Html( help="Template that will be used as a content template " "for all new page of this category.", ) history_head = fields.Many2one( "document.page.history", "HEAD", compute="_compute_history_head", store=True, auto_join=True, ) history_ids = fields.One2many( "document.page.history", "page_id", "History", readonly=True, ) menu_id = fields.Many2one("ir.ui.menu", "Menu", readonly=True) content_date = fields.Datetime( "Last Contribution Date", related="history_head.create_date", store=True, index=True, readonly=True, ) content_uid = fields.Many2one( "res.users", "Last Contributor", related="history_head.create_uid", store=True, index=True, readonly=True, ) company_id = fields.Many2one( "res.company", "Company", help="If set, page is accessible only from this company", index=True, ondelete="cascade", default=lambda self: self.env.company, ) backend_url = fields.Char( string="Backend URL", help="Use it to link resources univocally", compute="_compute_backend_url", ) state = fields.Selection( selection=[ ('draft', '草稿'), ('under_review', '审核中'), ('approved', '已通过'), ('rejected', '已驳回') ], string='状态', default='draft', tracking=True, # 关键修复:启用邮件跟踪 index=True ) visible_department_ids = fields.Many2many( 'hr.department', string='可见部门', required=True, tracking=True ) create_uid = fields.Many2one( 'res.users', string='创建人', index=True # 直接在此处添加索引 ) department_manager_ids = fields.Many2many( 'res.users', string='部门管理员', compute='_compute_department_managers', store=False, # 不存储,实时计算 help="自动关联可见部门的管理员用户", index=True ) @api.depends("menu_id", "parent_id.menu_id") def _compute_backend_url(self): tmpl = "/web#id={}&model=document.page&view_type=form" for rec in self: url = tmpl.format(rec.id) # retrieve action action = None parent = rec while not action and parent: action = parent.menu_id.action parent = parent.parent_id if action: url += f"&action={action.id}" rec.backend_url = url @api.constrains("parent_id") def _check_parent_id(self): if not self._check_recursion(): raise ValidationError(_("You cannot create recursive categories.")) def _get_page_index(self, link=True): """Return the index of a document.""" self.ensure_one() index = [ "<li>" + subpage._get_page_index() + "</li>" for subpage in self.child_ids ] r = "" if link: r = f'<a href="{self.backend_url}">{self.name}</a>' if index: r += "<ul>" + "".join(index) + "</ul>" return r @api.depends("history_head") def _compute_content(self): for rec in self: if rec.type == "category": rec.content = rec._get_page_index(link=False) else: if rec.history_head: rec.content = rec.history_head.content else: # html widget's default, so it doesn't trigger ghost save rec.content = self._HTML_WIDGET_DEFAULT_VALUE def _inverse_content(self): for rec in self: if rec.type == "content" and rec.content != rec.history_head.content: rec._create_history( { "page_id": rec.id, "name": rec.draft_name, "summary": rec.draft_summary, "content": rec.content, } ) def _create_history(self, vals): self.ensure_one() return self.env["document.page.history"].create(vals) def _search_content(self, operator, value): return [("history_head.content", operator, value)] @api.depends("history_ids") def _compute_history_head(self): for rec in self: if rec.history_ids: rec.history_head = rec.history_ids[0] else: rec.history_head = False @api.onchange("parent_id") def _onchange_parent_id(self): """We Set it the right content to the new parent.""" if ( self.content in (False, self._HTML_WIDGET_DEFAULT_VALUE) and self.parent_id.type == "category" ): self.content = self.parent_id.template def unlink(self): menus = self.mapped("menu_id") res = super().unlink() menus.unlink() return res def copy(self, default=None): default = dict( default or {}, name=_("%s (copy)") % self.name, content=self.content, draft_name="1.0", draft_summary=_("summary"), ) return super().copy(default=default) def _compute_department_managers(self): for record in self: record.department_manager_ids = record.visible_department_ids.mapped('manager_id.user_id') @api.model def search(self, args, offset=0, limit=None, order=None, count=False): """优化后的搜索方法""" user = self.env.user if not user.has_group('base.group_system'): # 使用更精确的逻辑运算符 security_condition = expression.OR([ expression.AND([ [('state', 'not in', ['draft', 'under_review'])] ]), expression.AND([ [('state', '=', 'draft')], [('create_uid', '=', user.id)] ]), expression.AND([ [('state', '=', 'under_review')], expression.OR([ [('create_uid', '=', user.id)], [('visible_department_ids.manager_id.user_id', '=', user.id)] ]) ]) ]) args = expression.AND([args, security_condition]) return super().search(args, offset, limit, order, count) def check_access_rule(self, operation): """修复后的权限验证方法""" user = self.env.user # 先执行自定义校验逻辑 for rec in self: creator_id = rec.create_uid.id current_user_id = user.id # 草稿文档校验(优先处理) if rec.state == 'draft' and creator_id != current_user_id: raise AccessError(_("草稿文档访问限制|文档ID:%s") % rec.id) # 审核文档校验 if rec.state == 'under_review': manager_ids = rec.visible_department_ids.mapped('manager_id.user_id.id') if current_user_id not in manager_ids and creator_id != current_user_id: raise AccessError(_("审核文档权限不足")) # 最后执行父类基础验证 return super(DocumentPage, self).check_access_rule(operation)
最新发布
05-14
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值